Summary
When routing claude-fable-5 through the Anthropic adapter with reasoning effort max (output_config.effort: "max"), the model consistently exhausts the entire max_tokens budget on adaptive thinking and returns zero visible text, while the proxy reports the turn as a normal completed response (no truncation/incomplete signal). Downstream clients (e.g. Claude Code / Claude Desktop) see this as "completed" with an empty message rather than a truncated one.
Reproduced 6/6 times on claude-fable-5 with requestedEffort: "max"; 0/3 failures on the same model without an explicit max effort override (default/low effort completed normally in ~1-1.5s).
Environment
- opencodex:
2.7.31 (npm, @bitkyc08/opencodex)
- OS: Windows 11 Pro (10.0.26200)
- Node: v22.17.0
- Bun (bundled runtime): 1.3.14
- Claude Code: 2.1.217 (Claude Desktop bundled CLI)
- Provider/model:
anthropic / claude-fable-5
- Adapter:
src/adapters/anthropic.ts
Evidence (from local ~/.opencodex/usage.jsonl)
{"requestId":"ocx-mrvgow0n-64","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":128798,"usage":{"inputTokens":101654,"outputTokens":8192,"totalTokens":109846}}
{"requestId":"ocx-mrvgsf9c-65","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":128240,"usage":{"inputTokens":101798,"outputTokens":8192,"totalTokens":109990}}
{"requestId":"ocx-mrvgvc5f-6c","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":126761,"usage":{"inputTokens":101907,"outputTokens":8192,"totalTokens":110099}}
{"requestId":"ocx-mrvh0wjg-6g","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":127187,"usage":{"inputTokens":51417,"outputTokens":8192,"totalTokens":59609}}
{"requestId":"ocx-mrvh5u8v-6h","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":123028,"usage":{"inputTokens":51435,"outputTokens":8192,"totalTokens":59627}}
{"requestId":"ocx-mrvhy0md-2","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":125836,"usage":{"inputTokens":54833,"outputTokens":8192,"totalTokens":63025}}
Every single requestedEffort: "max" call for claude-fable-5 hit exactly 8192 output tokens and took ~2 minutes. Non-max calls on the same model in the same log completed in ~1-1.5s with a handful of output tokens (e.g. 9, 14).
For comparison, a same-conditions Sonnet call completes in ~2.1s with 9 output tokens — so this is not an inherent slowness of the account/network, it's specific to Fable + max effort.
Root cause (traced in src/adapters/anthropic.ts, v2.7.31)
Two compounding bugs:
1. stop_reason is never read from the upstream Anthropic response.
Neither the streaming SSE parser nor the non-streaming path extracts it:
// streaming path, ~line 796-804
case "message_delta": {
const usage = data.usage as Record<string, number> | undefined;
pendingUsage = mergeAnthropicUsage(pendingUsage, usage);
break; // <- data.delta.stop_reason is discarded
}
case "message_stop": {
yield* emitDone(); // <- no stop_reason passed through
break;
}
// non-streaming path, ~line 820-848 (parseResponse)
const json = await response.json() as Record<string, unknown>;
// ...reads json.content and json.usage, never json.stop_reason
events.push({ type: "done", usage: usageFromAnthropic(usage) });
A repo-wide search for stop_reason inside src/adapters/anthropic.ts returns zero matches. Downstream, src/server/claude-messages.ts:745 synthesizes the outgoing message_delta:
emit("message_delta", { type: "message_delta", delta: { stop_reason: (message as Rec).stop_reason ?? "end_turn", stop_sequence: null }, usage: ... });
Since message.stop_reason is never populated by the adapter, this always falls back to "end_turn" — even when Anthropic actually returned "max_tokens". The client has no way to distinguish a genuinely finished turn from a token-capped one.
2. Adaptive-thinking models don't get max_tokens resized for effort, and default to 8192.
// ~line 36-37
const DEFAULT_MAX_TOKENS = 8192;
...
// ~line 610
max_tokens: parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS,
...
// ~line 626-646
if (usesAdaptiveThinking(modelId)) {
// Adaptive-thinking models replace the token budget with an effort knob and reject
// `thinking.type: "enabled"` outright — no budget/max_tokens re-sizing needed.
body.thinking = { type: "adaptive" };
body.output_config = { effort: adaptiveEffort(parsed.options.reasoning) };
} else {
// ...budget_tokens / max_tokens resizing only happens here
}
claude-fable-5 matches ADAPTIVE_THINKING_FAMILY_MINIMUMS.fable = [0, 0] (all Fable versions), so it always takes the adaptive branch, where max_tokens is left at whatever was set earlier (defaulting to 8192 when the caller doesn't pass max_output_tokens). At effort: "max", adaptive thinking can consume the entire 8192-token budget internally, leaving 0 tokens for visible output — Anthropic correctly returns stop_reason: "max_tokens" with empty/near-empty content, but bug #1 means this is never surfaced.
Expected behavior
Either:
- The adapter should thread
stop_reason (streaming: message_delta.delta.stop_reason; non-streaming: json.stop_reason) through to the emitted done/message event so "max_tokens" reaches the client as an incomplete/truncated response instead of "end_turn", and/or
max_tokens should scale with effort for adaptive-thinking models too (similar in spirit to reasoningBudget(), which already exists but is currently only used on the non-adaptive branch), so max/xhigh effort doesn't get boxed into the same 8192 ceiling as default effort.
Actual behavior
Client receives an HTTP 200, status: "completed" response with no visible text after ~2 minutes and 8192 output tokens fully consumed by internal thinking. Looks identical to a normal, successful, empty-output turn — no error, no truncation indicator.
Workaround
Avoid effort: "max" for claude-fable-5 (and likely high/xhigh, structurally same risk though not locally confirmed) until fixed. Default/low effort was reproduced as working correctly in the same logs.
Summary
When routing
claude-fable-5through the Anthropic adapter with reasoning effortmax(output_config.effort: "max"), the model consistently exhausts the entiremax_tokensbudget on adaptive thinking and returns zero visible text, while the proxy reports the turn as a normal completed response (no truncation/incomplete signal). Downstream clients (e.g. Claude Code / Claude Desktop) see this as "completed" with an empty message rather than a truncated one.Reproduced 6/6 times on
claude-fable-5withrequestedEffort: "max"; 0/3 failures on the same model without an explicitmaxeffort override (default/low effort completed normally in ~1-1.5s).Environment
2.7.31(npm,@bitkyc08/opencodex)anthropic/claude-fable-5src/adapters/anthropic.tsEvidence (from local
~/.opencodex/usage.jsonl){"requestId":"ocx-mrvgow0n-64","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":128798,"usage":{"inputTokens":101654,"outputTokens":8192,"totalTokens":109846}} {"requestId":"ocx-mrvgsf9c-65","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":128240,"usage":{"inputTokens":101798,"outputTokens":8192,"totalTokens":109990}} {"requestId":"ocx-mrvgvc5f-6c","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":126761,"usage":{"inputTokens":101907,"outputTokens":8192,"totalTokens":110099}} {"requestId":"ocx-mrvh0wjg-6g","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":127187,"usage":{"inputTokens":51417,"outputTokens":8192,"totalTokens":59609}} {"requestId":"ocx-mrvh5u8v-6h","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":123028,"usage":{"inputTokens":51435,"outputTokens":8192,"totalTokens":59627}} {"requestId":"ocx-mrvhy0md-2","model":"claude-fable-5","requestedEffort":"max","status":200,"durationMs":125836,"usage":{"inputTokens":54833,"outputTokens":8192,"totalTokens":63025}}Every single
requestedEffort: "max"call forclaude-fable-5hit exactly 8192 output tokens and took ~2 minutes. Non-maxcalls on the same model in the same log completed in ~1-1.5s with a handful of output tokens (e.g. 9, 14).For comparison, a same-conditions Sonnet call completes in ~2.1s with 9 output tokens — so this is not an inherent slowness of the account/network, it's specific to Fable +
maxeffort.Root cause (traced in
src/adapters/anthropic.ts, v2.7.31)Two compounding bugs:
1.
stop_reasonis never read from the upstream Anthropic response.Neither the streaming SSE parser nor the non-streaming path extracts it:
A repo-wide search for
stop_reasoninsidesrc/adapters/anthropic.tsreturns zero matches. Downstream,src/server/claude-messages.ts:745synthesizes the outgoingmessage_delta:Since
message.stop_reasonis never populated by the adapter, this always falls back to"end_turn"— even when Anthropic actually returned"max_tokens". The client has no way to distinguish a genuinely finished turn from a token-capped one.2. Adaptive-thinking models don't get
max_tokensresized for effort, and default to 8192.claude-fable-5matchesADAPTIVE_THINKING_FAMILY_MINIMUMS.fable = [0, 0](all Fable versions), so it always takes the adaptive branch, wheremax_tokensis left at whatever was set earlier (defaulting to 8192 when the caller doesn't passmax_output_tokens). Ateffort: "max", adaptive thinking can consume the entire 8192-token budget internally, leaving 0 tokens for visible output — Anthropic correctly returnsstop_reason: "max_tokens"with empty/near-emptycontent, but bug #1 means this is never surfaced.Expected behavior
Either:
stop_reason(streaming:message_delta.delta.stop_reason; non-streaming:json.stop_reason) through to the emitteddone/messageevent so"max_tokens"reaches the client as an incomplete/truncated response instead of"end_turn", and/ormax_tokensshould scale witheffortfor adaptive-thinking models too (similar in spirit toreasoningBudget(), which already exists but is currently only used on the non-adaptive branch), somax/xhigheffort doesn't get boxed into the same 8192 ceiling as default effort.Actual behavior
Client receives an HTTP 200,
status: "completed"response with no visible text after ~2 minutes and 8192 output tokens fully consumed by internal thinking. Looks identical to a normal, successful, empty-output turn — no error, no truncation indicator.Workaround
Avoid
effort: "max"forclaude-fable-5(and likelyhigh/xhigh, structurally same risk though not locally confirmed) until fixed. Default/low effort was reproduced as working correctly in the same logs.