Skip to content

fix: report stop_reason tool_use for tool calls on Anthropic-compatible egress - #4900

Open
eyeveil wants to merge 2 commits into
maximhq:devfrom
eyeveil:fix/4065-anthropic-stop-reason
Open

eyeveil wants to merge 2 commits into
maximhq:devfrom
eyeveil:fix/4065-anthropic-stop-reason

Conversation

@eyeveil

@eyeveil eyeveil commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Problem

When Bifrost's Anthropic-compatible ingress (/anthropic/v1/messages) routes to a non-Anthropic backend (e.g. Azure OpenAI), a tool-calling turn emits a correct tool_use content block but the terminal message_delta.delta.stop_reason is "end_turn" instead of "tool_use". Anthropic SDK consumers that dispatch tools on stop_reason == "tool_use" (the documented signal) fail to detect the tool call.

Root cause

The Anthropic Responses streaming egress converter defaulted response.completed to stop_reason: "end_turn" unless BifrostResponsesResponse.StopReason was explicitly set. OpenAI/Azure Responses-style completed payloads can carry function_call output items with no Bifrost stop reason — the converter had enough information to know the turn requested a tool, but didn't infer "tool_use".

Fix

Centralized Anthropic stop-reason inference for Responses egress (inferAnthropicStopReasonFromBifrostResponse), used by both the non-streaming conversion and the streaming response.completedmessage_delta conversion:

  • explicit stop reason mapping preserved ("tool_calls""tool_use", etc. via the existing mapping table);
  • nil stop reason + function_call output items ⇒ "tool_use";
  • nil stop reason + converted tool_use content blocks ⇒ "tool_use" (existing non-streaming inference, kept);
  • otherwise "end_turn".

Testing

  • Extended the non-streaming stop-reason table test (tool_calls + function-call output ⇒ tool_use).
  • Added TestToAnthropicResponsesStreamResponse_CompletedWithFunctionCallInfersToolUseStopReason covering the reported streaming case: response.completed, nil StopReason, function-call output ⇒ message_delta with stop_reason: "tool_use".
  • go build ./..., go vet ./providers/anthropic/, go test -count=1 ./providers/anthropic/ (core, cgo enabled) — all pass.

Fixes #4065

…ing turns on egress

The /anthropic/v1/messages egress defaulted response.completed to
stop_reason end_turn when the Bifrost StopReason was nil, even when the
output carried function_call items. Anthropic SDK consumers that branch
on stop_reason == "tool_use" missed tool calls when routing to
non-Anthropic backends. Stop-reason inference is now shared between the
streaming (message_delta) and non-streaming conversions: explicit stop
reason wins, function_call output infers tool_use, else end_turn.

Affected packages:
- core/providers/anthropic/responses.go

Fixes maximhq#4065

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5126b101-228a-41e3-95ee-a9d15ea937ab

📥 Commits

Reviewing files that changed from the base of the PR and between f45804f and dfd0878.

📒 Files selected for processing (2)
  • core/providers/anthropic/compaction_test.go
  • core/providers/anthropic/responses.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/providers/anthropic/responses.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved stop-reason handling for Anthropic responses and streams so tool usage is now detected more consistently.
    • Responses with tool-call data now correctly return tool_calls instead of defaulting to end_turn.
    • Streamed completed responses now emit the expected message_delta followed by message_stop with the correct stop reason.
    • Updated behavior for cases with reasoning-only function calls to avoid incorrectly inferring tool use.

Walkthrough

This PR changes Anthropic stop-reason derivation for streaming and non-streaming response conversion. It adds shared inference logic for function_call and tool_use handling, updates both conversion paths to use it, and expands tests around completed and streaming responses.

Changes

Stop reason inference fix

Layer / File(s) Summary
Stop-reason inference helper implementation
core/providers/anthropic/responses.go
Adds helper functions that detect function_call output and tool_use content blocks, then infer the Anthropic stop reason from Bifrost response data with end_turn as the fallback.
Wiring inference into response conversion paths
core/providers/anthropic/responses.go
Updates the streaming message_delta path and ToAnthropicResponsesResponse to use the shared stop-reason inference helper instead of the previous hard-coded logic.
Test coverage for function_call stop reason inference
core/providers/anthropic/compaction_test.go
Refactors the existing stop-reason test cases for function_call inputs and adds a streaming test that asserts tool_calls is emitted for completed responses with tool-call data.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • maximhq/bifrost#3640: Both PRs adjust Anthropic stop-reason handling for tool-call responses in conversion paths.
  • maximhq/bifrost#3685: Both PRs update stop-reason mapping logic in core/providers/anthropic/responses.go.

Suggested reviewers: danpiths, TejasGhatte, akshaydeo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: reporting tool-use stop_reason for Anthropic-compatible egress.
Description check ✅ Passed The description covers the problem, root cause, fix, testing, and related issue, though several template sections are omitted.
Linked Issues check ✅ Passed The changes map backend tool_calls/function_call outputs to Anthropic tool_use stop_reason and add regression tests for the reported case.
Out of Scope Changes check ✅ Passed The modified files stay within Anthropic response logic and tests, with no unrelated out-of-scope changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the change is a targeted, pure-function extraction that fixes a well-scoped streaming egress bug with no side effects on other provider paths.

The helper is a pure transformation with no HTTP calls or shared state; the inference logic is correct and all branches are exercised by existing or new tests. The only behavior difference outside the bug fix is that message_delta.stop_sequence now serializes as null instead of "", which is more spec-compliant. No regressions in the non-streaming path are introduced by the ordering of the new output check.

No files require special attention.

Important Files Changed

Filename Overview
core/providers/anthropic/responses.go Introduces inferAnthropicStopReasonFromBifrostResponse helper that correctly infers tool_use from function-call output items; wires it into both streaming and non-streaming Responses egress paths; removes the spurious stop_sequence: "" from message_delta.
core/providers/anthropic/compaction_test.go Adds streaming test for function-call inferred tool_use stop reason and expands the non-streaming stop-reason table with explicit-mapping and reasoning-exclusion cases.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["response.completed / ToAnthropicResponsesResponse"] --> B["inferAnthropicStopReasonFromBifrostResponse(bifrostResp, contentBlocks)"]
    B --> C{bifrostResp.StopReason != nil?}
    C -- Yes --> D["ConvertBifrostFinishReasonToAnthropic(stopReason)\ne.g. 'tool_calls' → tool_use"]
    C -- No --> E{"bifrostResponsesOutputHasFunctionCall(Output)?"}
    E -- "Yes (has ResponsesToolMessage, no ResponsesReasoning)" --> F["tool_use"]
    E -- No --> G{"anthropicContentBlocksHaveToolUse(contentBlocks)?"}
    G -- Yes --> H["tool_use"]
    G -- No --> I["end_turn"]

    D --> Z["Emit message_delta.stop_reason"]
    F --> Z
    H --> Z
    I --> Z
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["response.completed / ToAnthropicResponsesResponse"] --> B["inferAnthropicStopReasonFromBifrostResponse(bifrostResp, contentBlocks)"]
    B --> C{bifrostResp.StopReason != nil?}
    C -- Yes --> D["ConvertBifrostFinishReasonToAnthropic(stopReason)\ne.g. 'tool_calls' → tool_use"]
    C -- No --> E{"bifrostResponsesOutputHasFunctionCall(Output)?"}
    E -- "Yes (has ResponsesToolMessage, no ResponsesReasoning)" --> F["tool_use"]
    E -- No --> G{"anthropicContentBlocksHaveToolUse(contentBlocks)?"}
    G -- Yes --> H["tool_use"]
    G -- No --> I["end_turn"]

    D --> Z["Emit message_delta.stop_reason"]
    F --> Z
    H --> Z
    I --> Z
Loading

Reviews (2): Last reviewed commit: "[fix]: Anthropic provider - do not infer..." | Re-trigger Greptile

Comment thread core/providers/anthropic/compaction_test.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.

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/responses.go`:
- Around line 3541-3545: The helper bifrostResponsesOutputHasFunctionCall is
incorrectly treating all ResponsesMessageTypeFunctionCall items as tool calls,
even when they are reasoning-only. Update this function to ignore function_call
messages that have ResponsesReasoning set, matching the nearby streaming
conversion logic, so only real tool-use items cause a tool-use stop_reason.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b228e3f-9200-43d8-97cd-73c422e0b47e

📥 Commits

Reviewing files that changed from the base of the PR and between 6484317 and f45804f.

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

Comment thread core/providers/anthropic/responses.go
… function_call items

Addresses review on PR maximhq#4900: reasoning-only items can carry the
function_call type but convert to thinking blocks, so they must not
force stop_reason tool_use; also restores the explicit
tool_calls-without-output mapping test case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eyeveil

eyeveil commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review — the reasoning-only function_call skip in bifrostResponsesOutputHasFunctionCall already landed in dfd0878 (with a regression test); the CHANGES_REQUESTED review predates that commit.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr view 4900 --repo maximhq/bifrost --json commits --jq '.commits[] | .oid + " " + .messageHeadline' | head -50
echo "---"
gh api repos/maximhq/bifrost/commits/dfd0878d6 --jq '.sha, .commit.message' 2>/dev/null || echo "commit not found directly"

Length of output: 1067


🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh api repos/maximhq/bifrost/commits/dfd0878d68aa8be3b79d6e1641d5ac50172b848d --jq '.files[].filename'
echo "---diff---"
gh api repos/maximhq/bifrost/commits/dfd0878d68aa8be3b79d6e1641d5ac50172b848d --jq '.files[].patch' 2>/dev/null | head -150

Length of output: 2069


@nnNyx, confirmed — commit dfd0878 adds the guard to bifrostResponsesOutputHasFunctionCall (now requiring ResponsesReasoning == nil && ResponsesToolMessage != nil) plus the regression test "reasoning-only function_call output does not infer tool_use" in core/providers/anthropic/compaction_test.go. This directly addresses the concern from my earlier CHANGES_REQUESTED review. Triggering a fresh review now to re-evaluate against the latest commits.

✅ Action performed

Review finished.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants