diff --git a/src/provider/opencode/client.lisp b/src/provider/opencode/client.lisp index 0d00ad48..0aae48a9 100644 --- a/src/provider/opencode/client.lisp +++ b/src/provider/opencode/client.lisp @@ -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.") @@ -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) diff --git a/tests/opencode-provider-tests.lisp b/tests/opencode-provider-tests.lisp index 2a87e33c..504708ae 100644 --- a/tests/opencode-provider-tests.lisp +++ b/tests/opencode-provider-tests.lisp @@ -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." @@ -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)