fix(passthrough): record real TTFT and start_time for streaming requests - #5
Merged
Merged
Conversation
songkuan-zheng
force-pushed
the
fix/passthrough-ttft-streaming
branch
from
May 18, 2026 09:53
92da12f to
f4e41ec
Compare
Pass-through streaming requests (/v1/messages, /vertex_ai/*, /gemini/*, /cohere/*, /assemblyai/*, /openai/*, /cursor/*) all share PassThroughStreamingHandler.chunk_processor, which had two timing bugs that interacted to collapse spend_logs.completionStartTime onto spend_logs.endTime (off by ~1ms of clock resolution, not literally identical) — making the streaming phase (endTime - completionStartTime) round to roughly zero and TTFT effectively soak up the entire request duration for every pass-through streaming row. Root cause 1. `start_time` arg too late. The caller's start_time originates in BaseAnthropicMessagesStreamingIterator.__init__, which runs AFTER the upstream HTTP response has already been received. SpendLogs. startTime therefore reflects "moment we started reading the stream", not "moment the client request entered the proxy" — the real TTFT window is silently subtracted from Duration. 2. First-chunk arrival never recorded. The chunk loop yielded bytes to the client and collected them for logging, but never noted when the first byte arrived. With litellm_logging_obj.completion_start_time left as None, the fallback at litellm_logging.py:1834-1837 sets it to end_time — completionStartTime lands within ~1ms of endTime and streaming_phase rounds to 0. Both bugs hide each other. Fixing only #2 gives TTFT close to 0 with Duration deflated by ~TTFT. Fixing only #1 leaves completionStartTime still pinned to endTime. Both must be fixed for the math to be correct. Fix In chunk_processor, at the top of the try block: - Override start_time with litellm_logging_obj.start_time when the latter is an earlier datetime — that's the true request-entry timestamp set in common_request_processing.base_process_llm_request. - On the first chunk yielded by response.aiter_bytes(), call litellm_logging_obj._update_completion_start_time(datetime.now()) to populate the field that downstream payload builders look for. Verified end-to-end (Anthropic claude-sonnet-4-6, 200-word stream): Before fix: Duration=6528ms TTFT=6527ms streaming_phase=1ms After fix: Duration=8496ms TTFT=2373ms streaming_phase=6123ms Control: /v1/chat/completions (same model+prompt) Duration=8143ms TTFT=2071ms streaming_phase=6072ms Test plan - New e2e case 13 (`13_passthrough_streaming_ttft.md` + data/13_*.sh) sends a real ~200-word streamed completion through /v1/messages, polls spend_logs for up to 30s, asserts: * streaming_phase_ms > 1000 (catches bug #2 regression) * ttft_ms > 300 (catches bug #1 regression) * ttft_ms < duration_ms / 2 (catches either regression) Plus a soft parity check vs /v1/chat/completions for the same upstream model. - Cost ~$0.005 per case run. - GREEN against the fix; was RED before (streaming_phase=1ms, ttft=6527ms) — assertions 1 and 3 fired. Blast radius: all pass-through endpoints — they all flow through this single chunk_processor. User noticed the bug via Anthropic; the same fix improves TTFT observability for Vertex AI, Gemini, Cohere, and the other pass-through providers in the same release.
songkuan-zheng
force-pushed
the
fix/passthrough-ttft-streaming
branch
from
May 18, 2026 09:57
f4e41ec to
1f54c13
Compare
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Streaming requests through any pass-through endpoint (
/v1/messages,/vertex_ai/*,/gemini/*,/cohere/*, …) loggedcompletionStartTimewithin ~1 ms of
endTimeinspend_logs— collapsing the streamingphase to roughly zero and letting TTFT (time-to-first-token) effectively
soak up the entire request duration. Reported via the Anthropic
/v1/messagespath; the root cause is in shared code so the fixcovers every pass-through endpoint.
Two interacting bugs
Both live in
litellm/proxy/pass_through_endpoints/streaming_handler.py'sPassThroughStreamingHandler.chunk_processor:Bug 1 —
start_timearg is captured too lateThe caller's
start_timeoriginates inBaseAnthropicMessagesStreamingIterator.__init__, which runs afterthe upstream HTTP response has already been received. So
SpendLogs.startTimereflects "moment we started reading the stream",not "moment the client request entered the proxy". The real TTFT window
is silently subtracted from
Duration.Bug 2 — First-chunk arrival never recorded
The chunk loop yielded bytes to the client and collected them for
logging, but never noted when the first byte arrived. With
litellm_logging_obj.completion_start_timeleft asNone, the fallbackat
litellm_logging.py:1834-1837sets it toend_time—completionStartTimelands within ~1 ms ofendTime(clock resolutionnoise, not literally identical), and
streaming_phaserounds to 0.The bugs hide each other
/v1/chat/completions, same model+prompt)Numbers are from an Anthropic claude-sonnet-4-6 ~200-word streamed
completion against the e2e proxy. After the fix the pass-through path
is within ~5% of the transform path on the same upstream model — the
two should report the same streaming behavior because the underlying
HTTP request is the same.
Fix
At the top of
chunk_processor'stryblock:Inside the
async for chunk in response.aiter_bytes():loop, beforeyielding to the client:
What changes
litellm/proxy/pass_through_endpoints/streaming_handler.pychunk_processor:start_timeoverride + first-chunk recordinge2e/cases/13_passthrough_streaming_ttft.mde2e/cases/data/13_passthrough_streaming_ttft.she2e/cases/README.mdTest plan
/v1/chat/completionstransform path on same modelBlast radius
The shared
chunk_processoris the streaming hot path for everypass-through endpoint:
/v1/messages,/anthropic/*(Anthropic)/vertex_ai/*(Vertex AI, both regular and live)/gemini/*(Google AI Studio)/cohere/*(Cohere)/openai/*(OpenAI pass-through)/assemblyai/*(AssemblyAI)/cursor/*(Cursor)The user noticed it via Anthropic. The same fix improves TTFT
observability for all the others in the same release.
Out of scope
litellm.completion()/acompletionpath (used by/v1/chat/completions) is unaffected. ItsCustomStreamWrapperat
litellm_core_utils/streaming_handler.py:1856already calls_update_completion_start_timecorrectly.litellm_logging.py:1834-1837is left in place — itstill protects callers who fail to set
completion_start_timefornon-streaming reasons. Now nobody in this code path needs to rely on
it.