Skip to content

provider: thinking.block_binding injected unconditionally for anthropic-compatible providers, no config to disable #55160

Open

Description

@gabrielferreira-cit

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

  1. 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": "..." }
    }
  }
}
  1. Discover/use any model with id claude-sonnet-5-5 (also affects claude-opus-5-5, claude-haiku-5-5; version ≥ 5.1 triggers the injection).
  2. Send any message with the native adaptive thinking path (no body override):
opencode run --model flow-anthropic/claude-sonnet-5-5 "Say OK"
  1. 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_binding should not be injected for non-official anthropic-compatible providers by default (the version-based heuristic sk() 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 V2 ProviderConfig/model schema does not accept/expose compatibility or document the thinking.blockBinding model option, and excess properties are ignored silently.
  • The effective injection happens in the Anthropic Messages request builder: for models matching version ≥ 5.1, thinking is rewritten to { type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } } regardless of provider type.
  • body.thinking in provider config cannot override it, because the request-builder rewrites thinking after merging provider body.

Additional Context

What we verified

  • Plain curl to the same proxy with thinking: {type: "adaptive"} (no block_binding) → succeeds.
  • Plain curl with thinking: {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)

  1. 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()
    }
  },
}
  1. 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

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    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_binding for 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 compatibility setting rather than options.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 out block_binding and the thinking-binding-controls-2026-08-01 beta. 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:

    The 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.

  2. gabrielferreira-cit commented on Oct 13, 2026

    @gabrielferreira-cit
    Author

    Thanks for the pointer — confirming the per-model compatibility.supportsThinkingBlockBinding: false works 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:

    • With model discovery enabled, the per-model override defeats the discovery for any new Claude ≥ 5.1 model: it appears in the model list but 400s on first use until a manual entry is added. The provider-wide switch would make discovery zero-touch again for third-party Anthropic-compatible endpoints.
    • The internal plumbing already exists: the binary defines a Provider.Compatibility type with exactly supportsThinkingBlockBinding / supportsEffortUpdates fields — exposing it in provider config looks like a small change.
    • There's no real use case for per-model granularity here: whether the backend accepts the block_binding beta field is a property of the endpoint, not of the model. (First-party Anthropic accepts it everywhere; a given proxy/backend rejects it everywhere.)
  3. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    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 maybe supportsEffortUpdates) be set once in the provider-level compatibility, 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-level compatibility was 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.request plugin from the issue description is a working provider-wide workaround, because it covers every model, including newly discovered ones. If you add a check like if (!req.url.startsWith("https://<litellm-proxy>/")) return, it only touches requests to that proxy.

    Related:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions