Skip to content

Add adaptive thinking and effort options for Anthropic - #211

Merged
mattt merged 13 commits into
huggingface:mainfrom
ActuallyTaylor:anthropic-thinking-effort
Sep 11, 2026
Merged

mattt merged 13 commits into
huggingface:mainfrom
ActuallyTaylor:anthropic-thinking-effort

Conversation

@ActuallyTaylor

@ActuallyTaylor ActuallyTaylor commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Adds adaptive thinking and thinking display options to the Anthropic provider, plus all five effort levels. Effort is sent as output_config.effort while preserving structured output schemas and the existing extraBody override behavior.

Thinking can be configured through .adaptive(display:) or .enabled(budgetTokens:display:). The existing init(budgetTokens:) remains available; budgetTokens is now optional to represent adaptive thinking, so this belongs in the next minor release.

Thinking and signature stream events are decoded, but session responses and snapshots still expose only response content. Exposing thinking output and preserving signed thinking blocks for tool-call follow-ups remain separate work, as agreed in review.

Validation:

  • Strict Swift formatting and whitespace checks pass.
  • Full default suite: 351 tests passed.
  • All optional backends with AsyncHTTPClient: 394 tests passed (SystemLanguageModel tests already covered by the default run).
  • Clean all-backend URLSession run: 388 tests passed (SystemLanguageModel and Ollama integration tests already covered by the default run).
  • New offline request tests cover effort levels, schema preservation, thinking modes, service tier, overrides, and summarized/omitted thinking streams. Codable tests cover adaptive thinking and the legacy initializer.

@mattt

mattt commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Hi @ActuallyTaylor. Thanks for splitting this out so quickly. A few things before it can go in:

  • service_tier stopped being sent — the if let serviceTier block got dropped when the thinking block was rewritten. Could you restore it?
  • The wire value for extra-high effort is xhigh (all lowercase), so case extraHigh = "xHigh" will be rejected by the API.
  • swift format lint --strict --recursive . fails on trailing whitespace and a few long lines. swift format --in-place --recursive . should clear it.
  • The stream: parameter on createMessageParams isn't used by either call site (streaming sets params["stream"] itself), so it can go.
  • Tiny: ThinkingDelta should conform to Sendable, not SendableMetatype.

Decoding thinking_delta and signature_delta without yielding them yet is fine for this PR; surfacing them can follow the #103 shape. As discussed on #176, this goes in with the next minor since Thinking.init changes shape.

Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
@ActuallyTaylor

Copy link
Copy Markdown
Contributor Author

@mattt Sorry about those! They have all been fixed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical thinking-block replay and unresolved API and streaming issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds Anthropic adaptive thinking, display controls, effort configuration, and streaming-event decoding.

Changes:

  • Expands thinking and effort options.
  • Decodes thinking and signature stream events.
  • Updates option tests and serializes Anthropic integration tests.

Review findings:

  • Critical (2 votes), line 980: Thinking blocks and signatures are discarded, causing invalid tool-call follow-up requests. Preserve and replay complete blocks before associated tool_use blocks.
  • Moderate (2 votes), line 226: Restore init(budgetTokens:) to maintain source compatibility.
  • Moderate (1 vote), line 223: Prevent or validate invalid thinking mode/budget combinations.
  • Moderate (2 votes), line 1200: Thinking and signature deltas are decoded but not exposed to callers.
  • Nit (1 vote), line 644: Correct the “append insert” grammatical error.
  • Nit (1 vote), line 643: Add request-serialization tests for effort alone and combined with structured output.
File summaries
File Description
Tests/AnyLanguageModelTests/CustomGenerationOptionsTests.swift Tests expanded thinking options.
Tests/AnyLanguageModelTests/AnthropicLanguageModelTests.swift Serializes Anthropic integration tests.
Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift Implements Anthropic thinking, effort, and delta models.
Review details

Suppressed comments (2)

Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift:644

  • “append insert” is a grammatical error; the comment should say that the effort value is inserted into the existing dictionary.
            // If output_config was previously set during the response schema options, we need to append insert into that dictionary instead of replacing it.

Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift:647

  • The new effort request serialization is untested: the Anthropic option tests never set effort, and there is no stubbed request-body test for its output_config placement or its merge with structured-output format. Add coverage for effort alone and effort combined with a response schema so a nesting or overwrite regression is detected.
        if let effort = customOptions.effort {
            // If output_config was previously set during the response schema options, we need to append insert into that dictionary instead of replacing it.
            if let output_config = params["output_config"], var object = output_config.objectValue {
                object["effort"] = .string(effort.rawValue)
                params["output_config"] = .object(object)
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 980 to +983
private struct AnthropicThinking: Codable, Sendable {
let type: String
let thinking: String
let signature: String
Comment thread Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift Outdated
Comment thread Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift Outdated
Comment on lines +1200 to +1201
case thinkingDelta(ThinkingDelta)
case signatureDelta(SignatureDelta)

@mattt mattt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ActuallyTaylor. Thanks for the quick turnaround on the earlier points; lint and tests are clean on my machine.

Two things I'd like in this PR before merging, both API shape:

  1. Thinking's public memberwise init can build states the API rejects (.enabled with no budget, .adaptive with one). Could you keep .adaptive(display:) and .enabled(budgetTokens:display:) as the public constructors (with display defaulting to nil) and make the memberwise init non-public?
  2. Please keep init(budgetTokens:) as a delegating overload so existing callers (and the README example) keep compiling; feel free to deprecate it in favor of .enabled(budgetTokens:).

Separately, the thinking blocks themselves aren't kept anywhere yet, so a tool call made with thinking on gets replayed without its thinking block and Anthropic rejects the follow-up. That was already true on main, so I don't want to hold this PR on it. #219 added Transcript.ToolCall.providerMetadata for exactly this kind of thing (Gemini stashes its thought signatures there); if you'd like to wire the Anthropic signature through the same slot here, great, otherwise I'll open a follow-up issue and we can take it after this lands. Either way, could you soften the SignatureDelta doc comment so it doesn't promise preservation we don't do yet?

Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
@ActuallyTaylor

ActuallyTaylor commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@mattt I fixed the Thinking initializers and relaxed the SignatureDelta comment. My previous comment was completely wrong, Anthropic does not require a thinking block for a tool call so tool calls can just be sent back without thinking blocks and not have any problem.

Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
@mattt
mattt requested review from mattt and a balanced review from Copilot September 10, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A private initializer breaks test compilation, and thinking deltas and signatures are decoded but not exposed or preserved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift:1207

  • These new delta variants are decoded but never returned: streamResponse handles only .textDelta at lines 517–533 and silently ignores every other Delta. Consequently, requesting display: .summarized still exposes no thinking deltas to callers, so the behavior promised by the PR is not implemented. The stream’s public result needs a separate way to carry and yield thinking/signature updates rather than only recognizing their wire format.
            case thinkingDelta(ThinkingDelta)
            case signatureDelta(SignatureDelta)

Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift:989

  • The signature is decoded here but discarded: respond extracts only tool-use and text blocks, and toAnthropicMessages() later reconstructs assistant tool calls without the preceding signed thinking block. Anthropic requires thinking blocks and signatures to be echoed unchanged when continuing a thinking-enabled tool turn, so the next request can be rejected. Preserve the thinking text/signature in provider metadata or another transcript representation and re-emit it with the tool-use block.
    let signature: String
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift
@mattt mattt changed the title Expands support for Anthropic thinking & Adds support for effort. Add adaptive thinking and effort options for Anthropic Sep 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Thinking configurations can represent and serialize API-invalid type and token-budget combinations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread Sources/AnyLanguageModel/Models/AnthropicLanguageModel.swift Outdated

@mattt mattt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great. Thanks for all of your work on this, @ActuallyTaylor!

@mattt
mattt merged commit 0fdfcfb into huggingface:main Sep 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants