Skip to content

fix(openai): use second-to-last SSE as fallback when last frame lacks valid usage#5788

Closed
duyiliu wants to merge 1 commit into
QuantumNous:mainfrom
duyiliu:fix/openai-stream-usage-fallback
Closed

fix(openai): use second-to-last SSE as fallback when last frame lacks valid usage#5788
duyiliu wants to merge 1 commit into
QuantumNous:mainfrom
duyiliu:fix/openai-stream-usage-fallback

Conversation

@duyiliu

@duyiliu duyiliu commented Jun 28, 2026

Copy link
Copy Markdown

Summary

Some upstreams (e.g. opencode-zen for minimax-m3) emit a malformed trailing SSE
frame such as {"choices":[],"cost":"0"} followed by TCP EOF without a data: [DONE]
sentinel. The actual usage (including cached_tokens) is carried in the
second-to-last frame.

relay/channel/openai/relay-openai.go already has a second-to-last fallback, but
it is gated on isAudioModel, so it is skipped for non-audio models. In that
case handleLastResponse cannot extract a valid usage from the last frame,
containStreamUsage stays false, and the code falls back to
service.ResponseText2Usage(...) which performs local token counting and never
fills PromptTokensDetails.CachedTokens. Result: cache_tokens=0,
local_count_tokens=true, end_reason="eof" in the log — a silent billing
inaccuracy for any user of such an upstream.

Reproduction

$ curl -N -X POST http://localhost:3002/v1/chat/completions \
    -H "Authorization: Bearer $TOKEN" \
    -d '{"model":"minimax-m3","stream":true,"messages":[{"role":"user","content":"hi"}]}'

Upstream emits (3 content chunks + 1 usage chunk + 1 malformed {"choices":[],"cost":"0"}

  • TCP EOF, no [DONE]). Before this fix the log line shows:
prompt_tokens=14 completion_tokens=94 cache_tokens=0 local_count_tokens=true
stream_status.end_reason="eof"

After this fix the same upstream response yields:

prompt_tokens=183 completion_tokens=193 cache_tokens=114

(Large prompts also work, e.g. prompt_tokens=160553 cache_tokens=159730.)

Change

relay/channel/openai/relay-openai.go (OaiStreamHandler):

  1. Always save secondLastStreamData (previously only when isAudioModel).
  2. Move the second-to-last usage extraction to after handleLastResponse.
  3. Gate it on !containStreamUsage instead of isAudioModel.

The fallback is a pure pass-through: it only fires when the last frame
ValidUsage check failed, so behavior for normal streams (where the last frame
carries a valid usage and containStreamUsage is already true) is
unchanged.

Compatibility

  • Normal models whose last frame is valid: containStreamUsage=true after
    handleLastResponse → fallback is skipped → identical to main.
  • Audio models (the original use case): same effective behavior — the original
    code already preferred second-to-last; with this change, the last frame is
    preferred when it has valid usage, otherwise the second-to-last is used. The
    resulting usage is the same valid one; the only difference is which frame
    is preferred when both are valid, which is benign.
  • Malformed streams (this fix's target): cache tokens are now extracted
    correctly instead of being lost to local counting.

Test

Local smoke test on the patched binary (default token, channel 102,
opencode.ai/zen/go for minimax-m3):

model stream local prompt completion cache eof
minimax-m3 yes upstream 183 193 114 yes
minimax-m3 yes upstream 159832 168 157464 yes
minimax-m3 yes upstream 160553 384 159730 yes
minimax-m3 no upstream 186 134 114 no
deepseek-v4-flash yes upstream 93 989 0 no (unchanged)

No errors in service-3002-stderr.log during the run.

Summary by CodeRabbit

  • Bug Fixes
    • Improved streaming response handling so usage data can be recovered when the final SSE frame is malformed or lacks valid usage information.
    • Updated logging to clearly reflect when and why fallback usage extraction occurs.
    • Made stream finalization more reliable by handling the last response earlier when required.

@coderabbitai

coderabbitai Bot commented Jun 28, 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: 47ec7336-cffe-484d-94ca-ea24d0112558

📥 Commits

Reviewing files that changed from the base of the PR and between b929bbd and d19f25c.

📒 Files selected for processing (1)
  • relay/channel/openai/relay-openai.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/channel/openai/relay-openai.go

Walkthrough

OaiStreamHandler now records the penultimate SSE frame for all models, calls handleLastResponse earlier, and falls back to parsing usage from that penultimate frame when the final frame lacks valid usage. The debug log text is also updated.

Changes

SSE penultimate-frame usage fallback

Layer / File(s) Summary
secondLastStreamData tracking and usage fallback logic
relay/channel/openai/relay-openai.go
Adds unconditional secondLastStreamData tracking, introduces shouldSendLastResp, moves handleLastResponse earlier, and parses usage from secondLastStreamData when the final SSE frame does not contain valid usage; updates the fallback log wording.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

A bunny hops through SSE streams so fine,
The second-last nibble now does the right line.
If the last one comes limping with usage askew,
The backup frame whispers, “I’ve got it for you.” 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: using the second-to-last SSE frame when the last one lacks valid usage.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 2

🤖 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 `@relay/channel/openai/relay-openai.go`:
- Around line 150-152: The fallback in relay-openai.go can forward a penultimate
usage frame even when usage was not requested, because the normal last-frame
suppression via info.ShouldIncludeUsage is not applied here. Update the fallback
around secondLastStreamData and the stream usage handling logic to mirror the
same ShouldIncludeUsage check used for lastStreamData before emitting buffered
usage metadata, so penultimate usage-only frames are only forwarded when
requested.
- Around line 145-148: The error handling in openai relay currently logs the raw
lastStreamData payload when handleLastResponse fails, which can leak sensitive
streamed content. Update the logging in relayOpenAI.go around handleLastResponse
to remove the verbatim payload and instead log safe metadata such as the payload
length, responseId/model context, and the error message. Keep the fix localized
to the error path in the response handling flow.
🪄 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: 5e9ac7fa-a97b-4f24-8d6d-c101f2a2b1ac

📥 Commits

Reviewing files that changed from the base of the PR and between 2d5a041 and b929bbd.

📒 Files selected for processing (1)
  • relay/channel/openai/relay-openai.go

Comment on lines +145 to +148
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
&containStreamUsage, info, &shouldSendLastResp); err != nil {
logger.LogError(c, fmt.Sprintf("error handling last response: %s, lastStreamData: [%s]", err.Error(), lastStreamData))
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw streamed payloads on parse errors.

lastStreamData can contain model output/tool data; logging it verbatim on malformed upstream frames can leak sensitive content. Log metadata such as length instead.

Proposed fix
-		logger.LogError(c, fmt.Sprintf("error handling last response: %s, lastStreamData: [%s]", err.Error(), lastStreamData))
+		logger.LogError(c, fmt.Sprintf("error handling last response: %s, lastStreamDataLen: %d", err.Error(), len(lastStreamData)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
&containStreamUsage, info, &shouldSendLastResp); err != nil {
logger.LogError(c, fmt.Sprintf("error handling last response: %s, lastStreamData: [%s]", err.Error(), lastStreamData))
}
if err := handleLastResponse(lastStreamData, &responseId, &createAt, &systemFingerprint, &model, &usage,
&containStreamUsage, info, &shouldSendLastResp); err != nil {
logger.LogError(c, fmt.Sprintf("error handling last response: %s, lastStreamDataLen: %d", err.Error(), len(lastStreamData)))
}
🤖 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 `@relay/channel/openai/relay-openai.go` around lines 145 - 148, The error
handling in openai relay currently logs the raw lastStreamData payload when
handleLastResponse fails, which can leak sensitive streamed content. Update the
logging in relayOpenAI.go around handleLastResponse to remove the verbatim
payload and instead log safe metadata such as the payload length,
responseId/model context, and the error message. Keep the fix localized to the
error path in the response handling flow.

Comment thread relay/channel/openai/relay-openai.go Outdated
… valid usage

Some upstreams emit a malformed trailing frame (e.g. {"choices":[],"cost":"0"})
followed by TCP EOF without [DONE], causing the last frame to fail ValidUsage.
Previously the audio-only second-to-last fallback was skipped for non-audio models,
triggering ResponseText2Usage local counting and dropping cache_tokens.

This change moves the second-to-last usage extraction after handleLastResponse and
gates it on !containStreamUsage, so it acts as a pure fallback for malformed
streams without altering behavior for normal streams (where the last frame has
valid usage and containStreamUsage is already true).
@duyiliu duyiliu force-pushed the fix/openai-stream-usage-fallback branch from b929bbd to d19f25c Compare June 28, 2026 07:18

@duyiliu duyiliu left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Addressed inline below.

Comment 1 (line 152, ShouldIncludeUsage): ✅ Adopted in commit d19f25c9.
Added && info.ShouldIncludeUsage to the fallback condition so the penultimate-frame usage extraction mirrors the suppression behavior in handleLastResponse. Updated the comment to document the intent. Note: the previously-existing audio-only path did not gate on ShouldIncludeUsage either, so this strictly tightens behavior (clients that did not opt in via stream_options.include_usage will no longer have usage metadata "unlocked" by a malformed last frame). Final usage is still recorded for billing via record consume log regardless, which is the actual fix target.

Comment 2 (line 148, raw lastStreamData in error log): ⚠️ Pre-existing, intentionally out of scope.
This log line (logger.LogError(c, fmt.Sprintf("... lastStreamData: [%s]", ..., lastStreamData))) exists verbatim on the original main branch at the same location — it is not introduced by this PR. Agree it's a privacy concern, but conflating it with the cache fix would expand the PR's scope and make review harder. Happy to send it as a follow-up PR (or amend here if maintainer prefers a single commit).

Diff of the amend is +1 net line (the new && info.ShouldIncludeUsage plus 2 lines of comment), same relay/channel/openai/relay-openai.go file.

@duyiliu

duyiliu commented Jun 28, 2026

Copy link
Copy Markdown
Author

Closing this PR — going back to clean upstream 2d5a041 per local decision. The cache fallback fix (second-to-last SSE) was useful for malformed upstream streams where the last frame lacks valid usage, but the maintainer should review independently before adopting.

Leaving the diff in place for reference. Thanks @coderabbitai for the review.

@duyiliu duyiliu closed this Jun 28, 2026
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@duyiliu Thanks for the update — that makes sense. Reverting to the clean upstream baseline here sounds reasonable, and keeping this diff around as reference for the malformed trailing SSE case is still useful context if the maintainer decides to revisit it later.

Appreciate the note, and thanks for the thoughtful iteration on this.

🐇✨

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.

1 participant