Skip to content

fix anthropic tool call - #3767

Merged
akshaydeo merged 1 commit into
maximhq:devfrom
rmarku:rmarku/fix-anthropic-tool-calling
Jun 22, 2026
Merged

akshaydeo merged 1 commit into
maximhq:devfrom
rmarku:rmarku/fix-anthropic-tool-calling

Conversation

@rmarku

@rmarku rmarku commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix Anthropic → OpenAI streaming conversion for tool calls. Two bugs were corrupting tool-call deltas seen by strict OpenAI-compatible clients (e.g. genkit-go): continuation chunks re-declared function.type, and tools with no input fields (struct{} schema → JSON schema {}) ended up with empty accumulated arguments, which fails json.Unmarshal with "unexpected end of JSON input".

Changes

  • core/providers/anthropic/chat.go
    • Removed Type: schemas.Ptr(string(schemas.ChatToolTypeFunction)) from continuation input_json_delta chunks. Only the initial content_block_start setup chunk now declares function.type; strict OpenAI Chat Completions stream parsers treat a repeated type on a continuation as a fresh tool-call declaration.
    • Suppressed the spurious empty partial_json marker Anthropic emits immediately after content_block_start. The setup chunk already carries arguments: "", so re-emitting it as a continuation tripped strict parsers.
    • Added a synthetic arguments: "{}" flush on content_block_stop for tool blocks where no input_json_delta was ever forwarded. Without it, no-arg tools (e.g. response_start with struct{} input) accumulate "", which is not valid JSON.
    • Added sawArgsDelta map[int]bool to AnthropicStreamState to track, per content-block index, whether any non-empty delta was forwarded. Needed because no existing per-block accumulator on the state struct could be repurposed.
    • On content_block_stop, both contentBlockToToolCallIdx and sawArgsDelta entries are deleted to bound state size on long streams with many tool_use blocks. Deletion also acts as the duplicate-stop guard (second stop hits "not a tool block" and returns nil).
  • core/providers/anthropic/chat_test.go
    • TestToBifrostChatCompletionStream_NoArgToolFlushesEmptyObject: start → stop yields a flushed "{}"; accumulated arguments parse as an empty JSON object; duplicate stop does not re-flush.
    • TestToBifrostChatCompletionStream_EmptyPartialJSONSuppressedBeforeArgs: empty partial_json returns nil; subsequent real fragments stream through; stop does NOT add a synthetic "{}" (would yield {"x":1}{}).
    • TestToBifrostChatCompletionStream_ContinuationOmitsTypeField: regression guard — start chunk carries type=function, continuation chunk omits it.
    • TestToBifrostChatCompletionStream_MixedToolBlocks: interleaved no-arg and real-args tool blocks across two content-block indices; flush fires only for the no-arg block.

Design notes / trade-offs

  • The fix is in the converter rather than the upstream Anthropic SDK because the empty-partial_json marker is a documented quirk of Anthropic's SSE stream and the OpenAI-side strict-parser expectation is what we own.
  • sawArgsDelta is a separate map rather than reusing an accumulator because no per-content-block argument accumulator exists on AnthropicStreamState today; adding one purely for this check would be heavier than a map[int]bool.
  • The synthetic flush emits a continuation chunk (no type field), matching the rule above.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

cd core
go version
go test ./providers/anthropic/ -run TestToBifrostChatCompletionStream -v
go test ./...

Expected: all four new TestToBifrostChatCompletionStream_* subtests pass; full core/providers/anthropic package suite passes.

End-to-end validation can be done with this main.go against a strict client (genkit-go) calling a no-arg tool through the Anthropic → OpenAI path: previously failed with json: unexpected end of JSON input when unmarshalling the accumulated arguments; now succeeds with {}.

No new configs or environment variables.

Screenshots/Recordings

N/A — backend-only change.

Breaking changes

  • Yes
  • No

Related issues

Closes #3443

Security considerations

None. No changes to auth, secrets handling, PII, or sandboxing; the converter operates on already-authenticated streamed responses and the new code paths only adjust delta framing.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed (no docs surface for this internal converter behavior)
  • I verified builds succeed (Go and UI) — go build ./core/providers/anthropic/ succeeds; UI unaffected
  • I verified the CI pipeline passes locally if applicable — go test ./providers/anthropic/ passes

Summary by CodeRabbit

  • Bug Fixes

    • Improved tool-function streaming in chat completions to properly handle argument deltas and eliminate spurious empty markers.
    • Fixed edge case where tool calls with no arguments now correctly emit an empty arguments object instead of remaining incomplete.
  • Tests

    • Added comprehensive unit tests validating tool-use streaming behavior across multiple scenarios, including no-argument tools and interleaved cases.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04820d15-65af-412d-bd44-e272c705f1c8

📥 Commits

Reviewing files that changed from the base of the PR and between 2995197 and d075f48.

📒 Files selected for processing (2)
  • core/providers/anthropic/chat.go
  • core/providers/anthropic/chat_test.go

📝 Walkthrough

Walkthrough

Adds per-tool-block tracking and fixes streaming conversion: suppresses empty tool input fragments, stops repeating function.type on continuation input deltas, and emits a synthetic arguments: "{}" when a tool-use block ends without any non-empty input JSON.

Changes

Tool-call streaming spec compliance

Layer / File(s) Summary
State tracking initialization
core/providers/anthropic/chat.go
Extends AnthropicStreamState with sawArgsDelta map to record receipt of non-empty input_json_delta per content-block index; initializes it in NewAnthropicStreamState and defensively in ToBifrostChatCompletionStream.
Input delta event streaming
core/providers/anthropic/chat.go
Suppresses spurious empty partial_json for tool_use, records non-empty deltas per index, and omits function.type on continuation input_json_delta chunks while streaming arguments fragments.
Content block completion and synthetic flush
core/providers/anthropic/chat.go
On content_block_stop for tool_use, if no non-empty input_json_delta was forwarded, emits a synthetic tool-call delta with arguments: "{}" and clears per-block tracking state.
Test validation of streaming behavior
core/providers/anthropic/chat_test.go
Adds four tests validating: {} flush for no-arg tool, suppression of initial empty partial JSON, omission of type on continuation chunks, and correct per-block tracking for interleaved tool blocks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • maximhq/bifrost#3685: Modifies the same Anthropic chat streaming conversion logic and is code-level related.

Suggested reviewers

  • akshaydeo
  • TejasGhatte
  • danpiths

Poem

🐰 I nibble bytes and stitch the streams,
Empty markers hushed in gentle seams,
Type said once, then quiet flows,
When args are none, a tiny {} shows,
Now parsers sleep and dream in beams.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix anthropic tool call' is vague and generic—it lacks specificity about what bug was fixed. Revise to be more specific: e.g., 'Fix Anthropic→OpenAI tool-call streaming spec compliance' to clarify the core issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive, well-structured, and follows the template with all required sections properly completed.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #3443: removes type from continuation chunks, suppresses empty partial_json, adds synthetic '{}' flush for no-arg tools, and achieves OpenAI spec compliance.
Out of Scope Changes check ✅ Passed All changes are scoped to the Anthropic→OpenAI tool-call streaming converter; no unrelated modifications or refactoring outside the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@greptile-apps

greptile-apps Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — changes are confined to a pure streaming converter with no HTTP calls or side effects, all three bugs have direct test coverage, and the state cleanup on content_block_stop correctly bounds map growth.

The converter logic is straightforward: suppress empty deltas, omit the type field on continuations, and flush {} for no-arg tool blocks. Each code path is exercised by the new tests, no existing behaviour is altered for tools that already had arguments, and the delete-on-stop pattern avoids unbounded state growth on long streams.

No files require special attention.

Important Files Changed

Filename Overview
core/providers/anthropic/chat.go Adds sawArgsDelta tracking to AnthropicStreamState, suppresses empty partial_json markers, removes Type field from continuation tool-call chunks, and flushes synthetic {} on content_block_stop for no-arg tools. Logic is a pure converter change with no side effects; the delete-on-stop cleanup correctly bounds map size and doubles as a duplicate-stop guard.
core/providers/anthropic/chat_test.go Four new subtests cover: no-arg tool flush, empty partial_json suppression with real-args no-flush guard, continuation type-field omission, and mixed interleaved tool blocks. Coverage matches each stated behaviour change.

Reviews (3): Last reviewed commit: "fix anthropic tool call" | Re-trigger Greptile

Comment thread core/providers/anthropic/chat.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
core/providers/anthropic/chat.go (1)

1150-1152: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard input_json_delta when the tool block was never registered.

Lines 1150-1152 intentionally skip content_block_start for the structured-output tool, but Line 1229 still reads contentBlockToToolCallIdx without checking whether that block was registered. When Anthropic then streams input_json_delta for that skipped block, the zero value sends a continuation chunk to tool-call index 0, which reintroduces a corrupted OpenAI stream.

💡 Minimal fix
-					toolCallIdx := state.contentBlockToToolCallIdx[*chunk.Index]
-					state.sawArgsDelta[*chunk.Index] = true
+					toolCallIdx, ok := state.contentBlockToToolCallIdx[*chunk.Index]
+					if !ok {
+						return nil, nil, false
+					}
+					state.sawArgsDelta[*chunk.Index] = true

Also applies to: 1228-1230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/providers/anthropic/chat.go` around lines 1150 - 1152, The code skips
emitting a tool call for the structured-output block (when
structuredOutputToolName matches) but later unconditionally reads
contentBlockToToolCallIdx (and uses it for input_json_delta), causing a
zero-index continuation when the block was never registered; update the handling
in chat.go to check whether chunk.ContentBlock.Name exists in the
contentBlockToToolCallIdx map (use a map lookup/ok pattern) before using the
index and, if not present, ignore/skip any input_json_delta or continuation
handling for that block (same guard should be added to the other occurrence
around contentBlockStart/input_json_delta handling at the referenced nearby
lines).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@core/providers/anthropic/chat.go`:
- Around line 1150-1152: The code skips emitting a tool call for the
structured-output block (when structuredOutputToolName matches) but later
unconditionally reads contentBlockToToolCallIdx (and uses it for
input_json_delta), causing a zero-index continuation when the block was never
registered; update the handling in chat.go to check whether
chunk.ContentBlock.Name exists in the contentBlockToToolCallIdx map (use a map
lookup/ok pattern) before using the index and, if not present, ignore/skip any
input_json_delta or continuation handling for that block (same guard should be
added to the other occurrence around contentBlockStart/input_json_delta handling
at the referenced nearby lines).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45470367-1b5b-404a-ac9f-c8bb8abda2b9

📥 Commits

Reviewing files that changed from the base of the PR and between c6788b0 and f83b880.

📒 Files selected for processing (2)
  • core/providers/anthropic/chat.go
  • core/providers/anthropic/chat_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 26, 2026
@akshaydeo
akshaydeo dismissed coderabbitai[bot]’s stale review May 26, 2026 18:59

The merge-base changed after approval.

@akshaydeo
akshaydeo requested a review from a team as a code owner May 26, 2026 18:59
@rmarku
rmarku force-pushed the rmarku/fix-anthropic-tool-calling branch from f83b880 to 2995197 Compare May 27, 2026 11:41
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 27, 2026
@rmarku

rmarku commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on dev branch and fixed conflicts.

@akshaydeo
akshaydeo dismissed coderabbitai[bot]’s stale review May 29, 2026 12:41

The merge-base changed after approval.

@rmarku
rmarku force-pushed the rmarku/fix-anthropic-tool-calling branch from 2995197 to d075f48 Compare May 29, 2026 17:34
@akshaydeo

Copy link
Copy Markdown
Contributor

@TejasGhatte can you review this please

@akshaydeo
akshaydeo force-pushed the dev branch 3 times, most recently from 6711ce3 to a1beab5 Compare June 4, 2026 10:02
@akshaydeo
akshaydeo force-pushed the dev branch 2 times, most recently from e389df7 to a65fce4 Compare June 8, 2026 11:25
@rmarku

rmarku commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Do I need to update something? Just let me know. This is blocking our bifrost testing/adoption.

@akshaydeo
akshaydeo force-pushed the dev branch 3 times, most recently from fa15f50 to ca190fc Compare June 21, 2026 11:44
@fspaniol

Copy link
Copy Markdown

Hi @TejasGhatte can you please have an estimate on this review? I'm stuck waiting for this

@TejasGhatte

Copy link
Copy Markdown
Collaborator

Hi @TejasGhatte can you please have an estimate on this review? I'm stuck waiting for this

Hey @fspaniol checking this one

@fspaniol

Copy link
Copy Markdown

@akshaydeo it seems it needs another approval too, given that the org has 2 people, I guess it needs to be you 😄

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.

[Bug]: Anthropic→OpenAI streaming tool_call deltas violate OpenAI spec on continuation chunks (breaks strict clients)

4 participants