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.
Description
SessionPrompt.loopsetstoolChoice: "required"whenever a request carriesformat: { type: "json_schema" }:The Anthropic adapter lowers
requiredto{"type":"any"}, andclaude-fable-5-1rejects 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.
autoandnoneare controls — they pass, which is what makes the failures attributable totool_choicerather than to auth or request shape:autonone{"type":"any"}{"type":"tool",name}claude-fable-5-1claude-fable-5So this is model-gated, not an API-wide change —
claude-fable-5accepts all four.Then end-to-end through the AI SDK with opencode's exact shape (a
StructuredOutputtool plustoolChoice: "required"), capturing the outgoing body: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:
tool_choice 'required' is incompatible with thinking enabledThinking mode does not support this tool_choicethinking: true— reported in that thread and now hard-failing on 5-1The 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.
generateObjectagainst both models emits no tool and no tool choice at all:So the model-agnostic fix is for the
json_schemapath to use the provider's native structured output where available, instead of hand-rolling aStructuredOutputtool 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
requiredtoautowhen the model reasons — is what #29565 implemented (compatibleToolChoiceinsession/llm.ts, credit @serejaris). It is a smaller diff and would fix the 400. Worth noting it does weaken the guarantee: withautothe model may not call theStructuredOutputtool at all, so the request can come back without structured output and lean onformat.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
format: { type: "json_schema", schema: {...} }anthropic/claude-fable-5-1tool_choice: type "tool" and "any" are not supported for this model.Substituting
claude-fable-5succeeds, 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), andclaude-fable-5-1only qualifies because it matches theclaude-fable-5entry as a substring. A future model id that matches no known prefix would fall through to the forced-tool path, which would turnagent.ts'sgenerateObject/streamObjectcalls 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 againstclaude-fable-5-1andclaude-fable-5on 2026-09-02.