Skip to content

fix: preserve tool call stop reason in Anthropic streaming fallback#3640

Open
dicnunz wants to merge 2 commits into
maximhq:devfrom
dicnunz:fix-anthropic-tool-use-stop-reason
Open

fix: preserve tool call stop reason in Anthropic streaming fallback#3640
dicnunz wants to merge 2 commits into
maximhq:devfrom
dicnunz:fix-anthropic-tool-use-stop-reason

Conversation

@dicnunz
Copy link
Copy Markdown

@dicnunz dicnunz commented May 20, 2026

Summary

Fixes #3638.

When OpenAI-compatible chat streaming is converted into Bifrost Responses stream events, the terminal event now preserves recognized chat finish_reason values as Response.StopReason.

That lets the Anthropic serializer map a mixed text + tool-call stream ending with finish_reason: "tool_calls" to:

{"stop_reason":"tool_use"}

instead of falling back to end_turn.

What changed

  • Preserve mapped chat finish reasons (stop, tool_calls, length) on non-stream Responses conversion.
  • Preserve the same stop reason on terminal streaming Responses events.
  • Keep the existing status mapping behavior separate:
    • stop / tool_calls -> completed
    • length -> incomplete with max_output_tokens
    • unknown finish reasons remain unset.
  • Add regression coverage for the mixed text + tool-call Anthropic stream case.

Tests

go test ./schemas -run 'TestToBifrostResponses(StreamResponse|Response)_' -count=1
ok  github.com/maximhq/bifrost/core/schemas  0.491s

go test ./providers/anthropic -run 'TestMixedTextThenToolCallsStreamMapsStopReasonToToolUse|TestToAnthropicResponsesResponse_StopReasonFromBifrost|TestToAnthropicResponsesStreamResponse_CompletedWithCompactionStopReason' -count=1
ok  github.com/maximhq/bifrost/core/providers/anthropic  0.533s

@dicnunz
Copy link
Copy Markdown
Author

dicnunz commented May 20, 2026

Payment note: I saw this was an active company OSS bug with a narrow reproduction and no public bounty. If this saves the team time and you want to settle it as a small paid microfix, $10 via this checkout works: https://nicdunz.gumroad.com/l/slop-kill-pass?wanted=true

No action needed if you prefer to review it as a normal OSS contribution; the fix and focused tests are submitted either way.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 742e53d1-e36a-4424-b58b-c8346ebee8e7

📥 Commits

Reviewing files that changed from the base of the PR and between f8cd1cb and 2eb3900.

📒 Files selected for processing (1)
  • core/providers/anthropic/compaction_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Tests

    • Added coverage validating streaming responses that mix text and tool calls, ensuring final stream events include proper stop markers.
  • Bug Fixes

    • Fixed stop-reason mapping and propagation so completion/incompletion reasons are consistently preserved across streaming and aggregated responses.

Walkthrough

This PR fixes stop_reason propagation in OpenAI-to-Anthropic chat stream conversion. It adds a helper to map Chat finish_reason to Responses stop_reason, applies the helper to both streaming and non-streaming response conversions, and validates the behavior with schema-level and provider-level tests.

Changes

StopReason Propagation for Chat Responses

Layer / File(s) Summary
Stop-reason mapping helper
core/schemas/mux.go
New responsesStopReasonFromChatFinishReason helper maps Chat finish_reason strings to Responses stop_reason pointers, returning nil for empty or unmapped values.
Non-streaming response conversion
core/schemas/mux.go, core/schemas/mux_test.go
ToBifrostResponsesResponse now populates StopReason using the helper for each choice's finish reason, including when status is incomplete. Tests verify StopReason is set correctly for length (incomplete), tool_calls (completed), and unknown finish reasons.
Streaming response conversion and tests
core/schemas/mux.go, core/schemas/mux_test.go
Streaming terminal response emission sets StopReason using the helper. Tests assert StopReason is propagated in streaming completed and incomplete events.
Anthropic provider integration test
core/providers/anthropic/compaction_test.go
New test validates mixed text + tool-call OpenAI stream sequences convert through Bifrost and emit Anthropic message_delta with stop_reason of tool_use, followed by message_stop.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • danpiths
  • akshaydeo

Poem

A rabbit hops through streaming flows,
Where finish_reason at the ending shows.
Tool calls and text now keep their cue,
Anthropic sees the stop_reason true,
Hooray — the mapper did what it knew. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: preserve tool call stop reason in Anthropic streaming fallback' directly and clearly describes the main change—fixing stop reason preservation for tool calls in Anthropic streaming fallback scenarios.
Description check ✅ Passed The description covers the bug being fixed, what changed, includes test commands, and links to issue #3638, matching the template's core sections adequately.
Linked Issues check ✅ Passed The PR fully addresses issue #3638 by preserving OpenAI chat finish_reason values as Bifrost StopReason in both stream and non-stream conversions, updating the Anthropic serializer mapping, and adding comprehensive regression tests for schema and Anthropic provider levels.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the stated objectives: helper function for finish_reason mapping, response conversion updates, and targeted test additions covering the mixed text + tool-call scenario.

✏️ 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.

@coderabbitai coderabbitai Bot requested review from akshaydeo and danpiths May 20, 2026 16:57
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

🤖 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.

Inline comments:
In `@core/providers/anthropic/compaction_test.go`:
- Around line 774-787: The test currently only asserts the intermediate
message_delta stop_reason; add an assertion that a terminal message_stop event
exists in events and that its StopReason equals AnthropicStopReasonToolUse.
Locate the loop over events (which finds messageDelta using
AnthropicStreamEventTypeMessageDelta) and similarly scan for an event with type
AnthropicStreamEventTypeMessageStop (e.g., assign to messageStop), verify
messageStop and messageStop.Delta (or equivalent terminal field) are non-nil,
and assert *messageStop.Delta.StopReason == AnthropicStopReasonToolUse to
prevent mixed-stream regression.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26417184-df5f-48ee-8b32-8cb1ddc2b3d4

📥 Commits

Reviewing files that changed from the base of the PR and between 8596f7a and f8cd1cb.

📒 Files selected for processing (3)
  • core/providers/anthropic/compaction_test.go
  • core/schemas/mux.go
  • core/schemas/mux_test.go

Comment thread core/providers/anthropic/compaction_test.go
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Confidence Score: 5/5

The change is narrowly scoped to populating a previously-unset field; no existing behavior is removed and unknown finish reasons continue to return nil, leaving all current fallback paths intact.

The fix is small, mechanically straightforward, and the new helper delegates to the already-tested responsesStatusFromChatFinishReason so only recognized values are forwarded. Tests cover all three affected branches (stop/tool_calls → completed, length → incomplete, unknown → nil) plus the full streaming round-trip through the Anthropic serializer.

No files require special attention.

Important Files Changed

Filename Overview
core/schemas/mux.go Adds responsesStopReasonFromChatFinishReason helper and wires StopReason onto BifrostResponsesResponse for both non-streaming and streaming terminal events; only propagates recognized finish reasons ("stop", "tool_calls", "length")
core/schemas/mux_test.go Extends four existing tests with StopReason assertions covering length→incomplete, tool_calls→completed, unknown→nil, and streaming completed cases
core/providers/anthropic/compaction_test.go Adds end-to-end regression test for a mixed text + tool-call stream, verifying the terminal message_delta carries stop_reason: "tool_use" instead of falling back to end_turn

Reviews (2): Last reviewed commit: "test: assert Anthropic message stop afte..." | Re-trigger Greptile

@dicnunz
Copy link
Copy Markdown
Author

dicnunz commented May 20, 2026

Addressed the valid CodeRabbit test coverage point in 2eb3900. The mixed text + tool-use regression test now also asserts that a terminal message_stop event is emitted after the message_delta.stop_reason == tool_use assertion.

I did not put a stop reason on message_stop because Anthropic streaming carries stop_reason on message_delta; message_stop is the terminal marker. Focused tests still pass:

go test ./schemas -run 'TestToBifrostResponses(StreamResponse|Response)_' -count=1\nok github.com/maximhq/bifrost/core/schemas 0.674s\n\ngo test ./providers/anthropic -run 'TestMixedTextThenToolCallsStreamMapsStopReasonToToolUse|TestToAnthropicResponsesResponse_StopReasonFromBifrost|TestToAnthropicResponsesStreamResponse_CompletedWithCompactionStopReason' -count=1\nok github.com/maximhq/bifrost/core/providers/anthropic 0.720s\n```

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 streaming emits wrong stop_reason after mixed text + tool-call turns

2 participants