fix(anthropic): per-model thinking shape restores Claude reasoning on Copilot - #8
Conversation
… Copilot Copilot's /v1/messages shim serves two Claude generations with mutually incompatible extended-thinking request shapes: the 4.5-and-older family only implements thinking.type=enabled + budget_tokens (adaptive requests return no thinking blocks at all), while 4.6+/fable/5.x reject enabled with a 400 and require thinking.type=adaptive + output_config.effort. The provider-wide AdaptiveThinking switch sent adaptive to everything, which silently disabled visible reasoning for claude-haiku-4.5, claude-sonnet-4.5, and claude-opus-4.5 on Copilot. Upstream verification of the enabled shape on Copilot /v1/messages: BerriAI/litellm#28053 / litellm#31802. The first-party API had the mirror bug: effort-only models (claude-fable-5, claude-opus-4.7+, generation 5) got enabled+budget, which they reject. - internal/anthropic: ModelSupportsAdaptiveThinking / ModelRequiresAdaptiveThinking classify Claude generations (dotted, dashed, dated, and legacy version-first names), mirroring the models.dev reasoning_options catalogs for anthropic/github-copilot. - providers/anthropic: buildRequest resolves the thinking shape per model. AdaptiveThinking now means "effort-first gateway": adaptive wherever supported, enabled+budget for the 4.5 family; the first-party path keeps enabled except on effort-only models. Effort-only settings derive a budget on the enabled path (same ladder as ModeReasoningSettings). - Self-healing: a 400 mentioning thinking.type flips the shape once, retries, and pins the working shape on the model handle, so catalog drift degrades to one retry instead of a failed run. Co-authored-by: captaintrips-gratefulagents <captaintrips@gratefulagents.dev>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6c232762a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if sent.Thinking.Type == "adaptive" { | ||
| m.thinkingShape.Store(thinkingShapeEnabled) | ||
| } else { | ||
| m.thinkingShape.Store(thinkingShapeAdaptive) | ||
| } |
There was a problem hiding this comment.
Pin the shape only after the retry succeeds
When a shape-related 400 is followed by a failed retry (for example a transient 429/5xx/network error, or the opposite shape is also invalid), this stores the flipped shape before any successful response proves it works. Because the same AnthropicModel can be reused for later attempts or turns, subsequent calls bypass the per-model heuristic and are forced to the unverified shape, causing avoidable repeated failures or silently disabling thinking for models that needed the original shape.
Useful? React with 👍 / 👎.
Problem
Claude models on the Copilot provider never show thinking, even though we already route them to Copilot's native Anthropic
/v1/messagesendpoint (the approach of BerriAI/litellm#28053 / litellm#31802).The remaining gap was the thinking request shape, applied provider-wide instead of per-model. Copilot serves two Claude generations with mutually incompatible shapes:
/v1/messagesclaude-haiku-4.5,claude-sonnet-4.5,claude-opus-4.5thinking.type=enabled+budget_tokensonly — adaptive returns no thinking blocksadaptive→ reasoning silently disabledclaude-sonnet-4.6,claude-opus-4.6/4.7/4.8,claude-sonnet-5,claude-fable-5adaptive+output_config.effort(400 onenabled)adaptive✓Commit
bdd304aswitched everything toadaptiveafter the newer models' live 400; that fixed them but silently killed thinking for the 4.5 family. The LiteLLM issue independently verifiedenabled+budget_tokensemits thinking blocks end-to-end (incl. streamingcontent_block_*) on Copilot/v1/messagesforclaude-haiku-4.5.The first-party API had the mirror bug: effort-only models (
claude-fable-5,claude-opus-4-7+, generation 5) were sentenabled+budget, which they reject with a 400.The shape split mirrors the models.dev
reasoning_optionscatalogs foranthropicandgithub-copilot(and opencode's per-model adaptive gating).Changes
internal/anthropic/thinking.go:ModelSupportsAdaptiveThinking/ModelRequiresAdaptiveThinkingclassify Claude generations; tolerant of dotted (claude-sonnet-4.6), dashed (claude-sonnet-4-6), dated (...-20250929), and legacy version-first (claude-3-7-sonnet) names.providers/anthropic:buildRequestresolves the shape per model.AdaptiveThinkingnow means "effort-first gateway": adaptive wherever the generation supports it,enabled+budget for the 4.5-and-older family. The first-party path keepsenabledexcept on effort-only models. Effort-only settings derive a budget on the enabled path (same ladder asModeReasoningSettings).thinking.typeflips the shape once, retries, and pins the working shape on the model handle — catalog drift degrades to one retry instead of a failed run (both directions).Verification
TestCopilotClaude45UsesEnabledThinking(Copilotclaude-sonnet-4.5→enabled+budget_tokens=8192, emptyoutput_config); 400-flip retry + stickiness; unrelated-400 passthrough.TestCopilotClaudeUsesAdaptiveThinking(opus-4.8 → adaptive+max) still passes unchanged.go build ./...,go vet, fullgo test ./...— only failure is the known pre-existingpkg/agentsdk/sandboxcontainer-environment failure (os.Executable), unrelated.Residual risk
The generation split is a name heuristic; a Copilot-side capability change would be absorbed by the one-shot 400 flip rather than a hard failure.
claude-sonnet-4(emptyreasoning_optionson Copilot) now getsenabled+budget — its native first-party shape — instead ofadaptive; if the shim rejects thinking there entirely, the flip retries with adaptive and then surfaces the error.