Skip to content

fix(llm): reach reasoning parity on the Anthropic provider #922

Description

@devlikebear

Part of #919. Phase 0 of the provider modernization roadmap. LP-003.

Blocks LP-004 — a thinking budget must fit under a configurable max_tokens.

Summary

Make the Anthropic client honor reasoning_effort, and preserve thinking blocks with their signatures across tool-calling iterations.

Problem

Two distinct gaps in the same conversion layer.

1. reasoning_effort is silently ignored. anthropic.go:106 consults only effectiveThinkingBudget; ReasoningEffort is never read on the Anthropic path. The shipped default config already depends on it:

# config/default.yaml:43-46
light:
  provider: minimax      # kind: anthropic
  model: MiniMax-M2.7
  reasoning_effort: minimal   # no effect today

A setting that is accepted, validated, displayed in the console, and does nothing is worse than one that is rejected.

2. Thinking blocks are dropped on the return trip. toAnthropicAssistantMessage (anthropic.go:461) reconstructs assistant turns as text + tool_use only. ContentBlock (internal/llm/provider.go:17) has no thinking or signature field, so the parsed reasoning is flattened into ReasoningContent (a string) and lost as a structured block.

Anthropic requires signed thinking blocks to be echoed back on assistant turns that contain tool_use when extended thinking is enabled. So enabling thinking_budget on an Anthropic tier alongside tools is expected to fail from the second loop iteration onward. This is latent today only because no shipped tier sets thinking_budget.

gemini-native already solves the equivalent problem correctly — gemini_native_convert.go:161-170 encodes thoughtSignature on the way out and :286 restores it on the way back in. Mirror that design rather than inventing a new one.

Scope

  • Map ReasoningEffort to the Anthropic request. Where the model exposes a native effort control, use it; otherwise derive a thinking.budget_tokens from the effort level and document the mapping table in code.
  • Extend ContentBlock (or add an assistant-side equivalent) with a thinking type carrying text plus signature.
  • Capture thinking blocks with signatures in both the streaming and non-streaming response parsers.
  • Re-emit them in toAnthropicAssistantMessage, in original order, ahead of tool_use blocks.
  • Preserve redacted_thinking blocks opaquely — they must round-trip without inspection.
  • Confirm the existing ReasoningContent consumers (console reasoning stream, usage) still work.
  • Verify the round trip survives session persistence — blocks must be reconstructible from a stored transcript, not just from in-memory state.

Acceptance Criteria

  • reasoning_effort on an Anthropic tier changes the request payload; the value is visible in debug logs.
  • A tier with thinking_budget plus tools completes a multi-iteration tool loop without an API error.
  • Thinking blocks survive a server restart mid-conversation.
  • redacted_thinking blocks round-trip byte-identically.
  • Reasoning still streams to the console via OnReasoningDelta.

Validation

  • Unit tests for effort → request mapping at each effort level
  • Unit tests for thinking block capture and re-emission, streaming and non-streaming
  • Unit test for a tool loop replay from a persisted transcript
  • //go:build integration live test against the real API with thinking + tools enabled, following the TestAntigravityCLILive precedent
  • make test
  • make lint-diff

Out of Scope

  • Interleaved thinking, which requires a beta header — see LP-004.
  • Reasoning support on kind: anthropic third-party gateways that do not implement thinking; detect and degrade, do not emulate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestmlp-domain-platformMLP: Domain-agnostic agent platform

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions