Skip to content

fix: give reasoning deltas their own output_item.added so Anthropic streams stay valid - #5221

Closed
Shaik-Sirajuddin wants to merge 1 commit into
maximhq:devfrom
Shaik-Sirajuddin:fix/anthropic-reasoning-delta-block
Closed

Shaik-Sirajuddin wants to merge 1 commit into
maximhq:devfrom
Shaik-Sirajuddin:fix/anthropic-reasoning-delta-block

Conversation

@Shaik-Sirajuddin

@Shaik-Sirajuddin Shaik-Sirajuddin commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #5169.

ToBifrostResponsesStreamResponse (used whenever ResponsesStream falls back to Chat Completions - Ollama, Groq, Cerebras, DeepSeek, Mistral, Nebius, Parasail, SGL, vLLM, Perplexity sonar-*) emitted reasoning_summary_text.delta for a model's reasoning field without ever sending a preceding output_item.added. Since the Anthropic reverse-converter keys content-block indices on Item.ID/ItemID, the orphaned delta resolved to nothing - a content_block_delta for a block never started. That's an SSE protocol violation: crashes the official anthropic-python SDK (IndexError: list index out of range), and independently explains Claude Code's duplicate stream+non-stream requests (#5128) for reasoning-capable models.

Fix: give the reasoning item its own output_item.added/output_item.done (mirroring the existing text/tool-call paths), close it before any tool call opens, fold it into the terminal Output-array sort, and drop the now-redundant phantom empty text item.

Verified: new unit tests (fail pre-fix, pass post-fix), full core/schemas + core/providers/anthropic suites pass, and a live e2e round-trip with the real anthropic Python SDK (0.116.0) against real Bifrost + Ollama - crashes before the fix, passes cleanly after (screenshots in comment below).

@Shaik-Sirajuddin
Shaik-Sirajuddin requested a review from a team as a code owner July 15, 2026 04:21
@Shaik-Sirajuddin

Shaik-Sirajuddin commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Live e2e verification (real anthropic-python SDK, real Ollama, real Bifrost server)

Screenshots below are of a live terminal session (captured via scrot on an isolated X display), running python3 e2e_sdk_roundtrip.py <bifrost-anthropic-url> <label> — a local, uncommitted round-trip script that drives the actual official anthropic Python SDK's client.messages.stream(...) accumulator against a real Bifrost instance wired to a real local Ollama (qwen3:0.6b), forcing the ResponsesStream -> ChatCompletionStream fallback path this issue is about.

Before fix (upstream/dev, issue present)

before fix - IndexError crash

All three scenarios (reasoning-only, parallel-tool-calls, reasoning+tool-call) crash identically with:

File ".../anthropic/lib/streaming/_messages.py", line 465, in accumulate_event
    content = current_snapshot.content[event.index]
IndexError: list index out of range

After fix (this branch)

after fix - clean completion

All three scenarios complete cleanly:

--- reasoning-only ---
  OK - 431 SSE events, final content blocks: ['thinking', 'text']
--- parallel-tool-calls ---
  OK - 239 SSE events, final content blocks: ['thinking', 'tool_use', 'tool_use']
--- reasoning-plus-tool-call ---
  OK - 725 SSE events, final content blocks: ['thinking', 'tool_use']

=== Summary for AFTER FIX (fix/anthropic-reasoning-delta-block) ===
  [PASS] reasoning_only: ['thinking', 'text']
  [PASS] parallel_tool_calls: ['thinking', 'tool_use', 'tool_use']
  [PASS] reasoning_plus_tool_call: ['thinking', 'tool_use']

(Screenshots are hosted on a separate orphan branch on my fork, not part of this PR's diff.)

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1397ed05-94ec-47ce-b236-d967511bef29

📥 Commits

Reviewing files that changed from the base of the PR and between 2cba088 and 57f75e3.

📒 Files selected for processing (2)
  • core/schemas/mux.go
  • core/schemas/mux_test.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Refined streaming conversion for responses that include reasoning and tool calls, with correct lifecycle handling and stable reasoning output identifiers.
    • Avoided emitting phantom/empty text delta events for reasoning-only output.
    • Ensured reasoning “done” events are emitted before tool-call output begins, with deterministic final ordering for both reasoning and tool outputs.
  • Tests

    • Added unit tests covering reasoning delta timing, proper closure/completion assembly, and correct sequencing relative to tool calls.

Walkthrough

Chat-to-Responses streaming now gives reasoning output items stable identifiers and lifecycle events, prevents empty text deltas, closes reasoning before tool calls, and includes reasoning in deterministic terminal output aggregation.

Changes

Reasoning stream lifecycle

Layer / File(s) Summary
Reasoning state and closure
core/schemas/mux.go
Stream state tracks reasoning item creation, closure, output index, and buffered text; pooled state is initialized and reset, and open reasoning items emit completion events when closed.
Reasoning and tool-call event ordering
core/schemas/mux.go, core/schemas/mux_test.go
Reasoning deltas create identified output items and emit matching deltas, empty text items are suppressed, reasoning closes before tool calls and terminal completion, and sequencing is tested.
Terminal reasoning aggregation
core/schemas/mux.go
Terminal output combines reasoning and tool-call messages and sorts them by output index.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • maximhq/bifrost#5170 — Modifies the same Chat-to-Responses reasoning event lifecycle and related mux streaming tests.
  • maximhq/bifrost#3584 — Updates reasoning conversion ordering relative to tool-call content.
  • maximhq/bifrost#5094 — Adjusts Responses reasoning output lifecycle and ordering around tool-use content.

Sequence Diagram(s)

sequenceDiagram
  participant ChatCompletionStream
  participant ToBifrostResponsesStreamResponse
  participant ResponsesClient
  ChatCompletionStream->>ToBifrostResponsesStreamResponse: reasoning delta
  ToBifrostResponsesStreamResponse->>ResponsesClient: output_item.added with ItemID
  ToBifrostResponsesStreamResponse->>ResponsesClient: reasoning_summary_text.delta
  ToBifrostResponsesStreamResponse->>ResponsesClient: output_item.done
  ToBifrostResponsesStreamResponse->>ResponsesClient: tool-call output_item.added
Loading

Suggested reviewers: akshaydeo, tejasghatte, roroghost17

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 accurately summarizes the main fix: giving reasoning deltas their own output item for valid Anthropic streaming.
Description check ✅ Passed The PR description covers the problem, fix, and verification, and is mostly complete despite missing some template sections.
Linked Issues check ✅ Passed The changes match #5169 by adding a reasoning output_item.added, stable IDs, proper closure ordering, and tests for the streaming fix.
Out of Scope Changes check ✅ Passed The diff stays focused on the Anthropic-compatible streaming fix and its tests, with no clear unrelated changes.
✨ 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.

…treams stay valid

ToBifrostResponsesStreamResponse emitted reasoning_summary_text.delta
events without a preceding output_item.added and without Item/ItemID,
so the Anthropic reverse-converter (keyed on Item.ID > ItemID >
"oi:<OutputIndex>") registered no block for them - a content_block_delta
for a block whose content_block_start was never sent. This crashes the
official anthropic-python SDK (IndexError: list index out of range) and
explains Claude Code's duplicate stream+non-stream requests for
reasoning-capable models behind Chat-Completions-fallback providers.

Give the reasoning item its own output_item.added/output_item.done
(mirroring the text/tool-call paths), close it before any tool call
opens, fold it into the terminal Output-array sort, and drop the now
unnecessary phantom empty text item that used to stand in for it.

Fixes maximhq#5169.
@Shaik-Sirajuddin
Shaik-Sirajuddin force-pushed the fix/anthropic-reasoning-delta-block branch 2 times, most recently from 4680f06 to 57f75e3 Compare July 15, 2026 04:23
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

Mixed and resumed reasoning streams can still produce invalid content-block lifecycles.

  • Reasoning that resumes after a tool call targets an already-closed item.
  • Text followed by reasoning can leave both output items open at once.
  • Pool initialization and reset logic covers all newly added fields.

core/schemas/mux.go

Important Files Changed

Filename Overview
core/schemas/mux.go Adds the reasoning output-item lifecycle and terminal output integration, but some mixed chunk orders still produce invalid item transitions.
core/schemas/mux_test.go Covers reasoning registration, accumulation, completion, and closure before a tool call.

Reviews (1): Last reviewed commit: "fix: give reasoning deltas their own out..." | Re-trigger Greptile

Comment thread core/schemas/mux.go
Comment on lines +2029 to +2064
// otherwise downstream Anthropic-format consumers see a
// content_block_delta for a block whose content_block_start was
// never sent, which strict SSE clients reject.
if !state.ReasoningItemAdded {
outputIndex := state.CurrentOutputIndex
if outputIndex == 0 {
outputIndex = 1 // Skip 0 if text is using it
}
state.CurrentOutputIndex = outputIndex + 1
state.ReasoningOutputIndex = outputIndex

var itemID string
if state.MessageID == nil {
itemID = fmt.Sprintf("rs_item_%d", outputIndex)
} else {
itemID = fmt.Sprintf("rs_%s_item_%d", *state.MessageID, outputIndex)
}
state.ItemIDs["reasoning"] = itemID

reasoningType := ResponsesMessageTypeReasoning
role := ResponsesInputMessageRoleAssistant
item := &ResponsesMessage{
ID: &itemID,
Type: &reasoningType,
Role: &role,
}

responses = append(responses, &BifrostResponsesStreamResponse{
Type: ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: state.SequenceNumber,
OutputIndex: Ptr(outputIndex),
Item: item,
ExtraFields: cr.ExtraFields,
})
state.SequenceNumber++
state.ReasoningItemAdded = true

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.

P1 Closed Reasoning Item Receives Deltas

When a stream emits reasoning, then a tool call, then more reasoning, the tool-call path closes the reasoning item but leaves ReasoningItemAdded true. The later reasoning chunk therefore skips output_item.added and emits a delta for an item that already received output_item.done, producing an invalid Anthropic content-block lifecycle.

Context Used: Review Bifrost PRs for correctness, regressions, c... (source)

Comment thread core/schemas/mux.go
Comment on lines 2025 to +2064
if delta.Reasoning != nil && *delta.Reasoning != "" {
// Reasoning/thought content delta (for models that support reasoning)
// Reasoning/thought content delta (for models that support reasoning).
// Give the reasoning item its own output_item.added (with a stable
// Item.ID) before the first delta, mirroring the text item above -
// otherwise downstream Anthropic-format consumers see a
// content_block_delta for a block whose content_block_start was
// never sent, which strict SSE clients reject.
if !state.ReasoningItemAdded {
outputIndex := state.CurrentOutputIndex
if outputIndex == 0 {
outputIndex = 1 // Skip 0 if text is using it
}
state.CurrentOutputIndex = outputIndex + 1
state.ReasoningOutputIndex = outputIndex

var itemID string
if state.MessageID == nil {
itemID = fmt.Sprintf("rs_item_%d", outputIndex)
} else {
itemID = fmt.Sprintf("rs_%s_item_%d", *state.MessageID, outputIndex)
}
state.ItemIDs["reasoning"] = itemID

reasoningType := ResponsesMessageTypeReasoning
role := ResponsesInputMessageRoleAssistant
item := &ResponsesMessage{
ID: &itemID,
Type: &reasoningType,
Role: &role,
}

responses = append(responses, &BifrostResponsesStreamResponse{
Type: ResponsesStreamResponseTypeOutputItemAdded,
SequenceNumber: state.SequenceNumber,
OutputIndex: Ptr(outputIndex),
Item: item,
ExtraFields: cr.ExtraFields,
})
state.SequenceNumber++
state.ReasoningItemAdded = true

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.

P1 Text And Reasoning Items Overlap

When a provider emits text before reasoning, or includes both fields in one chunk, the text path opens output index 0 and this path opens the reasoning item without closing the text item. Both remain active until a tool call or terminal event, so the Anthropic reverse converter can emit interleaved content blocks instead of a valid start/delta/stop sequence.

Context Used: Review Bifrost PRs for correctness, regressions, c... (source)

@Shaik-Sirajuddin

Copy link
Copy Markdown
Contributor Author

Duplicate of #5170, which was opened first (2026-07-14) and already has review feedback addressed. Closing this one in favor of that.

@Shaik-Sirajuddin
Shaik-Sirajuddin deleted the fix/anthropic-reasoning-delta-block branch July 15, 2026 04:28

@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/schemas/mux.go (1)

1768-1855: 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Close the sibling item before opening text or reasoning.
core/schemas/mux.go:1775 and core/schemas/mux.go:2032 open a new block without closing the other one first, so reasoning-first responses that later emit content can leave two content blocks open until the terminal/tool-call path. The current tests cover reasoning→tool-call, not reasoning→text.

🤖 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/schemas/mux.go` around lines 1768 - 1855, Update the text-content
emission path and the corresponding reasoning emission path so any currently
open sibling content block is closed before opening the new text or reasoning
block. Ensure reasoning-first responses that later emit content close reasoning
before creating text, while preserving existing terminal and tool-call lifecycle
behavior.
🤖 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/schemas/mux.go`:
- Around line 1768-1855: Update the text-content emission path and the
corresponding reasoning emission path so any currently open sibling content
block is closed before opening the new text or reasoning block. Ensure
reasoning-first responses that later emit content close reasoning before
creating text, while preserving existing terminal and tool-call lifecycle
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8bffd4da-508d-46ec-9b6e-bafa815f868e

📥 Commits

Reviewing files that changed from the base of the PR and between af5c9f0 and 2cba088.

⛔ Files ignored due to path filters (2)
  • .github/pr-evidence/issue-5169-after-fix.png is excluded by !**/*.png
  • .github/pr-evidence/issue-5169-before-fix.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • core/schemas/mux.go
  • core/schemas/mux_test.go

@coderabbitai
coderabbitai Bot requested a review from danpiths July 15, 2026 04:30
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-compat streaming reasoning delta has no output_item.added, crashes strict SSE clients (e.g. official anthropic-python SDK)

1 participant