Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/provider/opencode/client.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
;;; Autolith's private store with an optional OPENCODE_API_KEY environment
;;; bootstrap. Tool names ride in the Chat Completions Base64 encoding
;;; inherited from OPENAI-COMPATIBLE-PROVIDER, so conversations persist in
;;; the same namespaced shape regardless of the serving provider.
;;; the same namespaced shape regardless of the serving provider. Every
;;; request also carries the provider's stable session identifier in the
;;; x-opencode-session header so the backend can pin caching.

(defparameter *opencode-model-prefix* "opencode/"
"The namespace distinguishing OpenCode models from other provider models.")
Expand Down Expand Up @@ -56,14 +58,19 @@

(-> opencode-provider-create (configuration) opencode-chat-completions-provider)
(defun opencode-provider-create (configuration)
"Create the OpenCode API key provider for CONFIGURATION."
(make-instance 'opencode-chat-completions-provider
:configuration configuration
:credential-manager (opencode-credential-manager-create
configuration)
:session-id (make-identifier)
:display-name "OpenCode"
:family ':opencode))
"Create the OpenCode API key provider for CONFIGURATION.

The provider's stable session identifier also rides as the x-opencode-session
request header so the backend can pin caching to one session."
(let ((session-id (make-identifier)))
(make-instance 'opencode-chat-completions-provider
:configuration configuration
:credential-manager (opencode-credential-manager-create
configuration)
:session-id session-id
:headers (list (cons "x-opencode-session" session-id))
:display-name "OpenCode"
:family ':opencode)))

(defmethod provider-request-object :around
((provider opencode-chat-completions-provider)
Expand Down
71 changes: 71 additions & 0 deletions tests/opencode-provider-tests.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,76 @@
(uiop:delete-directory-tree root :validate t :if-does-not-exist ':ignore)))
nil)

(-> opencode-provider-test--session-header () null)
(defun opencode-provider-test--session-header ()
"Test that OpenCode transport sends its stable session caching header."
(let* ((registry-snapshot (provider--registry-snapshot))
(configuration (opencode-provider-test--configuration))
(root (test-configuration-root configuration))
(conversation
(conversation-create configuration :identifier "opencode-session"))
(provider (opencode-provider-create configuration))
(credentials
(make-instance 'oauth-credentials
:access-token "synthetic-opencode-key"
:account-id "opencode"
:source-path #P"/tmp/opencode-test-key"))
(captured-headers nil))
(unwind-protect
(progn
(register-provider
"opencode-session-test"
:family ':opencode
:models '("opencode/session-test")
:endpoint *opencode-chat-completions-endpoint*
:factory
(lambda (selected &key reasoning-summaries-p)
(declare (ignore reasoning-summaries-p))
(opencode-provider-create selected))
:source ':runtime)
(test-call-with-function-replacements
(list
(list 'dexador:post
(lambda (url &key headers &allow-other-keys)
(declare (ignore url))
(setf captured-headers headers)
(values (make-string-input-stream "") 200 nil nil))))
(lambda ()
(provider-open-response-stream
provider
(json-object "model" "gpt-5.6-luna")
:credentials credentials
:conversation conversation)))
(test-assert
(string= (rest (assoc "x-opencode-session" captured-headers
:test #'string-equal))
(provider-session-id provider))
"OpenCode transport sends the stable session caching header")
(test-assert
(string= (rest (assoc "x-opencode-session"
(openai-compatible-provider-headers provider)
:test #'string-equal))
(provider-session-id provider))
"the OpenCode session header carries the provider session identity")
(let ((reconfigured
(provider-with-configuration
provider
(configuration-with-model
configuration "opencode/session-test"))))
(test-assert
(equal (openai-compatible-provider-headers reconfigured)
(list (cons "x-opencode-session"
(provider-session-id provider))))
"OpenCode reconfiguration preserves the session caching header")
(test-assert
(string= (provider-session-id reconfigured)
(provider-session-id provider))
"OpenCode reconfiguration preserves the session identity")))
(provider--registry-restore registry-snapshot)
(uiop:delete-directory-tree root :validate t :if-does-not-exist ':ignore)))
nil)


(-> opencode-provider-test--discovery () null)
(defun opencode-provider-test--discovery ()
"Test OpenCode discovery namespacing, collisions, caching, and failures."
Expand Down Expand Up @@ -615,6 +685,7 @@
(opencode-provider-test--login)
(opencode-provider-test--authentication-bootstrap)
(opencode-provider-test--request-model)
(opencode-provider-test--session-header)
(opencode-provider-test--discovery)
(opencode-provider-test--legacy-registry-snapshot)
(opencode-provider-test--builtin-registration)
Expand Down