Repository navigation
Description
Summary
OpenCode injects thinking.block_binding into every Anthropic Messages request for Claude models ≥ 5.1 (inferred from the model id), with no way to disable it through V2 provider config. Backends that do not accept this beta field — e.g. an Anthropic-compatible provider routed through LiteLLM to an Azure deployment — reject every request with a 400 invalid_request_error (thinking.adaptive.block_binding: Extra inputs are not permitted), making the provider unusable for these models.
This is analogous to issue #52617 (unconditional store field with no config to disable it).
Environment
- opencode version: 2.0.28
- OS: Linux x86_64 (Ubuntu 24.04)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/zsh
- Install/channel: latest
- Active plugins:
opencode-models-discovery@latest,superpowers@git+https://github.com/obra/superpowers.git, plus two local plugin files under~/.config/opencode/plugins/(one of them is the workaround described below)
Reproduction
- Configure an Anthropic-compatible provider whose baseURL points to a LiteLLM proxy that forwards to an Azure OpenAI-ish Anthropic deployment (Azure rejects unknown thinking fields):
// opencode.jsonc
{
"providers": {
"flow-anthropic": {
"name": "Flow Anthropic",
"package": "@opencode/ai/providers/anthropic-compatible",
"settings": { "baseURL": "https://<litellm-proxy>/v1", "apiKey": "..." }
}
}
}- Discover/use any model with id
claude-sonnet-5-5(also affectsclaude-opus-5-5,claude-haiku-5-5; version ≥ 5.1 triggers the injection). - Send any message with the native adaptive thinking path (no
bodyoverride):
opencode run --model flow-anthropic/claude-sonnet-5-5 "Say OK"- Every request fails with:
litellm.BadRequestError: Azure_aiException - {"type":"error","error":{"type":"invalid_request_error","message":"thinking.adaptive.block_binding: Extra inputs are not permitted"}}
The same request body without the thinking.block_binding field succeeds against the same backend (verified with plain curl), so the field is the sole cause.
Expected Behavior
One of:
thinking.block_bindingshould not be injected for non-officialanthropic-compatibleproviders by default (the version-based heuristicsk()assumes a first-party Anthropic endpoint that understands the beta field), or- there should be a documented V2 config switch to disable the injection per provider or per model.
Actual Behavior
The injection is unconditional for Claude models ≥ 5.1 and there is no exposed config control:
- The binary builds model compatibility via
options?.thinking?.blockBinding === false → compatibility.supportsThinkingBlockBinding = false(the same mechanism used internally for Bedrock), but the V2ProviderConfig/model schema does not accept/exposecompatibilityor document thethinking.blockBindingmodel option, and excess properties are ignored silently. - The effective injection happens in the Anthropic Messages request builder: for models matching version ≥ 5.1,
thinkingis rewritten to{ type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } }regardless of provider type. body.thinkingin provider config cannot override it, because the request-builder rewritesthinkingafter merging providerbody.
Additional Context
What we verified
- Plain
curlto the same proxy withthinking: {type: "adaptive"}(noblock_binding) → succeeds. - Plain
curlwiththinking: {type: "adaptive", block_binding: {...}}→ the exact 400 error above. So the field is the sole trigger. - Claude ≥ 5.1 models are unusable through this provider on stock OpenCode.
Workarounds (currently in use)
- Plugin hook stripping the field (works, keeps native adaptive thinking):
// plugins/flow-strip-block-binding.ts
export default {
id: "flow-strip-block-binding",
async setup(ctx) {
const registration = await ctx.session.hook("http.request", async (evt) => {
const req = evt.request
if (!req.body) return
let body
try {
body = JSON.parse(await req.clone().text())
} catch {
return
}
const thinking = body.thinking
if (!thinking || thinking.type !== "adaptive" || !("block_binding" in thinking)) return
const { block_binding, ...rest } = thinking
body.thinking = rest
const headers = new Headers(req.headers)
headers.delete("content-length")
evt.request = new Request(req.url, {
method: req.method,
headers,
body: JSON.stringify(body),
})
})
return async () => {
await registration.dispose()
}
},
}- Provider-level
body.thinking: { type: "enabled", budget_tokens: N }— forces the legacy manual mode, which skips the injection. Downside: loses adaptive thinking (thinking is billed on every request, no per-request adaptivity), so it is not equivalent.
Both workarounds would be removable if the config switch is exposed (and/or the default for third-party anthropic-compatible providers is changed).
Request
Please expose a native way to disable block_binding injection in V2 config (e.g. per-model options.thinking.blockBinding: false documented and honored from providers.<id>.models, or a provider-level toggle), or stop injecting it for non-first-party anthropic-compatible providers. Happy to provide further traces if useful.
Activity
opencode-agent commentedon Oct 13, 2026
Thanks for the detailed report and for testing it with curl.
I think this is a duplicate of #48757. That issue asked for a V2 opt-out from
thinking.block_bindingfor Anthropic-compatible endpoints that reject it. It was fixed in v2.0.25 (#52327, #52172), and you're on 2.0.28, so you already have the setting.It's a per-model
compatibilitysetting rather thanoptions.thinking.blockBinding, which was the V1 key:{ "providers": { "flow-anthropic": { "name": "Flow Anthropic", "package": "@opencode/ai/providers/anthropic-compatible", "settings": { "baseURL": "https://<litellm-proxy>/v1", "apiKey": "..." }, "models": { "claude-sonnet-5-5": { "compatibility": { "supportsThinkingBlockBinding": false } }, "claude-opus-5-5": { "compatibility": { "supportsThinkingBlockBinding": false } }, "claude-haiku-5-5": { "compatibility": { "supportsThinkingBlockBinding": false } } } } } }With this set, the request should keep
thinking: { type: "adaptive" }but leave outblock_bindingand thethinking-binding-controls-2026-08-01beta. That means you shouldn't need the plugin workaround anymore. This setting can't be set for a whole provider at the moment; it's per model only.Possible duplicates and related issues, ordered by likelihood:
block_binding), fixed in v2.0.25blockBinding: falseopt-out stopped working after the move to V2 (fixed)thinking.adaptive.block_binding: Extra inputs are not permittederror in v1, including through LiteLLMstorefieldThe bot will close this issue as a duplicate in 1 day unless you reply. Please reply if the per-model setting doesn't work for you on 2.0.28, or if you need something different, such as a provider-wide switch.
gabrielferreira-cit commentedon Oct 13, 2026
Thanks for the pointer — confirming the per-model
compatibility.supportsThinkingBlockBinding: falseworks on 2.0.28 (tested, plugin workaround removed).That said, I'd like to advocate for the provider-wide switch as a follow-up, since it's not currently possible:
Provider.Compatibilitytype with exactlysupportsThinkingBlockBinding/supportsEffortUpdatesfields — exposing it in provider config looks like a small change.block_bindingbeta field is a property of the endpoint, not of the model. (First-party Anthropic accepts it everywhere; a given proxy/backend rejects it everywhere.)opencode-agent commentedon Oct 13, 2026
Thanks for testing it and for confirming the per-model setting works on 2.0.28.
That leaves a separate request: let
supportsThinkingBlockBinding: false(and maybesupportsEffortUpdates) be set once in the provider-levelcompatibility, so newly discovered Claude 5.1+ models on a third-party Anthropic-compatible endpoint work without adding an entry for each one. Your point that this depends on the endpoint, not the model, makes sense. For context, provider-levelcompatibilitywas added in #54911, and these two fields were left as model-only on purpose. Allowing them on the provider would mean revisiting that choice. Maintainers will consider it.Until then, your
http.requestplugin from the issue description is a working provider-wide workaround, because it covers every model, including newly discovered ones. If you add a check likeif (!req.url.startsWith("https://<litellm-proxy>/")) return, it only touches requests to that proxy.Related:
compatibilityand kept these fields model-onlyClaude through GitHub Copilot works again
Engine: opencode 2.0.30
Engine: opencode 2.0.31