fix(responses): preserve streaming output in Langfuse - #36362
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
623e069 to
399e9fb
Compare
Greptile SummaryThe PR reconstructs empty terminal Responses output from preceding streaming events and unwraps completed-event payloads for Langfuse OTEL serialization.
Confidence Score: 4/5The PR needs a compatibility fix before merging because a valid type-changing streaming hook can now terminate the stream. The recovery behavior works for normal Pydantic stream events, but both iterator paths unconditionally serialize the post-hook result even though the hook contract permits arbitrary replacement values. Files Needing Attention: litellm/responses/streaming_iterator.py
|
| Filename | Overview |
|---|---|
| litellm/responses/streaming_iterator.py | Adds terminal-output recovery, but the new unconditional model_dump call breaks hooks that validly return non-Pydantic replacement chunks. |
| litellm/integrations/langfuse/langfuse_otel.py | Correctly unwraps a completed Responses event while retaining fallback behavior for direct response objects. |
| tests/test_litellm/responses/test_streaming_iterator.py | Covers sync item recovery and async text-only recovery, but does not exercise a type-changing streaming hook. |
| tests/test_litellm/integrations/test_langfuse_otel.py | Adds focused coverage proving nested completed-event output is serialized for Langfuse. |
Reviews (1): Last reviewed commit: "fix(responses): preserve streaming outpu..." | Re-trigger Greptile
| def _accumulate_streamed_output(self, chunk: ResponsesAPIStreamingResponse) -> None: | ||
| chunk_data: Final = chunk.model_dump() | ||
| event_type: Final = chunk_data.get("type") |
There was a problem hiding this comment.
Hook replacements break accumulation
When a streaming deployment hook returns a replacement such as a plain dictionary, _accumulate_streamed_output unconditionally calls model_dump() on it, causing an AttributeError that terminates the stream and invokes failure handling instead of returning the modified chunk.
Knowledge Base Used: Logging & Observability Integrations
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a developer streams a Responses API call successfully, but Langfuse records no generation output
"stream": trueresponse.completedeventAfter: the same streamed call records its assistant output in Langfuse
"stream": trueresponse.completedeventRelevant issues
Fixes #36361
Related to #25429 and #31332
Linear ticket
Pre-Submission checklist
Screenshots / Proof of Fix
Live proof is pending while this PR remains a draft. The host used for the original reproduction is currently unreachable from the test runner, so no after screenshot is claimed here
Type
Bug Fix
Changes
The Responses streaming iterator now retains post-hook
output_item.doneandoutput_text.donedata. When a provider sends an empty terminal output, the iterator rebuilds it with the existing shared SSE recovery helpers before success callbacks runThe Langfuse OTEL integration now reads output from the nested Responses object when the callback result is a
response.completedeventRegression coverage exercises sync output-item SSE recovery, async text-only SSE recovery, and the Langfuse OTEL wrapper. Both affected test files pass with 40 tests
Ruff checks, the strict-rule budget, and the type-discipline budget pass. The complete basedpyright gate cannot provision locally because the repository Rust dependency graph requires rustc 1.94.1 while the runner has 1.89.0
Final Attestation