Add adaptive thinking and effort options for Anthropic - #211
Conversation
|
Hi @ActuallyTaylor. Thanks for splitting this out so quickly. A few things before it can go in:
Decoding |
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
|
@mattt Sorry about those! They have all been fixed. |
There was a problem hiding this comment.
🟡 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_useblocks. - 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 itsoutput_configplacement or its merge with structured-outputformat. 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.
| private struct AnthropicThinking: Codable, Sendable { | ||
| let type: String | ||
| let thinking: String | ||
| let signature: String |
| case thinkingDelta(ThinkingDelta) | ||
| case signatureDelta(SignatureDelta) |
mattt
left a comment
There was a problem hiding this comment.
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:
Thinking's public memberwise init can build states the API rejects (.enabledwith no budget,.adaptivewith one). Could you keep.adaptive(display:)and.enabled(budgetTokens:display:)as the public constructors (withdisplaydefaulting tonil) and make the memberwise init non-public?- 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>
|
@mattt I fixed the Thinking initializers and relaxed the |
Signed-off-by: Taylor Lineman <git@actuallytaylor.com>
There was a problem hiding this comment.
🟡 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:
streamResponsehandles only.textDeltaat lines 517–533 and silently ignores every otherDelta. Consequently, requestingdisplay: .summarizedstill 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:
respondextracts only tool-use and text blocks, andtoAnthropicMessages()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
There was a problem hiding this comment.
🟡 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
mattt
left a comment
There was a problem hiding this comment.
This is looking great. Thanks for all of your work on this, @ActuallyTaylor!
Adds adaptive thinking and thinking display options to the Anthropic provider, plus all five effort levels. Effort is sent as
output_config.effortwhile preserving structured output schemas and the existingextraBodyoverride behavior.Thinking can be configured through
.adaptive(display:)or.enabled(budgetTokens:display:). The existinginit(budgetTokens:)remains available;budgetTokensis 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: