anomalyco / anomalyco/opencode
[BUG]: structured output forces tool_choice "required", which newer Anthropic models reject with a 400
@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
- Send a session prompt with
format: { type: "json_schema", schema: {...} } - Target
anthropic/claude-fable-5-1 - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.