Skip to content

[fix]: openai provider - add usage to completed event in responses to chat completions fallback - #3519

Merged
akshaydeo merged 4 commits into
maximhq:devfrom
stackblitz:fix/responses-to-chat-fallback-usage-streaming
May 15, 2026
Merged

[fix]: openai provider - add usage to completed event in responses to chat completions fallback#3519
akshaydeo merged 4 commits into
maximhq:devfrom
stackblitz:fix/responses-to-chat-fallback-usage-streaming

Conversation

@kevinpdev

Copy link
Copy Markdown
Contributor

Summary

When a responses streaming request to bifrost falls back to chat completions, bifrost streams from the upstream provider (OpenAI, Azure Openai, etc.) and translates each chunk into a responses event for the caller.

While many providers emit usage data in the same chunk as the finish_reason, some providers emit a finish_reason first and then usage data one chunk later. Bifrost currenly forwards response.completed/response.incomplete as soon as finish_reason chunk arrives. This makes the caller's terminal event have usage unset if the usage chunk comes after.

This applies to azure foundry and also openai provider (when a model supports chat completions but not responses). This could also fix the bug in other providers if they have the same multiple-chunk behavior.

Upstream chunk order:
...content deltas...
{ ..., "finish_reason": "stop", "usage": null } <- we sent terminal here (bug)
{ ..., "usage": {...} } <- arrives too late

Fix: accumulate usage from every chunk, hold the terminal event until the upstream stream ends, then attach usage before sending. Native chat-completion and Responses streaming paths are unchanged.

The fix is done on openai provider because this will fix any provider that relies on it, including any custom providers built on top of it.

Changes

  • core/providers/openai/openai.go: in the Responses->Chat Completions fallback path, defer the terminal
    response.completed/response.incomplete event until the upstream stream ends and attach accumulated usage
    before sending.
  • core/changelog.md: changelog entry.

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

The easiest reproduction for this is using an OSS model on azure foundry, and making your own custom provider for it. To reproduce you need to be on top of #3505 or you will run into other errors first.

Below is an example of using kimi-k2.6 on azure foundry. Azure foundry sends usage in a separate chunk after the response_completed = true chunk.

Request:

curl -sN http://localhost:8080/v1/responses \
    -H "content-type: application/json" \
    -d '{"model":"azure-foundry/kimi-k2.6","input":"Count from 1 to 10","stream":true}'

Provider:
image

Before (see usage field):

{
    "type": "response.completed",
    "sequence_number": 59,
    "response": {
        "id": "c68b48991033427a941b65a22d821120",
        "object": "",
        "created_at": 1778822303,
        "completed_at": null,
        "error": null,
        "incomplete_details": null,
        "instructions": null,
        "max_output_tokens": null,
        "max_tool_calls": null,
        "model": "Kimi-K2.6",
        "output": [
            {
                "id": "msg_c68b48991033427a941b65a22d821120_item_0",
                "type": "message",
                "status": "completed",
                "role": "assistant",
                "content": [
                    {
                        "type": "output_text",
                        "text": "1, 2, 3, 4, 5, 6, 7, 8, 9, 10.",
                        "annotations": [],
                        "logprobs": []
                    }
                ]
            }
        ],
        "previous_response_id": null,
        "prompt_cache_key": null,
        "reasoning": null,
        "safety_identifier": null,
        "service_tier": null,
        "status": "completed",
        "tools": null,
        "usage": null,
        "extra_fields": {
            "request_type": "",
            "latency": 0,
            "chunk_index": 0
        }
    },
    "item": null,
    "logprobs": null,
    "extra_fields": {
        "request_type": "responses_stream",
        "provider": "azure-foundry",
        "original_model_requested": "kimi-k2.6",
        "resolved_model_used": "Kimi-K2.6",
        "latency": 612,
        "chunk_index": 59
    }
}

After (see usage field):

{
    "type": "response.completed",
    "sequence_number": 78,
    "response": {
        "id": "59ec3c4a66844ff8a86a0a18d595ddbd",
        "object": "",
        "created_at": 1778822187,
        "completed_at": null,
        "error": null,
        "incomplete_details": null,
        "instructions": null,
        "max_output_tokens": null,
        "max_tool_calls": null,
        "model": "Kimi-K2.6",
        "output": [
            {
                "id": "msg_59ec3c4a66844ff8a86a0a18d595ddbd_item_0",
                "type": "message",
                "status": "completed",
                "role": "assistant",
                "content": [
                    {
                        "type": "output_text",
                        "text": "1, 2, 3, 4, 5, 6, 7, 8, 9, 10",
                        "annotations": [],
                        "logprobs": []
                    }
                ]
            }
        ],
        "previous_response_id": null,
        "prompt_cache_key": null,
        "reasoning": null,
        "safety_identifier": null,
        "service_tier": null,
        "status": "completed",
        "tools": null,
        "usage": {
            "input_tokens": 15,
            "input_tokens_details": {
                "cached_read_tokens": 2,
                "cached_write_tokens": 0,
                "cached_tokens": 2
            },
            "output_tokens": 196,
            "output_tokens_details": null,
            "total_tokens": 211
        },
        "extra_fields": {
            "request_type": "",
            "latency": 0,
            "chunk_index": 0
        }
    },
    "item": null,
    "logprobs": null,
    "extra_fields": {
        "request_type": "responses_stream",
        "provider": "azure-foundry",
        "original_model_requested": "kimi-k2.6",
        "resolved_model_used": "Kimi-K2.6",
        "latency": 154,
        "chunk_index": 78
    }
}

Screenshots/Recordings

See above

Breaking changes

  • Yes

  • No

  • The change only affects the Responses→Chat Completions fallback streaming path.

  • Callers previously received a terminal event with usage: null; now they receive the same terminal event with usage populated (when the upstream provides it).

Related issues

Security considerations

No security considerations

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

coderabbitai Bot commented May 15, 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0c8ed1f-fd2a-4f67-adca-ca6d44e834ec

📥 Commits

Reviewing files that changed from the base of the PR and between d7faa37 and 2c7c4f6.

📒 Files selected for processing (2)
  • core/changelog.md
  • core/providers/openai/openai.go
✅ Files skipped from review due to trivial changes (1)
  • core/changelog.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/providers/openai/openai.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a per-request stream idle timeout for all streaming endpoints for consistent idle-timeout behavior.
  • Bug Fixes

    • Fixed streaming chat-completion fallback to accumulate and include usage (including cost) in the final response event, and reliably set final latency and terminal details.

Walkthrough

Adds per-request stream idle-timeout propagation to OpenAI streaming handlers and buffers the final Responses terminal event in the Responses→Chat fallback to aggregate and attach usage before emitting the final event.

Changes

OpenAI streaming and fallback updates

Layer / File(s) Summary
Stream idle-timeout wiring
core/providers/openai/openai.go
Forward provider.networkConfig.StreamIdleTimeoutInSeconds into per-request streaming handlers and call providerUtils.SetStreamIdleTimeoutIfEmpty(...) in shared HandleOpenAI*Streaming entrypoints (text, chat, responses, speech, transcription, image gen/edit, passthrough).
Responses→Chat fallback: deferred final event & usage aggregation
core/providers/openai/openai.go
Introduce pendingFinalEvent to buffer the final completed/incomplete Responses event, accumulate usage (prompt/completion/total tokens, token details, and usage.Cost) across chunks, and emit the finalized terminal event after the stream read loop with filled usage, optional raw request/response, latency, and stream end marker.
Changelog
core/changelog.md
Add changelog entry noting that usage data is added to the completed event for chat completion fallbacks.

Sequence Diagram

sequenceDiagram
  participant Client
  participant HandleOpenAIResponsesStreaming
  participant SSEReader
  participant FallbackAggregator
  participant StreamEmitter

  Client ->> HandleOpenAIResponsesStreaming: open stream (includes streamIdleTimeoutInSeconds)
  HandleOpenAIResponsesStreaming ->> SSEReader: start SSE read (timeout set)
  SSEReader ->> FallbackAggregator: chunked Responses events (partial usage)
  FallbackAggregator ->> FallbackAggregator: aggregate usage and buffer terminal event
  SSEReader ->> HandleOpenAIResponsesStreaming: end of stream
  HandleOpenAIResponsesStreaming ->> FallbackAggregator: finalize usage and attach metadata
  FallbackAggregator ->> StreamEmitter: emit finalized completed/incomplete event (with usage)
  StreamEmitter ->> Client: deliver terminal event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • maximhq/bifrost#3495: Related OpenAI streaming timeout wiring and handler signature changes.
  • maximhq/bifrost#3505: Related Responses→Chat fallback streaming modifications affecting terminal event handling.

Suggested reviewers

  • akshaydeo
  • danpiths

Poem

🐰 I buffered each chunk like a stashed little treat,
Counting tokens and cost in a neat little heap.
When the stream finally closed,
I filled usage, then posed—
A completed event served, and I hopped off to sleep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: adding usage data to the completed event in the Responses-to-Chat-Completions fallback path for the OpenAI provider.
Description check ✅ Passed The description is comprehensive and addresses all key template sections: clear problem summary, detailed technical changes, type of change marked, affected areas identified, reproduction steps with examples, before/after evidence, breaking changes clearly stated as no, and checklist items marked complete.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 15, 2026 05:54
@greptile-apps

greptile-apps Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

Safe to merge for the common case; a narrow regression exists where a non-EOF read error after the terminal chunk causes the terminal event to be dropped entirely.

The happy path (normal EOF, usage in trailing chunk) is correctly handled and the usageSeen guard prevents phantom zero-usage objects. The one concrete regression is in the non-EOF read-error path: when that error fires after pendingFinalEvent is set but before EOF, the terminal event is silently discarded — a behavior change that wasn't possible before because the old code sent and returned at the terminal event.

core/providers/openai/openai.go — the non-EOF read-error return at line 1140 should flush pendingFinalEvent before returning.

Important Files Changed

Filename Overview
core/providers/openai/openai.go Defers terminal responses event until stream EOF so usage from a separate trailing chunk can be attached; introduces a narrow regression where a non-EOF read error after the terminal chunk drops the pending event entirely.
core/changelog.md Changelog entry added for this bug fix.

Comments Outside Diff (1)

  1. core/providers/openai/openai.go, line 1132-1140 (link)

    P1 Pending terminal event dropped on non-EOF read error

    When the non-EOF read-error path fires after pendingFinalEvent has already been set (i.e., the terminal chunk was seen but the usage-only chunk read fails), the function returns early at line 1140 and pendingFinalEvent is never sent. Before this PR, the terminal event was sent synchronously then the function returned, making post-terminal read errors structurally impossible. Now that reading continues past the terminal event, a corrupted or truncated usage chunk silently discards the successfully-received terminal event, and the caller only sees the error response instead.

Reviews (4): Last reviewed commit: "Merge branch 'dev' into fix/responses-to..." | Re-trigger Greptile

Comment thread core/providers/openai/openai.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/openai/openai.go`:
- Around line 1122-1123: The final aggregated usage should only be attached when
a usage chunk was actually observed: add a boolean flag (e.g., sawUsageChunk) in
the streaming/fallback flow where usage is accumulated (the variable usage of
type *schemas.BifrostLLMUsage) and set it true whenever a usage chunk arrives;
when building pendingFinalEvent or assigning usage to any
schemas.BifrostResponsesStreamResponse, guard the assignment with sawUsageChunk
so you only call usage.ToResponsesResponseUsage() and set the usage field if
sawUsageChunk is true (leave it nil otherwise). Update all similar blocks that
assign usage in the fallback stream handling (mentions: pendingFinalEvent logic
and the other spots around the BifrostLLMUsage accumulation and response
construction) to use this flag.
🪄 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: 73ea8ded-d1f4-40a7-8886-03aa14b4baf4

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce0993 and 9581cb6.

📒 Files selected for processing (2)
  • core/changelog.md
  • core/providers/openai/openai.go

Comment thread core/providers/openai/openai.go
@akshaydeo
akshaydeo requested a review from a team as a code owner May 15, 2026 05:59
@CLAassistant

CLAassistant commented May 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

kevinpdev added 2 commits May 15, 2026 02:13
… chat completions fallback

When a Responses streaming request falls back to chat completions, Bifrost streams from the upstream provider (OpenAI, Groq, Mistral, etc.) and translates each chunk into a Responses event for the caller.

The provider emits finish_reason first and usage one chunk later, but Bifrost was forwarding response.completed/response.incomplete as soon as finish_reason arrived — so the caller's terminal event had usage unset.

Upstream chunk order:
  ...content deltas...
  { ..., "finish_reason": "stop", "usage": null }   <- we sent terminal here (bug)
  { ..., "usage": {...} }                           <- arrives too late

Fix: accumulate usage from every chunk, hold the terminal event until the upstream stream ends, then attach usage before sending. Native chat-completion and Responses streaming paths are unchanged.

Modified files:
- core/providers/openai/openai.go: attach usage information to completed event

Update changelogs:
- core/changelog.md:
   [fix]: openai provider - add usage to completed event in responses to chat completions fallback [@kevinpdev](https://github.com/kevinpdev)
…s usage chunks

  Follow-up to 9581cb6. If a provider in the fallback path never sends usage
  data, the previous fix would still attach a zero-valued usage object to the
  final event instead of leaving it null. Only attach usage when we actually saw
  a usage chunk from upstream.

  Affected packages:
  - core/providers/openai: gate fallback terminal usage attach on observed usage chunk

  Update changelogs:
  - core/changelog.md:
     [fix]: openai provider - preserve nil usage when fallback stream omits usage chunks
  [@kevinpdev](https://github.com/kevinpdev)
@kevinpdev
kevinpdev force-pushed the fix/responses-to-chat-fallback-usage-streaming branch from e214102 to 194b3c5 Compare May 15, 2026 06:13

@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/openai/openai.go`:
- Around line 1350-1352: When usageSeen is true always overwrite the buffered
terminal event usage with the accumulated usage instead of only when
pendingFinalEvent.Response.Usage == nil; in the block referencing usageSeen and
pendingFinalEvent.Response (the symbols usageSeen, pendingFinalEvent.Response,
and usage.ToResponsesResponseUsage()), remove the nil-guard and assign
pendingFinalEvent.Response.Usage = usage.ToResponsesResponseUsage()
unconditionally so the terminal response.completed/response.incomplete reflects
the aggregated usage (including cost/token fields) every time.
🪄 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: d1fcf89f-df31-44af-a4c7-7fa6182fcedb

📥 Commits

Reviewing files that changed from the base of the PR and between 9581cb6 and e214102.

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

Comment thread core/providers/openai/openai.go Outdated
… fallback

Drop the nil guard so the merged usage accumulator always overwrites the terminal event's usage, even if a partial usage struct was already attached.

Modified files:
- core/providers/openai/openai.go: drop nil guard on terminal usage attach
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 15, 2026
@akshaydeo

Copy link
Copy Markdown
Contributor

❤️ for the PR @kevinpdev

@d3lm
d3lm dismissed stale reviews from Pratham-Mishra04 and coderabbitai[bot] via 2c7c4f6 May 15, 2026 13:47
@akshaydeo
akshaydeo merged commit 8b3a47f into maximhq:dev May 15, 2026
4 checks passed
@d3lm
d3lm deleted the fix/responses-to-chat-fallback-usage-streaming branch May 15, 2026 17:22
akshaydeo pushed a commit that referenced this pull request May 15, 2026
… chat completions fallback (#3519)

* [fix]: openai provider - add usage to completed event in responses to chat completions fallback

When a Responses streaming request falls back to chat completions, Bifrost streams from the upstream provider (OpenAI, Groq, Mistral, etc.) and translates each chunk into a Responses event for the caller.

The provider emits finish_reason first and usage one chunk later, but Bifrost was forwarding response.completed/response.incomplete as soon as finish_reason arrived — so the caller's terminal event had usage unset.

Upstream chunk order:
  ...content deltas...
  { ..., "finish_reason": "stop", "usage": null }   <- we sent terminal here (bug)
  { ..., "usage": {...} }                           <- arrives too late

Fix: accumulate usage from every chunk, hold the terminal event until the upstream stream ends, then attach usage before sending. Native chat-completion and Responses streaming paths are unchanged.

Modified files:
- core/providers/openai/openai.go: attach usage information to completed event

Update changelogs:
- core/changelog.md:
   [fix]: openai provider - add usage to completed event in responses to chat completions fallback [@kevinpdev](https://github.com/kevinpdev)

* [fix]: openai provider - preserve nil usage when fallback stream omits usage chunks

  Follow-up to 9581cb6. If a provider in the fallback path never sends usage
  data, the previous fix would still attach a zero-valued usage object to the
  final event instead of leaving it null. Only attach usage when we actually saw
  a usage chunk from upstream.

  Affected packages:
  - core/providers/openai: gate fallback terminal usage attach on observed usage chunk

  Update changelogs:
  - core/changelog.md:
     [fix]: openai provider - preserve nil usage when fallback stream omits usage chunks
  [@kevinpdev](https://github.com/kevinpdev)

* [fix]: openai provider - always use merged usage on terminal event in fallback

Drop the nil guard so the merged usage accumulator always overwrites the terminal event's usage, even if a partial usage struct was already attached.

Modified files:
- core/providers/openai/openai.go: drop nil guard on terminal usage attach

---------

Co-authored-by: Dominic Elm <elmdominic@gmx.net>
akshaydeo pushed a commit that referenced this pull request May 20, 2026
… chat completions fallback (#3519)

* [fix]: openai provider - add usage to completed event in responses to chat completions fallback

When a Responses streaming request falls back to chat completions, Bifrost streams from the upstream provider (OpenAI, Groq, Mistral, etc.) and translates each chunk into a Responses event for the caller.

The provider emits finish_reason first and usage one chunk later, but Bifrost was forwarding response.completed/response.incomplete as soon as finish_reason arrived — so the caller's terminal event had usage unset.

Upstream chunk order:
  ...content deltas...
  { ..., "finish_reason": "stop", "usage": null }   <- we sent terminal here (bug)
  { ..., "usage": {...} }                           <- arrives too late

Fix: accumulate usage from every chunk, hold the terminal event until the upstream stream ends, then attach usage before sending. Native chat-completion and Responses streaming paths are unchanged.

Modified files:
- core/providers/openai/openai.go: attach usage information to completed event

Update changelogs:
- core/changelog.md:
   [fix]: openai provider - add usage to completed event in responses to chat completions fallback [@kevinpdev](https://github.com/kevinpdev)

* [fix]: openai provider - preserve nil usage when fallback stream omits usage chunks

  Follow-up to 9581cb6. If a provider in the fallback path never sends usage
  data, the previous fix would still attach a zero-valued usage object to the
  final event instead of leaving it null. Only attach usage when we actually saw
  a usage chunk from upstream.

  Affected packages:
  - core/providers/openai: gate fallback terminal usage attach on observed usage chunk

  Update changelogs:
  - core/changelog.md:
     [fix]: openai provider - preserve nil usage when fallback stream omits usage chunks
  [@kevinpdev](https://github.com/kevinpdev)

* [fix]: openai provider - always use merged usage on terminal event in fallback

Drop the nil guard so the merged usage accumulator always overwrites the terminal event's usage, even if a partial usage struct was already attached.

Modified files:
- core/providers/openai/openai.go: drop nil guard on terminal usage attach

---------

Co-authored-by: Dominic Elm <elmdominic@gmx.net>
@akshaydeo akshaydeo mentioned this pull request May 20, 2026
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.

5 participants