anomalyco / anomalyco/opencode

[BUG]: structured output forces tool_choice "required", which newer Anthropic models reject with a 400

Open
#46,735 2 comments 2 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Sep 2, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

Description

SessionPrompt.loop sets toolChoice: "required" whenever a request carries format: { type: "json_schema" }:

toolChoice: format.type === "json_schema" ? "required" : undefined,

The Anthropic adapter lowers required to {"type":"any"}, and claude-fable-5-1 rejects that outright. Every structured-output request against it fails.

This is the same defect as #15226, which was closed by the inactivity bot in July with "if the issue is still relevant please open a new one" — it is, so here it is with a live reproduction. The fix PR for it, #29565, was closed unmerged by the automated PR cleanup without a maintainer review.

What I measured

Direct API probe. auto and none are controls — they pass, which is what makes the failures attributable to tool_choice rather than to auth or request shape:

model auto none {"type":"any"} {"type":"tool",name}
claude-fable-5-1 200 200 400 400
claude-fable-5 200 200 200 200
tool_choice: type "tool" and "any" are not supported for this model.

So this is model-gated, not an API-wide change — claude-fable-5 accepts all four.

Then end-to-end through the AI SDK with opencode's exact shape (a StructuredOutput tool plus toolChoice: "required"), capturing the outgoing body:

claude-fable-5-1: FAILED   wire tool_choice={"type":"any"}
    tool_choice: type "tool" and "any" are not supported for this model.
claude-fable-5:   200 OK   wire tool_choice={"type":"any"}

The chain is required{"type":"any"} → 400, in one run rather than inferred across two.

Not Anthropic-specific

#15226 collected the same failure across providers, each with its own error string:

  • Kimi K2.5 / Moonshot — tool_choice 'required' is incompatible with thinking enabled
  • DeepSeek v4 flash — Thinking mode does not support this tool_choice
  • Qwen3.5-9B
  • Anthropic with thinking: true — reported in that thread and now hard-failing on 5-1

The common factor is thinking-enabled models rejecting a forced tool choice. The newer Anthropic models make it unconditional rather than conditional on a thinking flag.

Suggested fix

The AI SDK's Anthropic adapter already supports native structured output and uses it in preference to a synthetic tool. generateObject against both models emits no tool and no tool choice at all:

wire keys: [model, max_tokens, output_config, messages, system]
tools=[]  tool_choice=undefined     -> 200 on claude-fable-5-1 and claude-fable-5

So the model-agnostic fix is for the json_schema path to use the provider's native structured output where available, instead of hand-rolling a StructuredOutput tool and forcing a choice. That removes the conflict everywhere at once and needs no per-model capability tracking. The tool-based path stays as the fallback for providers without native support.

A narrower alternative — downgrading required to auto when the model reasons — is what #29565 implemented (compatibleToolChoice in session/llm.ts, credit @serejaris). It is a smaller diff and would fix the 400. Worth noting it does weaken the guarantee: with auto the model may not call the StructuredOutput tool at all, so the request can come back without structured output and lean on format.retryCount, which #25430 reports as unwired. Native structured output gets both properties at once.

Happy to open the PR for either shape — say which you'd prefer and I'll build it.

Steps to reproduce
  1. Send a session prompt with format: { type: "json_schema", schema: {...} }
  2. Target anthropic/claude-fable-5-1
  3. The request fails with tool_choice: type "tool" and "any" are not supported for this model.

Substituting claude-fable-5 succeeds, which isolates it to the model.

Additional context

One adjacent fragility, for context rather than as a second ask: the Anthropic adapter decides whether to use native structured output from getModelCapabilities(modelId), and claude-fable-5-1 only qualifies because it matches the claude-fable-5 entry as a substring. A future model id that matches no known prefix would fall through to the forced-tool path, which would turn agent.ts's generateObject/streamObject calls into the same 400. That is upstream in @ai-sdk/anthropic, not here.

Also related: #45953 (same forced-choice class in the v2 SessionRunner, different code path) and #37672.

OpenCode version

dev; probed against claude-fable-5-1 and claude-fable-5 on 2026-09-02.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.