Skip to content

fix: capture responses streaming api error#2681

Merged
akshaydeo merged 5 commits intomainfrom
04-13-fix_capture_responses_streaming_api_error
Apr 15, 2026
Merged

fix: capture responses streaming api error#2681
akshaydeo merged 5 commits intomainfrom
04-13-fix_capture_responses_streaming_api_error

Conversation

@BearTS
Copy link
Copy Markdown
Contributor

@BearTS BearTS commented Apr 13, 2026

Summary

Improves error handling and logging for streaming responses by ensuring error messages and codes are properly propagated, fixing chunk indexing for error responses, and enhancing log data retention during streaming failures.

Changes

  • Enhanced OpenAI streaming error handling to extract error messages and codes from nested response structures
  • Fixed streaming accumulator to handle cases where only error information is available (no result object)
  • Improved error chunk indexing to prevent collision with successful response chunks by using unique trailing indices
  • Added raw response data to error chunks for better debugging visibility
  • Enhanced logging plugin to finalize and capture accumulated stream metadata before processing errors
  • Prevented overwriting of existing raw request/response data in error logs

Type of change

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

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

Test streaming error scenarios to verify proper error propagation and logging:

# Core/Transports
go version
go test ./...

# Test streaming responses with errors
# Verify error messages are properly extracted and logged
# Check that chunk indices don't collide between success and error responses
# Ensure raw response data is preserved in error cases

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

Related issues

N/A

Security considerations

No security implications - this change improves error handling and logging without exposing additional sensitive data.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

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: fcc0e638-59d6-40e5-a360-500b45d98469

📥 Commits

Reviewing files that changed from the base of the PR and between a50cdde and a6b9fb5.

📒 Files selected for processing (4)
  • framework/streaming/accumulator.go
  • framework/streaming/responses.go
  • framework/streaming/types.go
  • plugins/logging/main.go
✅ Files skipped from review due to trivial changes (1)
  • framework/streaming/types.go

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate terminal error chunks by reserving and reusing a dedicated error chunk index.
    • Ensured error payloads are passed through to error enrichment and reporting for accurate diagnostics.
  • Improvements

    • Preserves raw error responses on streaming failures so downstream consumers receive full context.
    • Enables streaming classification and routing when only an error is present.
    • Finalizes and logs streaming output reliably at error completion; accumulator initializes terminal-error state correctly.

Walkthrough

Provider streaming error handling now preserves nested provider error fields and the SSE payload; the streaming accumulator accepts error-only events and reserves/reuses a terminal error chunk index; terminal error response chunks include serialized RawResponse; logging finalizes, converts, applies, and cleans up accumulated streaming output on error.

Changes

Cohort / File(s) Summary
Provider Error Enrichment
core/providers/openai/openai.go
Copy nested response.Response.Error.Message and Code into bifrostErr when unset; pass the SSE chunk payload ([]byte(jsonData)) into providerUtils.EnrichError for ResponsesStreamResponseTypeError and ResponsesStreamResponseTypeFailed events.
Streaming Accumulator & Types
framework/streaming/accumulator.go, framework/streaming/types.go
Initialize TerminalErrorChunkIndex = -1 for new accumulators; accept error-only inputs (allow result == nil && bifrostErr != nil), derive requestType from bifrostErr when result is nil; added TerminalErrorChunkIndex field to StreamAccumulator.
Response Chunking
framework/streaming/responses.go
When bifrostErr != nil and bifrostErr.ExtraFields.RawResponse != nil, marshal that raw response into chunk.RawResponse; reserve/reuse a terminal error ChunkIndex under accumulator lock (set to MaxResponsesChunkIndex+1 if unset) to deduplicate terminal-error chunks.
Logging Plugin
plugins/logging/main.go, plugins/logging/operations.go
On streaming error with result == nil, finalize accumulator via ProcessStreamingChunk(..., true, ...), optionally convert accumulated output, apply streaming output to the log entry, then CleanupStreamAccumulator; only populate entry.RawRequest/entry.RawResponse from error extra fields when entry fields are still empty; status/latency assignment moved into explicit success/error branches.

Sequence Diagram(s)

sequenceDiagram
    participant Provider as OpenAI Provider
    participant Accumulator as Streaming Accumulator
    participant Responses as Response Builder
    participant Logger as Logging Plugin

    Provider->>Provider: Receive SSE chunk (error), parse JSON
    Provider->>Provider: Copy nested provider error fields into bifrostErr
    Provider->>Provider: Enrich Error (pass SSE payload)
    Provider->>Accumulator: ProcessStreamingChunk(result=nil, bifrostErr)

    Accumulator->>Accumulator: Accept error-only event
    Accumulator->>Accumulator: Derive requestType from bifrostErr.ExtraFields
    Accumulator->>Responses: Build error chunk (marshal RawResponse)
    Responses->>Accumulator: Lock and assign/reuse TerminalErrorChunkIndex
    Responses->>Logger: Emit finalized error chunk

    Logger->>Logger: Finalize accumulator, convert accumulated stream if needed
    Logger->>Logger: Apply streaming output to log entry
    Logger->>Logger: Cleanup stream accumulator
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 I munched the SSE that came at night,
Nested errors saved in tidy light,
An index set where duplicates cease,
Streams close clean — logs breathe peace,
Hops of joy, a streaming bite!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing error capture in the responses streaming API, which aligns with the substantial error handling improvements across five files.
Description check ✅ Passed The description provides comprehensive coverage of all required template sections including summary, detailed changes, type of change, affected areas, testing guidance, and security considerations.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 04-13-fix_capture_responses_streaming_api_error

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 Author

BearTS commented Apr 13, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 13, 2026

CLA assistant check
All committers have signed the CLA.

@BearTS BearTS force-pushed the 04-13-fix_capture_responses_streaming_api_error branch from 692fc19 to 48b03ec Compare April 13, 2026 13:46
@BearTS BearTS marked this pull request as ready for review April 13, 2026 13:47
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

🧹 Nitpick comments (1)
framework/streaming/responses.go (1)

903-912: Minor race window in ChunkIndex assignment.

There's a small TOCTOU window between reading MaxResponsesChunkIndex (line 911) and adding the chunk via addResponsesStreamChunk (line 934). If another goroutine adds a chunk in this window, the computed index could collide.

In practice this is low-risk since:

  1. Error chunks are terminal (stream is ending)
  2. Multiple concurrent error chunks for the same request is unlikely

Consider computing the unique index atomically inside addResponsesStreamChunk by passing a sentinel value (e.g., -1) for error chunks and having the method assign the next index:

🔧 Optional: Atomic index assignment
 	if bifrostErr != nil {
 		chunk.FinishReason = bifrost.Ptr("error")
 		if rawBytes, marshalErr := sonic.Marshal(bifrostErr.ExtraFields.RawResponse); marshalErr == nil && len(rawBytes) > 0 {
 			chunk.RawResponse = bifrost.Ptr(string(rawBytes))
 		}
-		// Error chunks may arrive without a stream chunk index (result is nil), which can
-		// collide with index 0 (e.g., response.created) and get deduplicated away.
-		// Force a unique trailing index so both prior chunks and terminal error are retained.
-		accumulator := a.getOrCreateStreamAccumulator(requestID)
-		accumulator.mu.Lock()
-		chunk.ChunkIndex = accumulator.MaxResponsesChunkIndex + 1
-		accumulator.mu.Unlock()
+		// Use sentinel value -1 to signal that addResponsesStreamChunk should assign next unique index
+		chunk.ChunkIndex = -1
 	}

Then update addResponsesStreamChunk to handle the sentinel:

// In addResponsesStreamChunk, after acquiring the lock:
if chunk.ChunkIndex == -1 {
    chunk.ChunkIndex = accumulator.MaxResponsesChunkIndex + 1
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/streaming/responses.go` around lines 903 - 912, There’s a TOCTOU
race when computing chunk.ChunkIndex using accumulator.MaxResponsesChunkIndex
while unlocked; instead pass a sentinel (e.g., -1) for error chunks from the
caller (where getOrCreateStreamAccumulator is used) and let
addResponsesStreamChunk perform the atomic assignment while holding the
accumulator lock: in addResponsesStreamChunk, after acquiring accumulator.mu,
detect if chunk.ChunkIndex == -1 and set it to
accumulator.MaxResponsesChunkIndex + 1 (then update MaxResponsesChunkIndex
accordingly); this moves the index computation/assignment into
addResponsesStreamChunk and eliminates the window around chunk index creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/providers/openai/openai.go`:
- Around line 1757-1758: The merge incorrectly skips using
response.Response.Error.Code when bifrostErr.Error.Code exists but is an empty
string; update the fallback logic in openai.go so empty top-level codes are
treated as missing (same as message handling) by changing the condition that
assigns bifrostErr.Error.Code to check both nil and empty value—e.g., replace
the current if response.Response.Error.Code != "" && bifrostErr.Error.Code ==
nil with a check like response.Response.Error.Code != "" &&
(bifrostErr.Error.Code == nil || *bifrostErr.Error.Code == "") so the nested
code is used when the existing code pointer is nil or points to an empty string.

---

Nitpick comments:
In `@framework/streaming/responses.go`:
- Around line 903-912: There’s a TOCTOU race when computing chunk.ChunkIndex
using accumulator.MaxResponsesChunkIndex while unlocked; instead pass a sentinel
(e.g., -1) for error chunks from the caller (where getOrCreateStreamAccumulator
is used) and let addResponsesStreamChunk perform the atomic assignment while
holding the accumulator lock: in addResponsesStreamChunk, after acquiring
accumulator.mu, detect if chunk.ChunkIndex == -1 and set it to
accumulator.MaxResponsesChunkIndex + 1 (then update MaxResponsesChunkIndex
accordingly); this moves the index computation/assignment into
addResponsesStreamChunk and eliminates the window around chunk index creation.
🪄 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: 6d1e9e6e-d571-4dc4-b760-c320685c4199

📥 Commits

Reviewing files that changed from the base of the PR and between 762f897 and 48b03ec.

📒 Files selected for processing (5)
  • core/providers/openai/openai.go
  • framework/streaming/accumulator.go
  • framework/streaming/responses.go
  • plugins/logging/main.go
  • plugins/logging/operations.go

Comment thread core/providers/openai/openai.go Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 13, 2026

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; all remaining findings are P2 edge cases unlikely to affect production behavior.

The core error-propagation fixes are correct and well-guarded. The TerminalErrorChunkIndex dedup mechanism is sound, and the logging path changes handle nil-result errors cleanly. The two P2 notes are theoretical (token-usage loss on providers that send usage mid-stream) or informational (undocumented logic fix in buildResponseForRequestType). Neither blocks merge.

framework/streaming/responses.go — the TerminalErrorChunkIndex / MaxResponsesChunkIndex coupling is worth a second look if token-usage accuracy for mid-stream errors becomes important.

Important Files Changed

Filename Overview
core/providers/openai/openai.go Extracts nested error message/code from response.Response.Error for both error event types, and passes the raw jsonData bytes to EnrichError instead of nil — correct fix for missing error detail propagation.
framework/streaming/accumulator.go Relaxes the nil guard from result == nil to result == nil && bifrostErr == nil, and extracts requestType from bifrostErr.ExtraFields.RequestType when result is absent — enables error-only accumulation correctly.
framework/streaming/responses.go Adds TerminalErrorChunkIndex logic to prevent chunk-index collision and enable proper dedup across plugin calls. Side-effect: MaxResponsesChunkIndex now points to the error chunk, so getLastResponsesChunkLocked returns the error chunk (nil token usage) rather than the last successful chunk.
framework/streaming/types.go Adds TerminalErrorChunkIndex int field to StreamAccumulator with clear documentation; initialized to -1 (unset sentinel) in both creation paths.
plugins/logging/main.go New Path A block finalizes the streaming accumulator before writing the error log entry, capturing pre-error stream metadata (model, latency, partial usage). Guards prevent overwriting already-populated RawRequest/RawResponse fields.
plugins/logging/operations.go Removes early return from applyStreamingOutputToEntry error branch so model, usage, and raw-response data accumulated before the error are now written to the log entry. Also fixes a logic bug where OutputTokensDetails was incorrectly nested inside the PromptTokensDetails guard.

Reviews (4): Last reviewed commit: "fix: wrong increments of chunk index" | Re-trigger Greptile

Comment thread framework/streaming/responses.go
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 the current code and only fix it if needed.

Inline comments:
In `@framework/streaming/responses.go`:
- Around line 901-913: Race condition: assigning chunk.ChunkIndex reads
accumulator.MaxResponsesChunkIndex while unlocked, allowing concurrent handlers
to compute the same index; fix by assigning the index while holding the
accumulator lock (i.e., increment MaxResponsesChunkIndex inside
getOrCreateStreamAccumulator's accumulator.mu critical section or here before
releasing the lock) so you atomically set chunk.ChunkIndex =
++accumulator.MaxResponsesChunkIndex (or move index assignment into
addResponsesStreamChunk which already holds the lock); also log sonic.Marshal
errors when serializing bifrostErr.ExtraFields.RawResponse to surface silent
failures.
🪄 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: 0df89b9c-e772-4866-959a-05457838c972

📥 Commits

Reviewing files that changed from the base of the PR and between 48b03ec and 0091d7a.

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

Comment thread framework/streaming/responses.go
@BearTS BearTS changed the base branch from v1.5.0 to graphite-base/2681 April 13, 2026 14:13
@BearTS BearTS force-pushed the graphite-base/2681 branch from 762f897 to a8cf690 Compare April 13, 2026 14:13
@BearTS BearTS force-pushed the 04-13-fix_capture_responses_streaming_api_error branch from 0091d7a to 9ea6845 Compare April 13, 2026 14:13
@BearTS BearTS changed the base branch from graphite-base/2681 to main April 13, 2026 14:13
@BearTS BearTS marked this pull request as draft April 13, 2026 14:19
@BearTS BearTS marked this pull request as ready for review April 13, 2026 14:26
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 the current code and only fix it if needed.

Inline comments:
In `@framework/streaming/responses.go`:
- Around line 909-913: The current logic in addResponsesStreamChunk
unconditionally increments accumulator.MaxResponsesChunkIndex and assigns
chunk.ChunkIndex, causing duplicate terminal-error callbacks to get new indexes
and bypass de-duplication; modify this by reserving and reusing a single
terminal-error index on the accumulator: under the same accumulator.mu lock used
around MaxResponsesChunkIndex, detect if the incoming chunk represents the
terminal error (use the chunk's terminal/error indicator) and if so, if the
accumulator already has a stored terminalErrorChunkIndex use that value for
chunk.ChunkIndex instead of incrementing; otherwise increment
MaxResponsesChunkIndex, assign it to chunk.ChunkIndex and also store it in
accumulator.terminalErrorChunkIndex for future reuse. Ensure all reads/writes to
MaxResponsesChunkIndex and terminalErrorChunkIndex happen inside the mutex to
avoid races.
🪄 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: 8c86133e-8dc9-463e-a4bb-f65ef87bcc7f

📥 Commits

Reviewing files that changed from the base of the PR and between 9ea6845 and 830c861.

📒 Files selected for processing (1)
  • framework/streaming/responses.go

Comment thread framework/streaming/responses.go
@BearTS BearTS force-pushed the 04-13-fix_capture_responses_streaming_api_error branch from a50cdde to a6b9fb5 Compare April 13, 2026 17:10
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.

♻️ Duplicate comments (1)
framework/streaming/responses.go (1)

901-904: ⚠️ Potential issue | 🟡 Minor

Preserve the error payload even when JSON marshaling fails.

RawResponse is interface{}. If this branch sees an unsupported value, or a plain string/[]byte, sonic.Marshal either drops the payload or JSON-quotes it, so the "raw" error chunk stops being raw. Falling back to a verbatim/string format here keeps the debugging data intact.

Suggested adjustment
 		if bifrostErr.ExtraFields.RawResponse != nil {
-			if rawBytes, marshalErr := sonic.Marshal(bifrostErr.ExtraFields.RawResponse); marshalErr == nil {
-				chunk.RawResponse = bifrost.Ptr(string(rawBytes))
-			}
+			switch raw := bifrostErr.ExtraFields.RawResponse.(type) {
+			case string:
+				chunk.RawResponse = bifrost.Ptr(raw)
+			case []byte:
+				chunk.RawResponse = bifrost.Ptr(string(raw))
+			default:
+				if rawBytes, marshalErr := sonic.Marshal(raw); marshalErr == nil {
+					chunk.RawResponse = bifrost.Ptr(string(rawBytes))
+				} else {
+					chunk.RawResponse = bifrost.Ptr(fmt.Sprintf("%v", raw))
+				}
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/streaming/responses.go` around lines 901 - 904, The RawResponse
currently gets JSON-marshaled with sonic.Marshal which can drop or quote the
original payload; update the branch that sets chunk.RawResponse (when
bifrostErr.ExtraFields.RawResponse != nil) to preserve the original payload on
marshal failure: attempt sonic.Marshal first, but if marshalErr != nil then
inspect bifrostErr.ExtraFields.RawResponse (type switch on []byte, string, or
other) and assign a verbatim representation—use the []byte as string, use string
as-is, and for other types use a safe fmt.Sprintf("%v", raw) fallback—so
chunk.RawResponse always contains the original/raw payload; reference the
symbols bifrostErr.ExtraFields.RawResponse, sonic.Marshal, and chunk.RawResponse
to locate where to implement this.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@framework/streaming/responses.go`:
- Around line 901-904: The RawResponse currently gets JSON-marshaled with
sonic.Marshal which can drop or quote the original payload; update the branch
that sets chunk.RawResponse (when bifrostErr.ExtraFields.RawResponse != nil) to
preserve the original payload on marshal failure: attempt sonic.Marshal first,
but if marshalErr != nil then inspect bifrostErr.ExtraFields.RawResponse (type
switch on []byte, string, or other) and assign a verbatim representation—use the
[]byte as string, use string as-is, and for other types use a safe
fmt.Sprintf("%v", raw) fallback—so chunk.RawResponse always contains the
original/raw payload; reference the symbols bifrostErr.ExtraFields.RawResponse,
sonic.Marshal, and chunk.RawResponse to locate where to implement this.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48ef73cf-0a83-434d-989c-163940c9ae9a

📥 Commits

Reviewing files that changed from the base of the PR and between 830c861 and a50cdde.

📒 Files selected for processing (3)
  • framework/streaming/accumulator.go
  • framework/streaming/responses.go
  • framework/streaming/types.go

Copy link
Copy Markdown
Contributor

akshaydeo commented Apr 15, 2026

Merge activity

  • Apr 15, 2:45 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 15, 2:45 PM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo akshaydeo merged commit 254e41e into main Apr 15, 2026
17 of 18 checks passed
@akshaydeo akshaydeo deleted the 04-13-fix_capture_responses_streaming_api_error branch April 15, 2026 14:45
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]: OpenAI Responses stream error branch drops raw response and returns generic provider stream error

3 participants