fix(proxy): decode bytes and pass-through SSE for Google-native streamGenerateContent (#27444) - #27456
Conversation
Greptile SummaryThis PR fixes corrupted SSE output for Google-native streaming (
Confidence Score: 4/5The change is narrowly scoped to a new The decode logic is sound and the ")` check that would miss CRLF-terminated SSE events and produce a spurious extra event boundary, though most clients tolerate this gracefully. No automated tests cover the new path. litellm/proxy/proxy_server.py — specifically the
|
| Filename | Overview |
|---|---|
| litellm/proxy/proxy_server.py | Adds a bytes branch in async_data_generator to decode raw SSE bytes from Google-native streaming iterators and pass pre-formatted SSE through unchanged; logic is correct for the happy path but the endswith("\n\n") terminator check doesn't cover CRLF-terminated events. |
Reviews (2): Last reviewed commit: "chore(lint): silence PLR0915 on async_da..." | Re-trigger Greptile
| if chunk.startswith(("data:", "event:", ":")): | ||
| try: | ||
| yield chunk if chunk.endswith("\n\n") else chunk + "\n\n" | ||
| except Exception as e: | ||
| yield f"data: {str(e)}\n\n" | ||
| continue |
There was a problem hiding this comment.
Partial chunk may corrupt SSE framing for large payloads
httpx.aiter_bytes() splits by TCP/buffer boundaries, not by SSE event boundaries. If a large Gemini SSE event is split across two aiter_bytes() calls, the first half (e.g. data: {"candidates":... — incomplete JSON) starts with data: and is yielded immediately with \n\n appended, making the client parse it as a complete but malformed event. The continuation bytes (e.g. ...}\n\n) don't start with an SSE prefix, fall through to yield f"data: {chunk}\n\n", and are double-wrapped. For typical small Gemini payloads this won't trigger, but it can occur for responses with long text fields.
| elif isinstance(chunk, bytes): | ||
| # Some upstream streaming iterators (e.g. AsyncGoogleGenAIGenerateContentStreamingIterator | ||
| # for /v1beta/.../streamGenerateContent) yield raw SSE bytes from Gemini. | ||
| # Decode to str so the f-string below does not emit a Python b'...' literal, | ||
| # and pass already-formatted SSE through unchanged to avoid double "data:" prefix. | ||
| chunk = chunk.decode("utf-8", errors="replace") | ||
| if chunk.startswith(("data:", "event:", ":")): | ||
| try: | ||
| yield chunk if chunk.endswith("\n\n") else chunk + "\n\n" | ||
| except Exception as e: | ||
| yield f"data: {str(e)}\n\n" | ||
| continue |
There was a problem hiding this comment.
Test plan items unchecked — fix is unverified
The PR description lists three test plan items (curl reproduction, OpenAI-format regression, existing proxy streaming tests) and all remain unchecked. Per the team's custom standard, a PR claiming to fix a reported issue should include evidence that the issue is resolved, such as passing tests or concrete verification steps. No automated tests for the bytes decode path appear to have been added.
Rule Used: What: Ensure that any PR claiming to fix an issue ... (source)
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Status update on this? @Anai-Guo |
|
Hi @shekohex — PR is ready from my side: tests added, Codecov is green, and Greptile already left a positive automated review. Just waiting on a maintainer pass. Happy to address any feedback once someone takes a look. |
|
@ishaan-berri @Sameerlite @yuneng-berri @mateo-berri @krrish-berri-2 could this get a maintainer pass soon? This is a real production regression for Google-native streaming clients. After #26914, I verified this still affects This PR looks scoped to the right fix: decode byte chunks and pass through already-formatted SSE. Checks are green; it seems to be waiting on maintainer review / conflict resolution. Could you please prioritize merging or releasing this fix? |
|
@greptileai re review |
|
@Perfecto23 thanks for the PR, can you resolve the conflict? |
|
@Sameerlite thanks, but I'm not the PR author. I'm just an affected user following this regression and trying to help get the fix landed. @Anai-Guo could you take a look at the conflict resolution when you get a chance? It looks like that is currently blocking maintainer review / merge. |
|
#28213 |
|
@Sameerlite thanks |
Summary
Fixes #27444 —
/v1beta/models/{model}:streamGenerateContent?alt=sseproduced corrupted output that no JSON parser (and no@google/genaiclient) can read:The bug is in
async_data_generatorinlitellm/proxy/proxy_server.py. The Google-native streaming iterator (AsyncGoogleGenAIGenerateContentStreamingIterator) yields raw SSE bytes fromhttpx.aiter_bytes(). Those bytes flow intoasync_data_generator, whereyield f\"data: {chunk}\n\n\"callsstr(bytes_obj), which renders the Pythonb'...'repr instead of the decoded string. The original payload is also already SSE-formatted (starts withdata:), so wrapping it again would double-prefix.OpenAI-format (
/v1/chat/completions) is unaffected because chunks there areBaseModelinstances and take the JSON-serializing branch.Fix
Add a
bytesbranch before the existingstr.startswith(\"data: \")(error-detection) branch:data:,event:, or:comment line), pass it through unchanged — adding a trailing\n\nonly if the upstream chunk didn't include one.data: {chunk}\n\nwrapping path.The existing OpenAI error-detection branch (
elif isinstance(chunk, str) and chunk.startswith(\"data: \")) is preserved unchanged.Test plan
:streamGenerateContent?alt=sse) now returns clean SSE;@google/genaiSDK parses it without error./v1/chat/completions) still works (BaseModel branch unchanged).🤖 Generated with Claude Code