AtlatestRepositoryslate

slate / tree / packages / slate-cli / testtest-agent-provider.sgl

1(import (sigil core)
2 (sigil string)
3 (sigil fs)
4 (sigil path)
5 (sigil process)
6 (sigil time)
7 (sigil test)
8 (slate-cli agent provider)
9 (slate-cli agent config)
10 (slate-cli agent openai-completions))
12(define ollama
13 (make-provider "ollama" openai-completions:
14 "http://127.0.0.1:11434/v1" #{ type: none: }
15 (list "qwen-test")))
17(test-group "provider-neutral records"
18 (test "provider and message records retain their portable fields"
19 (assert-equal (provider-id ollama) "ollama")
20 (assert-equal (provider-api ollama) openai-completions:)
21 (assert-equal (dict-ref (make-user-message "hello") role: "") "user"))
23 (test "Codex Sol defaults to low reasoning effort"
24 (let* ((config (default-agent-config))
25 (provider (agent-config-provider config "openai-codex")))
26 (assert-equal (provider-default-reasoning-effort provider) "low")))
28 (test "typed errors retain kind and context"
29 (let ((e (agent-error 'connection "refused" #{ turn: "turn-7" })))
30 (assert-true (agent-error? e))
31 (assert-equal (agent-error-kind e) 'connection)
32 (assert-equal (dict-ref (agent-error-context e) turn: "") "turn-7"))))
34(test-group "OpenAI-compatible request and response"
35 (test "request enables streaming usage and includes tools"
36 (let* ((request
37 (openai-request
38 "qwen-test" (list (make-user-message "write it"))
39 (list #{ name: "write" description: "write a file"
40 parameters: #{ type: "object" } })))
41 (messages (dict-ref request messages: '()))
42 (tools (dict-ref request tools: '())))
43 (assert-true (dict-ref request stream: #f))
44 (assert-true (dict-ref (dict-ref request stream_options: #{}) include_usage: #f))
45 (assert-equal (dict-ref request model: "") "qwen-test")
46 (assert-equal (dict-ref (car messages) content: "") "write it")
47 (assert-equal (dict-ref (dict-ref (car tools) function: #{}) name: "") "write")))
49 (test "assistant tool calls and tool results survive the next request round"
50 (let* ((assistant (make-assistant-message ""
51 (list (make-tool-call "call-7" "write" #{ path: "x" }))
52 (make-usage 1 1 0 2)))
53 (tool-result #{ role: "tool" content: "ok" name: "write"
54 tool-call-id: "call-7" })
55 (messages (dict-ref (openai-request "m" (list assistant tool-result) '())
56 messages: '()))
57 (assistant-wire (car messages))
58 (result-wire (cadr messages)))
59 (assert-equal
60 (dict-ref (dict-ref (car (dict-ref assistant-wire tool_calls: '()))
61 function: #{}) name: "")
62 "write")
63 (assert-equal (dict-ref result-wire tool_call_id: "") "call-7")))
65 (test "JSON-reloaded assistant tool-call arrays survive context rebuild"
66 (let* ((assistant #{ role: "assistant" content: "done"
67 tool-calls: #[ #{ id: "call-8" name: "write"
68 arguments: #{ path: "x" } } ] })
69 (messages (dict-ref (openai-request "m" (list assistant) '())
70 messages: '()))
71 (calls (dict-ref (car messages) tool_calls: '())))
72 (assert-equal
73 (dict-ref (dict-ref (car calls) function: #{}) name: "")
74 "write")))
76 (test "assistant text and usage normalize into unified records"
77 (let ((result
78 (openai-parse-response
79 "turn-1"
80 "{\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"hello\"}}],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":2,\"total_tokens\":5,\"prompt_tokens_details\":{\"cached_tokens\":1}}}")))
81 (assert-false (agent-error? result))
82 (assert-equal (dict-ref result content: "") "hello")
83 (assert-equal (dict-ref (dict-ref result usage: #{}) input: 0) 3)
84 (assert-equal (dict-ref (dict-ref result usage: #{}) cached: 0) 1)))
86 (test "SSE deltas assemble text, fragmented tool calls, and final usage"
87 (let* ((collector (make-openai-stream-collector "turn-stream"))
88 (deltas (vector '())))
89 (with-agent-delta-sink
90 (lambda (text) (vector-set! deltas 0 (append (vector-ref deltas 0) (list text))))
91 (lambda ()
92 (openai-stream-feed! collector
93 "{\"choices\":[{\"delta\":{\"content\":\"hel\",\"tool_calls\":[{\"index\":0,\"id\":\"call-1\",\"function\":{\"name\":\"wri\",\"arguments\":\"{\\\"path\\\":\"}}]}}]}" )
94 (openai-stream-feed! collector
95 "{\"choices\":[{\"delta\":{\"content\":\"lo\",\"tool_calls\":[{\"index\":0,\"function\":{\"name\":\"te\",\"arguments\":\"\\\"x\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":1,\"total_tokens\":3}}")))
96 (let* ((result (openai-stream-result collector))
97 (call (car (dict-ref result tool-calls: '()))))
98 (assert-equal (vector-ref deltas 0) '("hel" "lo"))
99 (assert-equal (dict-ref result content: "") "hello")
100 (assert-equal (dict-ref call name: "") "write")
101 (assert-equal (dict-ref (dict-ref call arguments: #{}) path: "") "x")
102 (assert-equal (dict-ref (dict-ref result usage: #{}) total: 0) 3))))
104 (test "empty SSE data and terminal markers are harmless"
105 (let ((collector (make-openai-stream-collector "turn-keepalive")))
106 (openai-stream-feed! collector " ")
107 (openai-stream-feed! collector "[DONE]")
108 (let ((result (openai-stream-result collector)))
109 (assert-false (agent-error? result))
110 (assert-equal (dict-ref result content: "missing") ""))))
112 (test "nullable OpenAI-compatible tool fragments are normalized"
113 (let ((collector (make-openai-stream-collector "turn-null-tool")))
114 (openai-stream-feed! collector
115 "{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":null,\"id\":\"call-null\",\"function\":{\"name\":\"read\",\"arguments\":null}}]}}]}")
116 (openai-stream-feed! collector
117 "{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":null,\"function\":{\"name\":null,\"arguments\":\"{}\"}}]}}]}")
118 (let* ((result (openai-stream-result collector))
119 (call (car (dict-ref result tool-calls: '()))))
120 (assert-false (agent-error? result))
121 (assert-equal (dict-ref call id: "") "call-null")
122 (assert-equal (dict-ref call name: "") "read"))))
124 (test "tool calls decode their JSON arguments"
125 (let* ((result
126 (openai-parse-response
127 "turn-2"
128 "{\"choices\":[{\"message\":{\"content\":null,\"tool_calls\":[{\"id\":\"call-1\",\"type\":\"function\",\"function\":{\"name\":\"write\",\"arguments\":\"{\\\"path\\\":\\\"demo.txt\\\"}\"}}]}}],\"usage\":{}}"))
129 (call (car (dict-ref result tool-calls: '()))))
130 (assert-equal (dict-ref call id: "") "call-1")
131 (assert-equal (dict-ref call name: "") "write")
132 (assert-equal (dict-ref (dict-ref call arguments: #{}) path: "") "demo.txt")))
134 (test "corrupt tool-call JSON is typed and names the turn"
135 (let ((result
136 (openai-parse-response
137 "turn-corrupt"
138 "{\"choices\":[{\"message\":{\"tool_calls\":[{\"id\":\"bad-1\",\"function\":{\"name\":\"write\",\"arguments\":\"{broken\"}}]}}]}")))
139 (assert-true (agent-error? result))
140 (assert-equal (agent-error-kind result) 'tool-arguments-parse)
141 (assert-equal (dict-ref (agent-error-context result) turn: "") "turn-corrupt")))
143 (test "missing choices is a typed response-shape error"
144 (let ((result (openai-parse-response "turn-empty" "{\"choices\":[]}")))
145 (assert-true (agent-error? result))
146 (assert-equal (agent-error-kind result) 'response-shape))))
148(test-group "providers.sgv"
149 (test "reader loads nested Sigil values without evaluation"
150 (let* ((dir (string-append "/tmp/slate-agent-config-read-"
151 (number->string (current-jiffy))))
152 (path (agent-providers-path dir)))
153 (ensure-directory (path-dirname path))
154 (write-file-string path
155 "#{ version: 1 providers: (#{ id: \"local\" api: openai-completions: base-url: \"http://localhost:9/v1\" auth: #{ type: none: } models: (\"tiny\") }) }")
156 (let* ((config (agent-config-load dir))
157 (provider (agent-config-provider config "local")))
158 (assert-false (agent-error? config))
159 (assert-equal (provider-base-url provider) "http://localhost:9/v1")
160 (assert-equal (car (provider-models provider)) "tiny"))
161 (delete-file path)
162 (delete-directory (path-dirname path))
163 (delete-directory dir)))
165 (test "providers.sgv is v2, declarative, and secret-free"
166 (let* ((config (default-agent-config))
167 (provider (agent-config-provider config "opencode-go")))
168 (assert-equal (dict-ref config version: 0) 2)
169 (assert-true (agent-config-secret-free? config))
170 (assert-equal (provider-wire-family provider) openai-completions:)
171 (assert-equal (provider-base-url provider) "https://opencode.ai/zen/go/v1")
172 (assert-equal (car (provider-models provider)) "deepseek-v4-flash")
173 (assert-equal (provider-default-model provider) "deepseek-v4-flash")
174 (assert-true (dict-ref (provider-capabilities provider) tools: #f)))))
176(test "built-in OpenAI catalog includes Astra alongside the existing default"
177 (let ((provider (agent-config-provider (default-agent-config) "openai-codex")))
178 (assert-true (and (member "gpt-6-astra" (provider-models provider)) #t))
179 (assert-equal (provider-default-model provider) "gpt-5.6-sol")))