Skip to content

fix(responses): run stream failure and success hooks on the iterating loop instead of blocking it - #43270

Merged
mateo-berri merged 3 commits into
mainfrom
litellm_responses_stream_failure_handlers_on_loop
Sep 26, 2026
Merged

mateo-berri merged 3 commits into
mainfrom
litellm_responses_stream_failure_handlers_on_loop

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A provider stream dropping mid-body on native /v1/responses stalls the whole proxy
  • The failure logging ran on a helper thread while the event loop sat waiting for it
  • It never returns when a callback needs the blocked loop, so the pod freezes until restart

How it solves it:

  • With a running loop, failure logging becomes a task on it, awaited before the error propagates
  • Sync failure callbacks are submitted after that async logging finishes, keeping the old order
  • The completed-stream success deployment hook is scheduled the same way
  • Without a running loop (the sync iterator) the blocking path is unchanged

User Flow

Before: a developer streaming from /v1/responses through a proxy with an async logging callback (Datadog here) freezes the proxy for everyone the moment one provider stream drops

  1. They send POST https://litellm-domain/v1/responses with "stream": true and get response.created plus the first output deltas
  2. The provider connection drops mid-body
  3. Every other request, GET https://litellm-domain/health/liveliness included, times out for as long as the failure logging takes (20s with a slow Datadog agent), and the pod can stay frozen until it is restarted when the drop lands while another request's log is still being sent
  4. Only after that does their stream get response.failed

After: the same drop reaches their client as response.failed while the proxy keeps serving

  1. They send POST https://litellm-domain/v1/responses with "stream": true and get response.created plus the first output deltas
  2. The provider connection drops mid-body
  3. GET https://litellm-domain/health/liveliness keeps answering 200 throughout and the failure event still reaches Datadog
  4. Their stream gets response.failed once the drop has been logged and ends

Linear ticket

Resolves LIT-8678

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Both legs ran the same rig on one laptop and differ only in the commit the proxy was booted from: the Before leg at the PR's merge base 4adbc13 and the After leg at the PR tip 0be4a64, each in its own worktree ($WT) with its own venv ($PY is $WT/.venv/bin/python, $MK the proxy master key). Every case boots one fresh proxy with --num_workers 2 on a random free port from rig/config.yaml, which carries three deployments: gpt-5.4-mini (openai/gpt-5.4-mini straight to api.openai.com), gpt-5.4-mini-flaky-upstream (the same model with api_base pointing at a local relay that forwards every request to https://api.openai.com and, once armed with touch rig/out/cut.signal, aborts the proxy's connection mid-body at the next chunk after 20 output deltas) and gpt-5.4-mini-bridge-flaky-upstream (openai/responses/gpt-5.4-mini through that relay, so /v1/chat/completions and /v1/messages ride the Responses API stream). litellm_settings.callbacks: ["datadog"] runs in agent mode (LITELLM_DD_AGENT_HOST/LITELLM_DD_AGENT_PORT) against a local stand-in that accepts every POST /api/v2/logs and holds its 202 for 20s, a slow Datadog agent, with DD_BATCH_SIZE=1 so every event flushes right away. Throughout each case six keep-alive HTTP/1.1 connections each send GET /health/liveliness once a second (what a Kubernetes liveness probe and every other caller of that pod see); a connection with no answer for 2s or more is listed as hung with how long it waited. The clients are the OpenAI Python SDK (client.responses.create(..., stream=True) on /v1/responses, client.chat.completions.create(..., stream=True) on /v1/chat/completions, a non-stream client.responses.create(...) for the requests whose log flush holds the logger) and the Anthropic Python SDK (client.messages.create(..., stream=True) on /v1/messages), all against the real gpt-5.4-mini. Response ids in the non-stream output are shortened to resp_<id>. Every timestamp is local wall clock, cases ran back to back

Before (4adbc13)

Mid-stream provider drop on /v1/responses

  1. proxy booted 19:23:38: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 49660 --num_workers 2 (pid 22071, /health/liveliness answering)
  2. 19:24:07 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 19:24:07 $PY rig/client_stream.py http://127.0.0.1:49660/v1 $MK gpt-5.4-mini-flaky-upstream
  4. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:24:08 POST http://127.0.0.1:49660/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:24:10 response.created
    19:24:10 response.in_progress
    19:24:10 response.output_item.added
    19:24:10 response.content_part.added
    19:24:10 delta #1
    19:24:10 delta #5
    19:24:50 response.failed
    19:24:50 stream ended normally after 10 deltas
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:49660/health/liveliness once a second throughout:
    conn4: GET /health/liveliness sent 19:24:10 answered 200 after 19.6s
    conn6: GET /health/liveliness sent 19:24:10 answered 200 after 19.6s
    conn3: GET /health/liveliness sent 19:24:10 answered 200 after 19.6s
    summary: 222 probes over 6 keep-alive connections; 3 connection(s) hung (conn3, conn4, conn6); slowest answered probe 19.6s
    
  6. Upstream relay:
    19:24:08 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:24:09 <- HTTP/1.1 200 OK
    19:24:09 <- first response.output_text.delta passed through to the proxy
    19:24:10 CUT: aborting the proxy's connection mid-body after 5665 bytes and 20 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    19:24:10 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:24:30 sent 202 for ['error']
    19:24:30 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:24:50 sent 202 for ['error']
    
  8. Result: cut_at=19:24:10 response.failed_at=19:24:50 cut_to_failed=40s response.completed_at=None probe=summary: 222 probes over 6 keep-alive connections; 3 connection(s) hung (conn3, conn4, conn6); slowest answered probe 19.6s

Drop while six other requests' log flushes hold the logger

  1. proxy booted 19:25:04: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 25322 --num_workers 2 (pid 24987, /health/liveliness answering)
  2. 19:25:25 $PY rig/client_stream.py http://127.0.0.1:25322/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 19:25:27 stream is past its first delta; 6 concurrent non-stream requests: for i in 1 2 3 4 5 6; do $PY rig/client_once.py http://127.0.0.1:25322/v1 $MK gpt-5.4-mini & done; wait (each one's success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 19:25:29 all 6 non-stream requests answered; cut armed (touch rig/out/cut.signal)
  5. Non-stream clients:
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:29 resp_<id> status=completed text='Hi there, friend.'
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:28 resp_<id> status=completed text='Hi there, friend.'
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:29 resp_<id> status=completed text='Hi there, friend.'
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:28 resp_<id> status=completed text='Hi there, friend.'
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:29 resp_<id> status=completed text='Hi there, friend.'
    19:25:27 POST http://127.0.0.1:25322/v1/responses stream=False model=gpt-5.4-mini
    19:25:28 resp_<id> status=completed text='Hi there!'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:25:26 POST http://127.0.0.1:25322/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:25:27 response.created
    19:25:27 response.in_progress
    19:25:27 response.output_item.added
    19:25:27 response.content_part.added
    19:25:27 delta #1
    19:25:27 delta #5
    19:25:27 delta #20
    19:25:27 delta #50
    19:26:09 response.failed
    19:26:09 stream ended normally after 273 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:25322/health/liveliness once a second throughout:
    summary: 282 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s
    
  8. Upstream relay:
    19:25:26 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:25:27 <- HTTP/1.1 200 OK
    19:25:27 <- first response.output_text.delta passed through to the proxy
    19:25:29 CUT: aborting the proxy's connection mid-body after 76086 bytes and 512 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    19:25:28 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:25:29 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:25:48 POST /api/v2/logs: 7 event(s) ['info', 'info', 'info', 'info', 'info', 'error', 'error']; holding the 202 for 20.0s
    19:25:48 POST /api/v2/logs: 7 event(s) ['info', 'info', 'info', 'info', 'info', 'error', 'error']; holding the 202 for 20.0s
    19:25:49 sent 202 for ['info']
    19:25:49 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:25:49 POST /api/v2/logs: 7 event(s) ['info', 'info', 'info', 'info', 'info', 'error', 'error']; holding the 202 for 20.0s
    19:25:49 POST /api/v2/logs: 7 event(s) ['info', 'info', 'info', 'info', 'info', 'error', 'error']; holding the 202 for 20.0s
    19:26:08 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error', 'error']
    19:26:08 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error', 'error']
    19:26:09 sent 202 for ['info']
    19:26:09 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error', 'error']
    19:26:09 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error', 'error']
    
  10. Result: cut_at=19:25:29 response.failed_at=19:26:09 cut_to_failed=40s response.completed_at=None probe=summary: 282 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s

Healthy stream completes

  1. proxy booted 19:26:28: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 54144 --num_workers 2 (pid 28870, /health/liveliness answering)
  2. 19:26:58 no cut armed; $PY rig/client_stream.py http://127.0.0.1:54144/v1 $MK gpt-5.4-mini-flaky-upstream
  3. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:26:58 POST http://127.0.0.1:54144/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:26:59 response.created
    19:26:59 response.in_progress
    19:26:59 response.output_item.added
    19:26:59 response.content_part.added
    19:26:59 delta #1
    19:26:59 delta #5
    19:27:00 delta #20
    19:27:00 delta #50
    19:27:05 response.output_text.done
    19:27:05 response.content_part.done
    19:27:05 response.output_item.done
    19:27:05 response.completed
    19:27:05 stream ended normally after 745 deltas
    
  4. Six keep-alive connections each sending GET http://127.0.0.1:54144/health/liveliness once a second throughout:
    summary: 66 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s
    
  5. Upstream relay:
    19:26:59 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:26:59 <- HTTP/1.1 200 OK
    19:26:59 <- first response.output_text.delta passed through to the proxy
    19:27:05 <- upstream stream ended normally after 221346 bytes and 1371 output_text.delta events
    
  6. Datadog agent stand-in (holds every 202 for 20s):
    19:27:05 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    
  7. Result: cut_at=None response.failed_at=None cut_to_failed=n/a response.completed_at=19:27:05 probe=summary: 66 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s

Mid-stream drop on /v1/chat/completions through the Responses bridge

  1. proxy booted 19:27:22: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 56906 --num_workers 2 (pid 31663, /health/liveliness answering)
  2. 19:27:44 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 19:27:44 $PY rig/client_stream_chat.py http://127.0.0.1:56906/v1 $MK gpt-5.4-mini-bridge-flaky-upstream
  4. Streaming client (OpenAI Python SDK, client.chat.completions.create(..., stream=True) on a model served through the Responses API):
    19:27:45 POST http://127.0.0.1:56906/v1/chat/completions stream=True model=gpt-5.4-mini-bridge-flaky-upstream
    19:27:46 content chunk #1
    19:27:46 content chunk #5
    19:28:47 STREAM ERROR openai.APIError: litellm.APIConnectionError: APIConnectionError: OpenAIException - Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:56906/health/liveliness once a second throughout:
    conn4: GET /health/liveliness sent 19:27:47 answered 200 after 19.5s
    conn5: GET /health/liveliness sent 19:27:47 answered 200 after 19.5s
    conn2: GET /health/liveliness sent 19:27:47 answered 200 after 19.5s
    conn6: GET /health/liveliness sent 19:27:47 answered 200 after 19.5s
    conn1: GET /health/liveliness sent 19:27:47 answered 200 after 19.5s
    summary: 301 probes over 6 keep-alive connections; 5 connection(s) hung (conn1, conn2, conn4, conn5, conn6); slowest answered probe 19.5s
    
  6. Upstream relay:
    19:27:45 -> POST /v1/responses HTTP/1.1 (179 body bytes) forwarded to https://api.openai.com
    19:27:46 <- HTTP/1.1 200 OK
    19:27:46 <- first response.output_text.delta passed through to the proxy
    19:27:47 CUT: aborting the proxy's connection mid-body after 5960 bytes and 20 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    19:27:47 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:28:07 sent 202 for ['error']
    19:28:07 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:28:27 sent 202 for ['error']
    19:28:27 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:28:47 sent 202 for ['error']
    
  8. Result: cut_at=19:27:47 response.failed_at=19:28:47 cut_to_failed=60s response.completed_at=None probe=summary: 301 probes over 6 keep-alive connections; 5 connection(s) hung (conn1, conn2, conn4, conn5, conn6); slowest answered probe 19.5s

Mid-stream drop on /v1/messages through the Responses bridge

  1. proxy booted 19:29:00: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 21091 --num_workers 2 (pid 36798, /health/liveliness answering)
  2. 19:29:25 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 19:29:25 $PY rig/client_stream_messages.py http://127.0.0.1:21091 $MK gpt-5.4-mini-bridge-flaky-upstream
  4. Streaming client (Anthropic Python SDK, client.messages.create(..., stream=True) on a model served through the Responses API):
    19:29:26 POST http://127.0.0.1:21091/v1/messages stream=True model=gpt-5.4-mini-bridge-flaky-upstream
    19:29:27 message_start
    19:29:27 content_block_start
    19:29:27 content_block_delta #1
    19:29:27 content_block_delta #5
    19:29:47 STREAM ERROR anthropic.APIStatusError: {'type': 'error', 'error': {'type': 'api_error', 'message': "Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>"}}
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:21091/health/liveliness once a second throughout:
    conn5: GET /health/liveliness sent 19:29:27 answered 200 after 19.9s
    conn4: GET /health/liveliness sent 19:29:27 answered 200 after 19.9s
    conn3: GET /health/liveliness sent 19:29:27 answered 200 after 19.9s
    conn2: GET /health/liveliness sent 19:29:27 answered 200 after 19.9s
    summary: 78 probes over 6 keep-alive connections; 4 connection(s) hung (conn2, conn3, conn4, conn5); slowest answered probe 19.9s
    
  6. Upstream relay:
    19:29:26 -> POST /v1/responses HTTP/1.1 (246 body bytes) forwarded to https://api.openai.com
    19:29:27 <- HTTP/1.1 200 OK
    19:29:27 <- first response.output_text.delta passed through to the proxy
    19:29:27 CUT: aborting the proxy's connection mid-body after 5651 bytes and 20 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    19:29:27 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:29:47 sent 202 for ['error']
    
  8. Result: cut_at=19:29:27 response.failed_at=19:29:47 cut_to_failed=20s response.completed_at=None probe=summary: 78 probes over 6 keep-alive connections; 4 connection(s) hung (conn2, conn3, conn4, conn5); slowest answered probe 19.9s

Drop while one earlier request's log flush holds the logger (the repro's frozen-pod sequence, three attempts)

Attempt 1

  1. proxy booted 19:38:15: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 33495 --num_workers 2 (pid 61942, /health/liveliness answering)
  2. 19:38:56 $PY rig/client_stream.py http://127.0.0.1:33495/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 19:38:59 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:33495/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 19:39:02 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    19:38:59 POST http://127.0.0.1:33495/v1/responses stream=False model=gpt-5.4-mini
    19:39:01 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:38:57 POST http://127.0.0.1:33495/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:38:59 response.created
    19:38:59 response.in_progress
    19:38:59 response.output_item.added
    19:38:59 response.content_part.added
    19:38:59 delta #1
    19:38:59 delta #5
    19:38:59 delta #20
    19:38:59 delta #50
    19:39:41 response.failed
    19:39:41 stream ended normally after 401 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:33495/health/liveliness once a second throughout:
    summary: 294 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s
    
  8. Upstream relay:
    19:38:58 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:38:58 <- HTTP/1.1 200 OK
    19:38:58 <- first response.output_text.delta passed through to the proxy
    19:39:02 CUT: aborting the proxy's connection mid-body after 110455 bytes and 747 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    19:39:01 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:39:21 POST /api/v2/logs: 3 event(s) ['info', 'error', 'error']; holding the 202 for 20.0s
    19:39:21 sent 202 for ['info']
    19:39:41 sent 202 for ['info', 'error', 'error']
    
  10. Result: cut_at=19:39:02 response.failed_at=19:39:41 cut_to_failed=39s response.completed_at=None probe=summary: 294 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s

Attempt 2

  1. proxy booted 19:39:58: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 21627 --num_workers 2 (pid 73627, /health/liveliness answering)
  2. 19:40:32 $PY rig/client_stream.py http://127.0.0.1:21627/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 19:40:35 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:21627/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 19:40:37 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    19:40:35 POST http://127.0.0.1:21627/v1/responses stream=False model=gpt-5.4-mini
    19:40:37 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:40:34 POST http://127.0.0.1:21627/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:40:35 response.created
    19:40:35 response.in_progress
    19:40:35 response.output_item.added
    19:40:35 response.content_part.added
    19:40:35 delta #1
    19:40:35 delta #5
    19:40:35 delta #20
    19:40:35 delta #50
    19:41:17 response.failed
    19:41:17 stream ended normally after 265 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:21627/health/liveliness once a second throughout:
    summary: 294 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s
    
  8. Upstream relay:
    19:40:34 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:40:35 <- HTTP/1.1 200 OK
    19:40:35 <- first response.output_text.delta passed through to the proxy
    19:40:37 CUT: aborting the proxy's connection mid-body after 73929 bytes and 495 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    19:40:37 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:40:57 sent 202 for ['info']
    19:40:57 POST /api/v2/logs: 3 event(s) ['info', 'error', 'error']; holding the 202 for 20.0s
    19:41:17 sent 202 for ['info', 'error', 'error']
    
  10. Result: cut_at=19:40:37 response.failed_at=19:41:17 cut_to_failed=40s response.completed_at=None probe=summary: 294 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s

Attempt 3

  1. proxy booted 19:41:38: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 38155 --num_workers 2 (pid 80554, /health/liveliness answering)
  2. 19:42:43 $PY rig/client_stream.py http://127.0.0.1:38155/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 19:42:46 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:38155/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 19:42:47 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    19:42:46 POST http://127.0.0.1:38155/v1/responses stream=False model=gpt-5.4-mini
    19:42:47 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    19:42:44 POST http://127.0.0.1:38155/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    19:42:45 response.created
    19:42:45 response.in_progress
    19:42:45 response.output_item.added
    19:42:45 response.content_part.added
    19:42:45 delta #1
    19:42:45 delta #5
    19:42:45 delta #20
    19:42:46 delta #50
    19:43:28 response.failed
    19:43:28 stream ended normally after 345 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:38155/health/liveliness once a second throughout:
    conn5: GET /health/liveliness sent 19:42:48 answered 200 after 19.7s
    conn1: GET /health/liveliness sent 19:42:48 answered 200 after 19.7s
    summary: 256 probes over 6 keep-alive connections; 2 connection(s) hung (conn1, conn5); slowest answered probe 19.7s
    
  8. Upstream relay:
    19:42:45 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    19:42:45 <- HTTP/1.1 200 OK
    19:42:45 <- first response.output_text.delta passed through to the proxy
    19:42:48 CUT: aborting the proxy's connection mid-body after 95300 bytes and 640 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    19:42:47 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:42:48 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:43:07 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    19:43:07 sent 202 for ['info']
    19:43:08 sent 202 for ['error']
    19:43:08 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    19:43:27 sent 202 for ['info']
    19:43:28 sent 202 for ['error']
    
  10. Result: cut_at=19:42:48 response.failed_at=19:43:28 cut_to_failed=40s response.completed_at=None probe=summary: 256 probes over 6 keep-alive connections; 2 connection(s) hung (conn1, conn5); slowest answered probe 19.7s

After (0be4a64)

Mid-stream provider drop on /v1/responses

  1. proxy booted 21:37:05: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 23575 --num_workers 2 (pid 2497, /health/liveliness answering)
  2. 21:37:32 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 21:37:32 $PY rig/client_stream.py http://127.0.0.1:23575/v1 $MK gpt-5.4-mini-flaky-upstream
  4. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:37:33 POST http://127.0.0.1:23575/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:37:34 response.created
    21:37:34 response.in_progress
    21:37:34 response.output_item.added
    21:37:34 response.content_part.added
    21:37:34 delta #1
    21:37:34 delta #5
    21:38:15 response.failed
    21:38:15 stream ended normally after 11 deltas
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:23575/health/liveliness once a second throughout:
    summary: 276 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s
    
  6. Upstream relay:
    21:37:33 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:37:34 <- HTTP/1.1 200 OK
    21:37:34 <- first response.output_text.delta passed through to the proxy
    21:37:35 CUT: aborting the proxy's connection mid-body after 5942 bytes and 20 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    21:37:35 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:37:55 sent 202 for ['error']
    21:37:55 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:38:15 sent 202 for ['error']
    
  8. Result: cut_at=21:37:35 response.failed_at=21:38:15 cut_to_failed=40s response.completed_at=None probe=summary: 276 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s

Drop while six other requests' log flushes hold the logger

  1. proxy booted 21:38:35: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 44750 --num_workers 2 (pid 10818, /health/liveliness answering)
  2. 21:39:08 $PY rig/client_stream.py http://127.0.0.1:44750/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 21:39:10 stream is past its first delta; 6 concurrent non-stream requests: for i in 1 2 3 4 5 6; do $PY rig/client_once.py http://127.0.0.1:44750/v1 $MK gpt-5.4-mini & done; wait (each one's success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 21:39:12 all 6 non-stream requests answered; cut armed (touch rig/out/cut.signal)
  5. Non-stream clients:
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:11 resp_<id> status=completed text='Hi there, friend.'
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:11 resp_<id> status=completed text='Hi there!'
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:12 resp_<id> status=completed text='Hi there, friend.'
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:12 resp_<id> status=completed text='Hi there, friend.'
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:11 resp_<id> status=completed text='Hi there! Hello.'
    21:39:10 POST http://127.0.0.1:44750/v1/responses stream=False model=gpt-5.4-mini
    21:39:11 resp_<id> status=completed text='Hi there!'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:39:08 POST http://127.0.0.1:44750/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:39:10 response.created
    21:39:10 response.in_progress
    21:39:10 response.output_item.added
    21:39:10 response.content_part.added
    21:39:10 delta #1
    21:39:10 delta #5
    21:39:10 delta #20
    21:39:10 delta #50
    21:40:12 response.failed
    21:40:12 stream ended normally after 388 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:44750/health/liveliness once a second throughout:
    summary: 408 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.1s
    
  8. Upstream relay:
    21:39:09 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:39:09 <- HTTP/1.1 200 OK
    21:39:10 <- first response.output_text.delta passed through to the proxy
    21:39:13 CUT: aborting the proxy's connection mid-body after 107015 bytes and 704 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    21:39:11 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:39:12 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:39:31 POST /api/v2/logs: 6 event(s) ['info', 'info', 'info', 'info', 'info', 'error']; holding the 202 for 20.0s
    21:39:31 POST /api/v2/logs: 6 event(s) ['info', 'info', 'info', 'info', 'info', 'error']; holding the 202 for 20.0s
    21:39:31 POST /api/v2/logs: 6 event(s) ['info', 'info', 'info', 'info', 'info', 'error']; holding the 202 for 20.0s
    21:39:32 POST /api/v2/logs: 6 event(s) ['info', 'info', 'info', 'info', 'info', 'error']; holding the 202 for 20.0s
    21:39:32 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:39:32 sent 202 for ['info']
    21:39:51 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error']
    21:39:51 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error']
    21:39:52 sent 202 for ['info', 'info', 'info', 'info', 'info', 'error']
    21:39:52 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:39:52 sent 202 for ['info']
    21:40:12 sent 202 for ['error']
    
  10. Result: cut_at=21:39:13 response.failed_at=21:40:12 cut_to_failed=59s response.completed_at=None probe=summary: 408 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.1s

Healthy stream completes

  1. proxy booted 21:40:37: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 32256 --num_workers 2 (pid 23446, /health/liveliness answering)
  2. 21:41:07 no cut armed; $PY rig/client_stream.py http://127.0.0.1:32256/v1 $MK gpt-5.4-mini-flaky-upstream
  3. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:41:07 POST http://127.0.0.1:32256/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:41:09 response.created
    21:41:09 response.in_progress
    21:41:09 response.output_item.added
    21:41:09 response.content_part.added
    21:41:09 delta #1
    21:41:09 delta #5
    21:41:09 delta #20
    21:41:09 delta #50
    21:41:15 response.output_text.done
    21:41:15 response.content_part.done
    21:41:15 response.output_item.done
    21:41:15 response.completed
    21:41:15 stream ended normally after 767 deltas
    
  4. Six keep-alive connections each sending GET http://127.0.0.1:32256/health/liveliness once a second throughout:
    summary: 72 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s
    
  5. Upstream relay:
    21:41:08 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:41:08 <- HTTP/1.1 200 OK
    21:41:08 <- first response.output_text.delta passed through to the proxy
    21:41:15 <- upstream stream ended normally after 227502 bytes and 1422 output_text.delta events
    
  6. Datadog agent stand-in (holds every 202 for 20s):
    21:41:15 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    
  7. Result: cut_at=None response.failed_at=None cut_to_failed=n/a response.completed_at=21:41:15 probe=summary: 72 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s

Mid-stream drop on /v1/chat/completions through the Responses bridge

  1. proxy booted 21:41:38: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 46622 --num_workers 2 (pid 28697, /health/liveliness answering)
  2. 21:42:08 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 21:42:08 $PY rig/client_stream_chat.py http://127.0.0.1:46622/v1 $MK gpt-5.4-mini-bridge-flaky-upstream
  4. Streaming client (OpenAI Python SDK, client.chat.completions.create(..., stream=True) on a model served through the Responses API):
    21:42:08 POST http://127.0.0.1:46622/v1/chat/completions stream=True model=gpt-5.4-mini-bridge-flaky-upstream
    21:42:09 content chunk #1
    21:42:09 content chunk #5
    21:43:09 STREAM ERROR openai.APIError: litellm.APIConnectionError: APIConnectionError: OpenAIException - Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:46622/health/liveliness once a second throughout:
    summary: 390 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s
    
  6. Upstream relay:
    21:42:08 -> POST /v1/responses HTTP/1.1 (179 body bytes) forwarded to https://api.openai.com
    21:42:09 <- HTTP/1.1 200 OK
    21:42:09 <- first response.output_text.delta passed through to the proxy
    21:42:09 CUT: aborting the proxy's connection mid-body after 6204 bytes and 23 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    21:42:09 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:42:29 sent 202 for ['error']
    21:42:29 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:42:49 sent 202 for ['error']
    21:42:49 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:43:09 sent 202 for ['error']
    
  8. Result: cut_at=21:42:09 response.failed_at=21:43:09 cut_to_failed=60s response.completed_at=None probe=summary: 390 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.0s

Mid-stream drop on /v1/messages through the Responses bridge

  1. proxy booted 21:43:17: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 57576 --num_workers 2 (pid 36647, /health/liveliness answering)
  2. 21:44:01 cut armed (touch rig/out/cut.signal: the relay drops the upstream connection after 20 output deltas)
  3. 21:44:01 $PY rig/client_stream_messages.py http://127.0.0.1:57576 $MK gpt-5.4-mini-bridge-flaky-upstream
  4. Streaming client (Anthropic Python SDK, client.messages.create(..., stream=True) on a model served through the Responses API):
    21:44:02 POST http://127.0.0.1:57576/v1/messages stream=True model=gpt-5.4-mini-bridge-flaky-upstream
    21:44:04 message_start
    21:44:04 content_block_start
    21:44:04 content_block_delta #1
    21:44:04 content_block_delta #5
    21:44:24 STREAM ERROR anthropic.APIStatusError: {'type': 'error', 'error': {'type': 'api_error', 'message': "Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>"}}
    
  5. Six keep-alive connections each sending GET http://127.0.0.1:57576/health/liveliness once a second throughout:
    summary: 156 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.1s
    
  6. Upstream relay:
    21:44:03 -> POST /v1/responses HTTP/1.1 (246 body bytes) forwarded to https://api.openai.com
    21:44:03 <- HTTP/1.1 200 OK
    21:44:03 <- first response.output_text.delta passed through to the proxy
    21:44:04 CUT: aborting the proxy's connection mid-body after 5651 bytes and 20 output_text.delta events
    
  7. Datadog agent stand-in (holds every 202 for 20s):
    21:44:04 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:44:24 sent 202 for ['error']
    
  8. Result: cut_at=21:44:04 response.failed_at=21:44:24 cut_to_failed=20s response.completed_at=None probe=summary: 156 probes over 6 keep-alive connections; 0 connection(s) hung (none); 0 never answered; slowest answered probe 0.1s

Drop while one earlier request's log flush holds the logger (the repro's frozen-pod sequence, three attempts)

Attempt 1

  1. proxy booted 21:51:59: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 23430 --num_workers 2 (pid 95090, /health/liveliness answering)
  2. 21:52:33 $PY rig/client_stream.py http://127.0.0.1:23430/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 21:52:34 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:23430/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 21:52:36 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    21:52:35 POST http://127.0.0.1:23430/v1/responses stream=False model=gpt-5.4-mini
    21:52:36 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:52:33 POST http://127.0.0.1:23430/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:52:34 response.created
    21:52:34 response.in_progress
    21:52:34 response.output_item.added
    21:52:34 response.content_part.added
    21:52:34 delta #1
    21:52:34 delta #5
    21:52:34 delta #20
    21:52:35 delta #50
    21:53:36 response.failed
    21:53:36 stream ended normally after 409 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:23430/health/liveliness once a second throughout:
    summary: 402 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s
    
  8. Upstream relay:
    21:52:34 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:52:34 <- HTTP/1.1 200 OK
    21:52:34 <- first response.output_text.delta passed through to the proxy
    21:52:37 CUT: aborting the proxy's connection mid-body after 112447 bytes and 763 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    21:52:36 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:52:56 POST /api/v2/logs: 2 event(s) ['info', 'error']; holding the 202 for 20.0s
    21:53:16 sent 202 for ['info', 'error']
    21:53:16 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:53:36 sent 202 for ['error']
    
  10. Result: cut_at=21:52:37 response.failed_at=21:53:36 cut_to_failed=59s response.completed_at=None probe=summary: 402 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.1s

Attempt 2

  1. proxy booted 21:54:08: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 47366 --num_workers 2 (pid 9250, /health/liveliness answering)
  2. 21:54:48 $PY rig/client_stream.py http://127.0.0.1:47366/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 21:54:49 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:47366/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 21:54:51 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    21:54:50 POST http://127.0.0.1:47366/v1/responses stream=False model=gpt-5.4-mini
    21:54:51 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:54:48 POST http://127.0.0.1:47366/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:54:49 response.created
    21:54:49 response.in_progress
    21:54:49 response.output_item.added
    21:54:49 response.content_part.added
    21:54:49 delta #1
    21:54:49 delta #5
    21:54:49 delta #20
    21:54:50 delta #50
    21:55:51 response.failed
    21:55:51 stream ended normally after 390 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:47366/health/liveliness once a second throughout:
    summary: 402 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s
    
  8. Upstream relay:
    21:54:49 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:54:49 <- HTTP/1.1 200 OK
    21:54:49 <- first response.output_text.delta passed through to the proxy
    21:54:52 CUT: aborting the proxy's connection mid-body after 107451 bytes and 725 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    21:54:51 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:55:11 POST /api/v2/logs: 2 event(s) ['info', 'error']; holding the 202 for 20.0s
    21:55:31 sent 202 for ['info', 'error']
    21:55:31 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:55:51 sent 202 for ['error']
    
  10. Result: cut_at=21:54:52 response.failed_at=21:55:51 cut_to_failed=59s response.completed_at=None probe=summary: 402 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s

Attempt 3

  1. proxy booted 21:56:00: cd $WT && .venv/bin/python litellm/proxy/proxy_cli.py --config rig/config.yaml --port 25140 --num_workers 2 (pid 19871, /health/liveliness answering)
  2. 21:56:35 $PY rig/client_stream.py http://127.0.0.1:25140/v1 $MK gpt-5.4-mini-flaky-upstream (no cut armed yet)
  3. 21:56:36 stream is past its first delta; one non-stream request: $PY rig/client_once.py http://127.0.0.1:25140/v1 $MK gpt-5.4-mini (its success log flush holds the Datadog logger's lock on its worker for 20s)
  4. 21:56:37 non-stream request answered; cut armed (touch rig/out/cut.signal); the stream client gets up to 150s from here
  5. Non-stream client:
    21:56:36 POST http://127.0.0.1:25140/v1/responses stream=False model=gpt-5.4-mini
    21:56:37 resp_<id> status=completed text='Hi there, friend.'
    
  6. Streaming client (OpenAI Python SDK, client.responses.create(..., stream=True)):
    21:56:35 POST http://127.0.0.1:25140/v1/responses stream=True model=gpt-5.4-mini-flaky-upstream
    21:56:36 response.created
    21:56:36 response.in_progress
    21:56:36 response.output_item.added
    21:56:36 response.content_part.added
    21:56:36 delta #1
    21:56:36 delta #5
    21:56:36 delta #20
    21:56:36 delta #50
    21:57:38 response.failed
    21:57:38 stream ended normally after 298 deltas
    
  7. Six keep-alive connections each sending GET http://127.0.0.1:25140/health/liveliness once a second throughout:
    summary: 396 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s
    
  8. Upstream relay:
    21:56:35 -> POST /v1/responses HTTP/1.1 (103 body bytes) forwarded to https://api.openai.com
    21:56:36 <- HTTP/1.1 200 OK
    21:56:36 <- first response.output_text.delta passed through to the proxy
    21:56:38 CUT: aborting the proxy's connection mid-body after 82724 bytes and 546 output_text.delta events
    
  9. Datadog agent stand-in (holds every 202 for 20s):
    21:56:37 POST /api/v2/logs: 1 event(s) ['info']; holding the 202 for 20.0s
    21:56:57 sent 202 for ['info']
    21:56:57 POST /api/v2/logs: 2 event(s) ['info', 'error']; holding the 202 for 20.0s
    21:57:17 sent 202 for ['info', 'error']
    21:57:17 POST /api/v2/logs: 1 event(s) ['error']; holding the 202 for 20.0s
    21:57:37 sent 202 for ['error']
    
  10. Result: cut_at=21:56:38 response.failed_at=21:57:38 cut_to_failed=60s response.completed_at=None probe=summary: 396 probes over 6 keep-alive connections; 0 connection(s) hung (none); slowest answered probe 0.0s

Observations from both legs, with whether this PR causes each, makes it worse, or leaves it alone:

  • response.failed lands ~40s after the cut on /v1/responses, both legs; PR leaves it alone
  • Cases 2 and 6 error 59-60s after the cut, was 40s; PR causes it
  • Chat bridge error 60s after the cut on both legs; PR leaves it alone
  • Messages bridge error 20s after the cut on both legs; PR leaves it alone
  • Before cases 1, 4, 5 hung 3, 5, 4 probe connections; PR removes that
  • Before cases 2, 6 logged loop-binding errors; PR removes them
  • Case 2 hung nothing on either leg
  • Client receives about half the provider's output delta count; PR leaves it alone
  • One dropped stream sends Datadog two or three error events; PR leaves it alone
  • Datadog received duplicate event batches under lock contention (case 2); PR leaves it alone
  • Dropped-stream error text is aiohttp's TransferEncodingError; PR leaves it alone
  • Case 5 logs one AnthropicResponsesStreamWrapper ERROR line, both legs; PR leaves it alone
  • Permanent freeze (repro leg 2, one worker) not seen two-worker

Type

🐛 Bug Fix

Decisions

  1. A running loop is detected with asyncio.get_running_loop() rather than by iterator class, so a sync iterator driven from inside a live loop takes the non-blocking path as well, and with no loop the sync path is byte for byte what it was
  2. The base's hook set is kept as it was (the async failure handler, then the sync failure_handler on the executor); only where and when they run changed. An earlier revision of this PR went through dispatch_failure_handlers(prefer_async_handlers=True), which only submits the sync handler when sync failure callbacks are configured; it was dropped because it changed which callbacks fire, not just when
  3. On the async iterator the failure logging is awaited inside __anext__ before the exception propagates, instead of being fired and forgotten the way chat streaming's _log_stream_failure_and_raise does it. That keeps the base's guarantee that logging has completed by the time the caller sees the error, holds only this one request while it waits (every other request keeps being served), and means a client that goes away right after the error cannot cancel the logging out from under a failed-deployment attribution. A bounded wait was considered and rejected as a knob nobody asked for
  4. The sync failure_handler is executor-submitted in a finally after the awaited async handler, so the base's order (async first, then sync) holds even when the async handler raises
  5. A sync iterator iterated from inside a running loop (__next__ under a loop) cannot await, so it schedules the same task fire-and-forget; tasks live in a module-level strong-reference set with a done callback that logs a failed task at ERROR, where the base swallowed the exception
  6. One end_time is captured once and handed to both handlers instead of each computing its own
  7. The completed-stream success deployment hook gets the same treatment because it blocked the loop the same way from the same file and the stream discards its return value; on the async iterator it is awaited before StopAsyncIteration propagates, since that is an Exception and goes through the same except

Caveats (if any)

Low

  • The sync __next__ path with no running loop still blocks its own thread
    • Unchanged from the base and nobody reported it, so widening the fix there is net negative
  • A sync iterator driven inside a running loop schedules its logging fire-and-forget
    • A caller that closes that loop right away can cancel the task, and the done callback logs it at ERROR
    • The async iterator awaits the task, so the proxy path is not exposed; awaiting inside __next__ needs a loop we do not own
  • response.failed waits for a stalled logging callback with no timeout
    • Cases 2 and 6 land 59-60s after the cut, 40s on the base
    • On purpose per Decision 3, and Greptile's P1 thread on it conceded; a bounded wait would drop failure attribution on router fallbacks
  • Mock and cached streaming iterators schedule the success hook but never await it
    • Test-only surfaces, not driven; adding an await there changes test fixtures for no user-facing gain
  • Router cooldown after a mid-stream failure was not read back in QA
    • The failure handler order is the base's, so cooldown behavior is inherited, not changed
  • Non-required CI reds at the tip match main or a fix newer than the merge base, one rerun spent (workflow f69f748c) and nothing ported here

Live A/B risk check

Verdict: no dependent path breaks. Three legs ran the same 33 scenarios (28 proxy runs plus 5 SDK runs) against the real gpt-5.4-mini through a forwarding relay, each as one proxy with --num_workers 2 in its own worktree and venv against one Postgres database: Before at the merge base 4adbc13 (proxy port 35950, relay 21618), After at the PR tip 0be4a64 (33108, 52916), and Merged at dcc6517d71, the tip merged into main 5856a36307 (47492, 25310). Every leg printed its worktree, its loaded litellm.__file__, and whether the head-only symbols _spawn_logging_task and _await_pending_logging import (absent on Before, present on After and Merged). The rig loaded one consumer of every hook kind through the documented extension points: a CustomLogger with only sync hooks, one with only async hooks, a CustomGuardrail whose async_post_call_success_deployment_hook raises on purpose, a legacy failure_callback callable, and a legacy success_callback callable, each appending a row (hook, thread, loop, request keys) per call; spend rows were read back through GET /spend/logs by request tag

Breaking: none observed. Per scenario the hook call counts are identical on all three legs (87 callback rows per leg; a mid-stream cut on /v1/responses records async_log_failure_event once and the legacy failure callable once on each leg, an early cut with router fallback records the failure hooks plus the three success hooks on each leg, and the SDK sync iterator under a running loop records the async, legacy, and sync failure hooks once each on each leg, including the variant that exits the interpreter right after the failure). The relay forwarded and cut the same requests on every leg (6 GET /v1/models, 29 POST /v1/responses, the same cut mode per marker, request bodies the same size Before vs Merged and 1 byte smaller on After from the marker text). Spend rows are 1 per marker per run on every leg. Client verdicts differ only in call ids, marker text, and output length

Backward incompatible:

  • Under a held logger lock the client sees response.failed later: QA case 2 at 59s after the cut and case 6 at 59s, 59s, 60s, against 40s and 39s, 40s, 40s on the base, because the failure logging is now awaited before the error propagates (Decision 3, commit f1407ac); shipped on purpose so the error never outruns its log
  • Async failure and success callbacks now run on the serving loop's thread instead of a helper thread with a private loop: the callback rows show ThreadPoolExecutor-N_0 on Before and MainThread on After and Merged for every async hook. This is the fix itself (commit a2ac961); a callback bound to the serving loop now works, and one that blocks its thread blocks a loop it already blocked before
  • A raising success deployment hook or logging task is now logged: LiteLLM:ERROR: streaming_iterator.py:169 - Responses stream post-call success hook failed: ... appears once on After and Merged for the raising-guardrail scenario and never on Before, which swallowed it (commit a2ac961, Decision 5)

Regression risk (dependents this run did not reach):

  • The sync iterator with no running loop at all (a plain script calling litellm.responses(stream=True)): its code path is the base's, unchanged by the diff, but no leg drove it; it takes a scripted SDK run outside asyncio with the relay cut armed
  • Consumer cancellation during the await: a client that disconnects while its failure logging is being awaited leaves the task running (the strong-reference set keeps it), but no leg cut the client during the 20s flush; it takes closing the client socket mid-hold and reading the stand-in's log for the event
  • The mock and cached streaming iterators inherit the success hook scheduling and never await it, so their hook is now fire-and-forget where it blocked before; not driven
  • Router cooldown from a mid-stream failure was not read back after the cut; the early-cut scenario did complete through the fallback deployment on every leg, with the same spend row model on each

Dependency graph (dependents of _handle_failure, _run_post_success_hooks, ResponsesAPIStreamingIterator.__anext__, __next__, and the new task helpers):

  • /v1/responses streaming through the proxy (mid-stream cut, early cut with fallback, clean EOF without a terminal event, healthy stream, healthy stream with a raising guardrail): verified-live, and tested by the five unit tests in tests/unit/responses/test_streaming_iterator.py
  • /v1/chat/completions through the Responses bridge (openai/responses/...), mid-stream cut: verified-live (QA case 4 as well)
  • /v1/messages through the Responses bridge, mid-stream cut: verified-live (QA case 5 as well)
  • Non-streaming /v1/responses: verified-live, unchanged
  • SDK litellm.aresponses(stream=True) under asyncio.run, exiting right after the failure and with a 3s settle: verified-live
  • SDK sync litellm.responses(stream=True) iterated inside a running loop, exiting right after, with a 3s settle and a legacy failure callback, and with only CustomLogger instances: verified-live
  • SDK sync iterator with no running loop: untested
  • Callback kinds (async CustomLogger, sync-only CustomLogger, legacy failure and success callables, guardrail post-call success deployment hook, the Datadog integration under flush_lock contention in QA cases 2 and 6): verified-live
  • Spend rows and request tags: verified-live
  • GET /health/liveliness on six keep-alive connections during a failure: verified-live (0 hung on After in every case; 3, 5, and 4 hung on Before in cases 1, 4, and 5)
  • Mock and cached streaming iterators: untested
  • Router deployment cooldown after a mid-stream failure: untested
  • Dashboard and API clients: unreachable, no response field or wire contract changed

Not verified:

  • Outbound request bodies beyond their size: the relay recorded the request line, byte count, and cut mode, not the body key set or values
  • Hostile-input cells: the diff reads no new header or body field, so none apply
  • The real Datadog intake: every leg used a local stand-in that holds each 202 for 20s, which is what reproduces the reported freeze; nothing was read back from Datadog itself
  • The bridge paths' callback rows: the recorder keys rows by a marker inside input or messages, and the bridge kwargs carry neither on any leg, so the chat and messages bridge scenarios were compared on client verdict, spend rows, and relay counts only
  • The sync no-loop iterator, the mock and cached iterators, router cooldown, and a mid-await client disconnect, as listed under regression risk

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

  • 0be4a64 passes /live-pr-risk


Note

Medium Risk
Changes async hook scheduling and error propagation timing on the proxy’s hot /v1/responses streaming path; incorrect ordering or awaiting could regress logging, fallbacks, or health under load.

Overview
Fixes LIT-8678, where mid-stream /v1/responses failures could block the proxy event loop because failure and post-success deployment hooks ran via run_async_function on a helper thread while the iterating loop waited.

When asyncio.get_running_loop() is active, those hooks are create_task'd on that loop, tracked on the iterator, and the async ResponsesAPIStreamingIterator awaits them in __anext__ before the error propagates. Async failure logging still runs before sync failure_handler (executor), and a single end_time is shared. The completed-stream success deployment hook uses the same scheduling; with no running loop, behavior stays on the blocking path.

Unit tests assert hooks run on the test loop, failure attribution finishes before the consumer sees the error (router fallback), and sync-only / ordering guarantees when the sync iterator runs under a loop.

Reviewed by Cursor Bugbot for commit 0be4a64. Bugbot is set up for automated code reviews on this repo. Configure here.

… loop instead of blocking it

A dropped provider stream on native /v1/responses ran the failure logging through run_async_function from inside the async iterator, which parks the event loop thread on a helper-thread future until every failure callback returns, and never returns when a callback waits on state only that loop can advance. With a running loop the failure handlers (and the completed-stream success deployment hook) are now scheduled as tasks on it, the way chat streaming already does; the sync iterator keeps its blocking path
@devin-ai-integration

devin-ai-integration Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Changes how stream completion hooks run in the iterator.

The PR appears safe to merge based on the current changes and review history

Findings

  1. P1 Logging delays stream termination ▶

Summary

The PR moves Responses stream failure logging and the completed-stream deployment hook onto the iterating event loop, then waits for failure logging before propagating async stream errors. The latest changes sequence sync failure callbacks after async logging and add an ordering test

Reviews (5) · Last reviewed commit: "fix(responses): submit the sync failure ..."

Comment thread litellm/responses/streaming_iterator.py
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_responses_stream_failure_handlers_on_loop (0be4a64) with main (d08746f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (4eb340a) during the generation of this report, so d08746f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

…ting loop before propagating

Keep the merge-base hook set for the native Responses stream: async_failure_handler plus the executor-thread failure_handler on failure, and the post-call success deployment hook on completion. Inside a running loop the async handler is scheduled as a task on that loop and the async iterator awaits it before re-raising, so the loop is never blocked on a foreign-loop future and the failure is attributed before the router's fallback wrapper re-enters the same logging object. The sync iterator inside a running loop keeps the task fire-and-forget with a strong reference.
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 26, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/responses/streaming_iterator.py
Comment on lines +1035 to +1037
except Exception:
await self._await_pending_logging()
raise

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.

P1 Logging delays stream termination If a logging callback is slow or stalls, this wait has no timeout, so the client cannot receive a provider error and the router cannot start a mid-stream fallback until logging finishes. It also catches normal StopAsyncIteration, making a successful stream wait for its success hook before ending. The event loop remains responsive, but the affected client can still be left waiting.

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.

Base blocked the whole loop for the same duration, so this is parity. Awaiting also keeps failed-deployment attribution correct during router fallbacks

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.

You're right. The important distinction is that this await yields to the iterating event loop; it no longer blocks the loop as the old helper-thread path did.

Waiting before propagating the exception is intentional: it preserves the existing ordering and ensures the primary deployment's failure is logged before router fallback can reuse or mutate the logging object. The added regression test also codifies that attribution requirement.

This can still delay the affected consumer if a callback never completes, but that is a per-stream delay rather than a proxy-wide event-loop stall. I’m withdrawing the original finding.

Comment thread litellm/responses/streaming_iterator.py Fixed
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 26, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

1 similar comment
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0be4a64. Configure here.

@mateo-berri mateo-berri 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.

LGTM

@mateo-berri
mateo-berri merged commit 5ac640e into main Sep 26, 2026
148 of 158 checks passed
@mateo-berri
mateo-berri deleted the litellm_responses_stream_failure_handlers_on_loop branch September 26, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants