Repository navigation
feat: opencode v2 plugin support + thinking-binding-controls beta for 5.x models - #37
Open
TomasRichtr wants to merge 2 commits into
Open
TomasRichtr wants to merge 2 commits into
TomasRichtr wants to merge 2 commits into
Conversation
added 2 commits
October 16, 2026 09:22
Claude Code >= 2.1.2xx sends thinking-binding-controls-2026-08-01. OpenCode v2 natively adds thinking.block_binding to requests for models flagged supportsThinkingBlockBinding (claude-haiku-5-5 and other 5.x models). The API only accepts that field under the new beta, so with the old flag set those requests fail with: thinking.adaptive.block_binding: Extra inputs are not permitted Ref: dotCipher#33
OpenCode 2.0 loads plugins through a new contract: the default export must
carry an id plus a setup() (Promise) or effect() (Effect) function, and V1
hooks never run there — the plugin fails with:
Plugin must export a default definition with an id and an effect or
setup function
This adds a v2 setup() next to the existing v1 server() (which opencode
1.18.x keeps using) and maps the same behavior onto the v2 APIs:
- auth.loader custom fetch -> session.hook("http.request")
- auth.loader response wrapping -> session.hook("http.response")
- experimental.chat.system.transform -> session.hook("context")
- auth.methods OAuth -> integration.transform with an oauth
method override on the anthropic integration: authorize (Claude CLI
keychain fast path, then browser PKCE with the local callback server)
and a layered refresh callback whose result opencode persists
Divergences under v2 (documented in the source): no model injection (the
v2 catalog already lists Claude models), no inline 429 token rotation
(Claude refresh tokens are single-use — only the persisted refresh
callback may rotate them), and rewrites apply to anthropic.com hosts
only.
The V1 fetch's body/URL/response phases are extracted into shared helpers
(transformClaudeRequestBody, normalizeAnthropicApiUrl,
wrapAnthropicResponseWithSse) so v1 and v2 share one wire-format
implementation; refreshAuth is split into a pure refreshTokensLayered so
the v2 refresh callback can return tokens for opencode to persist.
Closes dotCipher#34
Ref: dotCipher#33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two fixes for running this plugin on OpenCode 2.x, both hit in practice on opencode
2.0.30with Claude Pro/Max OAuth:thinking-binding-controls-2026-08-01beta flag (ref Beta header rewrite breaks Opus 5.5, Sonnet 5.5 and Fable 5.1 on OpenCode 1.18.x #33) — OpenCode v2 natively addsthinking.block_bindingto requests for models flaggedsupportsThinkingBlockBinding(claude-haiku-5-5 and other 5.x models). The API only accepts that field under the new beta, so with the old flag set those requests fail with:Changes
fix:commit (constants.ts)thinking-binding-controls-2026-08-01toBETA_FLAGS, with a comment on how to re-derive the flag list from a Claude Code install (grep -aoE ...over the binary).feat:commit (index.ts, tests, README, package.json)setup()next toserver()on the default export ({ id, setup, server }). opencode ≥ 2.0 usessetup()and ignoresserver(); 1.18.x keeps usingserver(). No new runtime dependencies.auth.loadercustom fetch →session.hook("http.request")(header/body/URL rewrite, provider-scoped toanthropic)auth.loaderresponse wrapping →session.hook("http.response")(SSE tool-name/argument translation)experimental.chat.system.transform→session.hook("context")auth.methodsOAuth →integration.transformregistering anoauthmethod override on theanthropicintegration:authorize(Claude CLI keychain fast path, then browser PKCE via the existing local callback server) and a layeredrefreshcallback whose result opencode persiststransformClaudeRequestBody,normalizeAnthropicApiUrl,wrapAnthropicResponseWithSse) so v1 and v2 share one wire-format implementation; splitsrefreshAuthinto a purerefreshTokensLayered(the v2 refresh callback must return tokens rather than write the auth store itself, since v2 has noclient.auth.setequivalent — opencode persists what the callback returns).refreshcallback invalidates the stored token), and rewrites apply toanthropic.comhosts only.plugin module shapecase asserts the{ id, setup, server }shape, that the method override targetsanthropic/oauthwith asyncauthorize/refresh(a plain-object return crashes v2's credential resolver — that one cost a debugging session), and that the three provider-scoped hooks register.Testing
npm run typecheckandnpm testclean (123 tests).2.0.30/ macOS against a real Claude Pro account: plugin loads without the v2 error, expired stored credentials auto-refresh through the keychain layer and the refreshed credential is persisted, and requests on bothclaude-haiku-4-5andclaude-haiku-5-5succeed (the latter previously 400ing onblock_binding).Versioning
Bumped to
1.11.0—mainalready requires the{ id, server }entrypoint (opencode ≥ 1.18.29), so addingsetupbreaks nothing that currently works. If you'd rather ship the v2 support as2.0.0(e.g. to drop the V1 path eventually), happy to change it.