Skip to content

Fix: anthropic format#122

Merged
SantiagoDePolonia merged 4 commits intomainfrom
fix/anthropic-format
Mar 8, 2026
Merged

Fix: anthropic format#122
SantiagoDePolonia merged 4 commits intomainfrom
fix/anthropic-format

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Mar 6, 2026

Do these changes help Anthropic support?

  • Yes, model_context_window_exceeded -> length is beneficial. Without it,
    OpenAI-compatible clients would see a raw Anthropic stop reason instead of
    the expected truncation-style finish reason.
  • Yes, but narrowly, the malformed-stream fallback is beneficial. If Anthropic
    says stop_reason: "tool_use" but no tool_calls chunk was ever emitted,
    returning raw tool_use is more honest than fabricating OpenAI-style
    tool_calls.

Are those arguments/fields used?

  • emittedToolCalls: yes, fully used in code. It exists only to distinguish
    “Anthropic said tool_use” from “the client actually received tool call
    chunks.”
  • model_context_window_exceeded: yes, it is a real Anthropic stop-reason
    value, documented for Claude 4.5+ and older models behind a beta header.
  • No new request-side Anthropic arguments or payload fields are introduced by
    this branch. This branch only changes response/stream interpretation plus
    tests.

Net assessment:

  • This PR still makes sense, but only as a narrow compatibility/defensive fix.
  • The model_context_window_exceeded mapping is clearly useful.
  • The tool_use fallback is defensive rather than mainstream; it protects
    partial or malformed streams, not the normal happy path.

Residual risk:

  • The fallback case is unit-tested in anthropic_test.go:504, but it is not
    backed by a real contract/integration capture, so its value is mainly
    robustness rather than coverage of a documented normal stream shape.

Sources:

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced streaming response handling to properly manage tool use scenarios when no tool calls are emitted, ensuring correct behavior in edge cases.
    • Improved error handling for context window exceeded scenarios to maintain consistency.
  • Tests

    • Added test coverage for tool use streaming scenarios and context window handling.

@SantiagoDePolonia SantiagoDePolonia self-assigned this Mar 6, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fe4f652f-d8b7-4955-aca1-5ea3786e8c2f

📥 Commits

Reviewing files that changed from the base of the PR and between 0da42c1 and f43a75d.

📒 Files selected for processing (2)
  • internal/providers/anthropic/anthropic.go
  • internal/providers/anthropic/anthropic_test.go

📝 Walkthrough

Walkthrough

The Anthropic provider's streaming implementation now tracks whether tool call deltas were emitted during the stream and conditionally maps stop reasons accordingly. Stop reason normalization was extended to handle "model_context_window_exceeded" as a "length" stop reason.

Changes

Cohort / File(s) Summary
Streaming Tool Call Tracking
internal/providers/anthropic/anthropic.go
Added emittedToolCalls flag to track tool call emission; implemented conditional stop reason mapping that preserves raw "tool_use" when no tool calls were emitted; extended normalizeAnthropicStopReason to map "model_context_window_exceeded" to "length"; updated content_block and message_delta handlers to manage the flag and apply conditional normalization.
Tool Call Streaming Tests
internal/providers/anthropic/anthropic_test.go
Added test case for streaming with tool_use stop reason but no tool chunks to verify no tool_calls in delta and clean completion; extended stop reason normalization tests to cover "model_context_window_exceeded" mapping to "length".

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A streaming rabbit hops with glee,
Tool calls tracked for all to see,
Stop reasons mapped with utmost care,
Context windows handled fair—
The provider dances through the air! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix: anthropic format' is vague and does not clearly convey the specific nature of the changes made. Provide a more descriptive title that clearly indicates the main change, such as 'Fix: Anthropic tool_use stop_reason handling in streaming' or 'Fix: Anthropic stream converter tool call tracking and stop reason mapping'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/anthropic-format

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the Anthropic provider’s OpenAI-compatibility by normalizing finish_reason values (and extending streaming support for tool calls), then aligns contract golden fixtures and tests accordingly.

Changes:

  • Normalize Anthropic stop_reason → OpenAI-style finish_reason (end_turn/stop_sequencestop, tool_usetool_calls, etc.).
  • Extend SSE stream conversion to emit tool_calls deltas (including incremental arguments via partial_json).
  • Update contract golden JSON outputs and add/adjust tests to assert the new finish reasons.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/contract/testdata/golden/anthropic/messages.golden.json Golden updated: finish_reason normalized to stop.
tests/contract/testdata/golden/anthropic/messages_extended_thinking.golden.json Golden updated: finish_reason normalized to stop.
tests/contract/testdata/golden/anthropic/messages_multi_turn.golden.json Golden updated: finish_reason normalized to stop.
tests/contract/testdata/golden/anthropic/messages_multimodal.golden.json Golden updated: finish_reason normalized to stop.
tests/contract/testdata/golden/anthropic/messages_stream.golden.json Golden updated: streamed final finish_reason normalized to stop.
tests/contract/testdata/golden/anthropic/messages_with_params.golden.json Golden updated: stop_sequence normalized to stop.
tests/contract/testdata/golden/anthropic/messages_with_tools.golden.json Golden updated: tool_use normalized to tool_calls.
tests/contract/anthropic_test.go Contract test updated to expect tool_calls finish reason for tools fixture.
internal/providers/anthropic/anthropic_test.go Adds coverage for normalized finish reasons and streaming tool call chunk emission.
internal/providers/anthropic/anthropic.go Implements stop-reason mapping + streaming tool-call delta handling (partial_json).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/providers/anthropic/anthropic.go (2)

609-650: ⚠️ Potential issue | 🟠 Major

Skip streamed tool_use blocks that never provide a function name.

Line 636 emits a tool_calls delta before validating event.ContentBlock.Name. That diverges from extractToolCalls(), which drops nameless tool blocks, and it means the stream path can produce function.name:"" while still setting emittedToolCalls=true and later upgrading the final finish_reason to tool_calls.

Proposed fix
-func (sc *streamConverter) getToolCallState(blockIndex int, block *anthropicContent) streamToolCallState {
+func (sc *streamConverter) getToolCallState(blockIndex int, block *anthropicContent) (streamToolCallState, bool) {
 	if state, ok := sc.toolCalls[blockIndex]; ok {
-		return state
+		return state, true
+	}
+	if block == nil || strings.TrimSpace(block.Name) == "" {
+		return streamToolCallState{}, false
 	}

 	state := streamToolCallState{
 		Ordinal: sc.nextToolCallIdx,
 	}
-	if block != nil {
-		state.ID = block.ID
-		state.Name = block.Name
-	}
+	state.ID = block.ID
+	state.Name = block.Name
 	sc.toolCalls[blockIndex] = state
 	sc.nextToolCallIdx++
-	return state
+	return state, true
 }
@@
 	case "content_block_start":
 		if event.ContentBlock != nil && event.ContentBlock.Type == "tool_use" {
-			state := sc.getToolCallState(event.Index, event.ContentBlock)
+			state, ok := sc.getToolCallState(event.Index, event.ContentBlock)
+			if !ok {
+				return ""
+			}
 			sc.emittedToolCalls = true
 			return sc.emitChatChunk(map[string]interface{}{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/providers/anthropic/anthropic.go` around lines 609 - 650, The
convertEvent path is emitting a tool_calls delta for content_block_start
tool_use even when event.ContentBlock.Name is empty, which diverges from
extractToolCalls(); modify convertEvent so that when handling
"content_block_start" for a "tool_use" it first verifies event.ContentBlock.Name
(or state.Name) is non-empty and if empty simply return "" (do not call
getToolCallState, do not set sc.emittedToolCalls, and do not emit any delta);
otherwise proceed as now (call getToolCallState, mark emittedToolCalls and emit
the tool_calls chunk). Ensure the checks reference convertEvent,
getToolCallState, sc.emittedToolCalls and event.ContentBlock.Name/Name to locate
the change.

395-425: ⚠️ Potential issue | 🟠 Major

Mirror the malformed tool_use fallback in the non-streaming path.

Line 399 always maps tool_use to tool_calls, even when extractToolCalls(resp.Content) returned nil. That leaves clients with finish_reason:"tool_calls" and no message.tool_calls, which is the same malformed-payload inconsistency the stream path now avoids via mapStreamStopReason().

Proposed fix
 	content := extractTextContent(resp.Content)
 	toolCalls := extractToolCalls(resp.Content)

-	finishReason := mapAnthropicStopReasonToOpenAI(resp.StopReason)
+	finishReason := resp.StopReason
+	if resp.StopReason != "tool_use" || len(toolCalls) > 0 {
+		finishReason = mapAnthropicStopReasonToOpenAI(resp.StopReason)
+	}

 	usage := core.Usage{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/providers/anthropic/anthropic.go` around lines 395 - 425,
convertFromAnthropicResponse currently sets finishReason via
mapAnthropicStopReasonToOpenAI(resp.StopReason) and always maps tool_use to
tool_calls even when extractToolCalls(resp.Content) returns nil, causing
finish_reason:"tool_calls" with no message.tool_calls; change
convertFromAnthropicResponse to mirror the streaming fallback: after calling
toolCalls := extractToolCalls(resp.Content) and finishReason :=
mapAnthropicStopReasonToOpenAI(resp.StopReason), if finishReason indicates
"tool_calls" but toolCalls is nil/empty, set finishReason to the fallback string
used in streaming (e.g., "tool_use") so clients don’t receive a finish_reason
that promises missing message.tool_calls; use the same symbol names
(convertFromAnthropicResponse, extractToolCalls, mapAnthropicStopReasonToOpenAI)
to locate where to apply this check and mutation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/providers/anthropic/anthropic_test.go`:
- Around line 374-386: The test uses unchecked type assertions on chunks and
nested fields (chunks, firstChoices, firstChoice, firstDelta, firstToolCalls,
firstToolCall, firstFunction) which can panic; update the assertions to perform
safe type checks (using the comma-ok form) at each step and call t.Fatalf with
descriptive messages when a cast fails or an expected length is wrong, e.g.,
check chunks[0]["choices"] is []interface{} and len==1 before indexing, verify
firstChoices[0] is map[string]interface{} before accessing "delta", verify
firstDelta["tool_calls"] is []interface{} and non-empty, and similar for
subsequent nested maps so failures produce clear error messages instead of
panics.

---

Outside diff comments:
In `@internal/providers/anthropic/anthropic.go`:
- Around line 609-650: The convertEvent path is emitting a tool_calls delta for
content_block_start tool_use even when event.ContentBlock.Name is empty, which
diverges from extractToolCalls(); modify convertEvent so that when handling
"content_block_start" for a "tool_use" it first verifies event.ContentBlock.Name
(or state.Name) is non-empty and if empty simply return "" (do not call
getToolCallState, do not set sc.emittedToolCalls, and do not emit any delta);
otherwise proceed as now (call getToolCallState, mark emittedToolCalls and emit
the tool_calls chunk). Ensure the checks reference convertEvent,
getToolCallState, sc.emittedToolCalls and event.ContentBlock.Name/Name to locate
the change.
- Around line 395-425: convertFromAnthropicResponse currently sets finishReason
via mapAnthropicStopReasonToOpenAI(resp.StopReason) and always maps tool_use to
tool_calls even when extractToolCalls(resp.Content) returns nil, causing
finish_reason:"tool_calls" with no message.tool_calls; change
convertFromAnthropicResponse to mirror the streaming fallback: after calling
toolCalls := extractToolCalls(resp.Content) and finishReason :=
mapAnthropicStopReasonToOpenAI(resp.StopReason), if finishReason indicates
"tool_calls" but toolCalls is nil/empty, set finishReason to the fallback string
used in streaming (e.g., "tool_use") so clients don’t receive a finish_reason
that promises missing message.tool_calls; use the same symbol names
(convertFromAnthropicResponse, extractToolCalls, mapAnthropicStopReasonToOpenAI)
to locate where to apply this check and mutation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0f4c852b-55bb-489a-87a5-c28eb5b4e20a

📥 Commits

Reviewing files that changed from the base of the PR and between 8149ab0 and 0da42c1.

📒 Files selected for processing (10)
  • internal/providers/anthropic/anthropic.go
  • internal/providers/anthropic/anthropic_test.go
  • tests/contract/anthropic_test.go
  • tests/contract/testdata/golden/anthropic/messages.golden.json
  • tests/contract/testdata/golden/anthropic/messages_extended_thinking.golden.json
  • tests/contract/testdata/golden/anthropic/messages_multi_turn.golden.json
  • tests/contract/testdata/golden/anthropic/messages_multimodal.golden.json
  • tests/contract/testdata/golden/anthropic/messages_stream.golden.json
  • tests/contract/testdata/golden/anthropic/messages_with_params.golden.json
  • tests/contract/testdata/golden/anthropic/messages_with_tools.golden.json

@SantiagoDePolonia SantiagoDePolonia merged commit 639547a into main Mar 8, 2026
13 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the fix/anthropic-format branch March 22, 2026 14:25
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.

2 participants