This issue was posted by Claude Code using claude-opus-4-8 on behalf of David.
Reported from an adversarially-verified review; David directed the review and the finding was confirmed against the current main. Issue text is AI-drafted.
Where: code_puppy/model_utils.py → resolve_anthropic_thinking_payload
What. if extended_thinking not in ("enabled", "adaptive"): return None, and make_model_settings only sets anthropic_thinking when the payload is non-None. pydantic-ai's AnthropicModel._translate_thinking then returns OMIT (verified: if thinking is None or thinking is False: return OMIT), so the thinking parameter is absent from the wire request. The function never emits {"type": "disabled"} for any model.
Impact. A user on claude-opus-5 or claude-sonnet-5 who selects Extended Thinking = "off" in /model_settings (choices are ["enabled", "adaptive", "off"]) still gets adaptive thinking on every request and pays for the thinking tokens, with no error and no UI signal. It also interacts with the max_tokens cap: thinking and response text share that budget, so a turn sized for a thinking-free answer can truncate mid-response.
Suggested fix. Return {"type": "disabled"} when the target model's profile reports anthropic_supports_adaptive_thinking, and keep returning None only for classic models where omission genuinely means off. Clamp effort to high when thinking is disabled on Opus 5 — pydantic-ai encodes this as anthropic_disallows_top_effort_when_thinking_disabled and raises a UserError for xhigh/max. Fable 5 rejects {"type":"disabled"} at any effort, so that family must keep the omit behaviour.
Reported from an adversarially-verified review; David directed the review and the finding was confirmed against the current
main. Issue text is AI-drafted.Where:
code_puppy/model_utils.py→resolve_anthropic_thinking_payloadWhat.
if extended_thinking not in ("enabled", "adaptive"): return None, andmake_model_settingsonly setsanthropic_thinkingwhen the payload is non-None. pydantic-ai'sAnthropicModel._translate_thinkingthen returnsOMIT(verified:if thinking is None or thinking is False: return OMIT), so thethinkingparameter is absent from the wire request. The function never emits{"type": "disabled"}for any model.Impact. A user on claude-opus-5 or claude-sonnet-5 who selects Extended Thinking = "off" in
/model_settings(choices are["enabled", "adaptive", "off"]) still gets adaptive thinking on every request and pays for the thinking tokens, with no error and no UI signal. It also interacts with themax_tokenscap: thinking and response text share that budget, so a turn sized for a thinking-free answer can truncate mid-response.Suggested fix. Return
{"type": "disabled"}when the target model's profile reportsanthropic_supports_adaptive_thinking, and keep returningNoneonly for classic models where omission genuinely means off. Clampefforttohighwhen thinking is disabled on Opus 5 — pydantic-ai encodes this asanthropic_disallows_top_effort_when_thinking_disabledand raises aUserErrorfor xhigh/max. Fable 5 rejects{"type":"disabled"}at any effort, so that family must keep the omit behaviour.