AtlatestRepositoryslate
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 ollama13
(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* ((request37
(openai-request38
"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-equal60
(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-equal73
(dict-ref (dict-ref (car calls) function: #{}) name: "")74
"write")))76
(test "assistant text and usage normalize into unified records"77
(let ((result78
(openai-parse-response79
"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-sink90
(lambda (text) (vector-set! deltas 0 (append (vector-ref deltas 0) (list text))))91
(lambda ()92
(openai-stream-feed! collector93
"{\"choices\":[{\"delta\":{\"content\":\"hel\",\"tool_calls\":[{\"index\":0,\"id\":\"call-1\",\"function\":{\"name\":\"wri\",\"arguments\":\"{\\\"path\\\":\"}}]}}]}" )94
(openai-stream-feed! collector95
"{\"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! collector115
"{\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":null,\"id\":\"call-null\",\"function\":{\"name\":\"read\",\"arguments\":null}}]}}]}")116
(openai-stream-feed! collector117
"{\"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* ((result126
(openai-parse-response127
"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 ((result136
(openai-parse-response137
"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 path155
"#{ 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")))