Skip to content

fix(router): hold Responses lifecycle events until output so a pre-output fallback announces one response - #43238

Merged
mateo-berri merged 4 commits into
mainfrom
litellm_responses_fallback_single_created
Sep 26, 2026
Merged

mateo-berri merged 4 commits into
mainfrom
litellm_responses_fallback_single_created

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A pre-output fallback on /v1/responses streams two response.created events with different ids
  • The first announced id never completes and sequence numbers restart at 0 mid-stream

How it solves it:

  • Hold response.created, response.in_progress, and response.queued until the first output event
  • Drop the held events once a fallback stream yields its first event, flush them on any other exit, a fallback that dies before its first event included
  • Cap the hold at 200 events, mirroring the /v1/messages fallback wrapper

Intentional product change: on a native /v1/responses stream, response.created and response.in_progress now arrive together with the first output event instead of a few hundred milliseconds before it. Clients lose the early announcement, never the events, and time to first token is unchanged

User Flow

Before: a developer streaming /v1/responses through a proxy with fallbacks sees response.created twice, and the first id never completes

  1. The admin lists primary and backup with router_settings.fallbacks: [{primary: [backup]}] and restarts the proxy
  2. The developer sends POST https://litellm-domain/v1/responses with "stream": true through openai-python and iterates the events
  3. primary streams response.created (id A, seq 0) and response.in_progress (seq 1), then fails before any output
  4. The proxy retries on backup and the client receives its response.created (id B, seq 0 again), response.in_progress, the output events, and response.completed (id B)
  5. The client has seen two response.created events with two ids, sequence numbers that restart at 0, and an id A that never completes

After: the same developer sees exactly one response.created, the one whose id completes

  1. The admin lists primary and backup with router_settings.fallbacks: [{primary: [backup]}] and restarts the proxy
  2. The developer sends POST https://litellm-domain/v1/responses with "stream": true through openai-python and iterates the events
  3. primary streams response.created and response.in_progress, then fails before any output; nothing has reached the client yet
  4. The proxy retries on backup and the client receives one response.created (id B, seq 0), one response.in_progress (seq 1), the output events, and response.completed (id B), with contiguous sequence numbers

Design decisions

The router's /v1/responses fallback wrapper now holds the primary stream's pre-output lifecycle events (response.created, response.in_progress, response.queued, the same boundary litellm/responses/streaming_iterator.py already draws, so its _PRE_OUTPUT_LIFECYCLE_EVENT_TYPES constant is made public and shared instead of copied) until the first output-bearing event and forwards them then. A mid-stream fallback error before that point drops them once the fallback stream yields its first event, so the client only ever sees the lifecycle events of the response that completes. A fallback that fails before yielding anything announced no response of its own, so the held pair is replayed ahead of the error and the failure reaches the client under the id it belongs to, the same wire as when no fallback was attempted (case 7 of the proof, raised by Bugbot on the first tip). Three alternatives were weighed and rejected: forwarding both pairs as today (two ids, sequence numbers restarting at 0); forwarding the primary's pair and suppressing the fallback's (the client then holds an id that never completes and cannot chain through previous_response_id); and rewriting the fallback's ids to the primary's (the completed id would not match the response the provider stored, so previous_response_id and GET /v1/responses/{id} would break)

The cost of the chosen option is the delay named under the TLDR. Every non-fallback exit keeps the wire unchanged: when no fallback lands (fallbacks disabled, every fallback failing, or a non-fallback error) the held events are flushed before the error propagates, a stream that ends without output flushes them at the end, and a stream that already produced output holds nothing, so a later error is never replayed (case 4 of the proof). The hold is capped at MAX_HELD_PRE_OUTPUT_RESPONSES_EVENTS = 200, mirroring MAX_BUFFERED_PRE_CONTENT_ANTHROPIC_CHUNKS on the /v1/messages fallback wrapper, which is the precedent this follows; past the cap the events commit and a later fallback replays a pair again, far above the three lifecycle events a provider sends. The fallback attempt moved into _aresponses_fallback_attempt, the same split the Anthropic wrapper already has, to keep _aresponses_streaming_iterator under the C901 budget, and the fallback stream is wrapped in contextlib.aclosing so its close is deterministic when the consumer stops early. The wrapper's two close guards catch Exception rather than BaseException, so a cancellation raised while a stream is being closed propagates instead of being logged and swallowed. No open PR was worth building on: #42958 and #35018 touch neither the router's responses fallback wrapper nor this symptom

Linear ticket

Resolves LIT-8625

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)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Both legs run the same rig and differ only in the commit the proxy was booted from. The upstream is a fault-injecting chunked SSE server (fault_upstream.py on 127.0.0.1:42838, raw asyncio, no framework) that picks its behavior from the request's model name: failed-event sends response.created and response.in_progress and then event: response.failed with a server_error; drop-before-output sends those two events and closes the socket mid chunked body; abort-before-output sends them and then a TCP RST; drop-after-partial sends them plus two text deltas and then closes the socket mid body; healthy streams a full answer ending in response.completed; close-before-any-event answers 200 with SSE headers and closes the socket before its first frame. backup is a real openai/gpt-5.4-mini deployment, so every fallback that fires spends real money on OpenAI; backup-empty is the zero-frame mode, standing in for a fallback deployment that dies before it announces anything

Proxy config shared by both legs (config.yaml):

model_list:
  - model_name: backup
    litellm_params:
      model: openai/gpt-5.4-mini
      api_key: os.environ/OPENAI_API_KEY
      timeout: 120
  - model_name: primary-failed-event
    litellm_params:
      model: openai/failed-event
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: primary-drop
    litellm_params:
      model: openai/drop-before-output
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: primary-abort
    litellm_params:
      model: openai/abort-before-output
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: primary-drop-after-partial
    litellm_params:
      model: openai/drop-after-partial
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: primary-healthy
    litellm_params:
      model: openai/healthy
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: backup-empty
    litellm_params:
      model: openai/close-before-any-event
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
  - model_name: primary-drop-to-empty-backup
    litellm_params:
      model: openai/drop-before-output
      api_base: http://127.0.0.1:42838/v1
      api_key: fault
router_settings:
  num_retries: 0
  fallbacks:
    - primary-failed-event: [backup]
    - primary-drop: [backup]
    - primary-abort: [backup]
    - primary-drop-after-partial: [backup]
    - primary-drop-to-empty-backup: [backup-empty]
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

Each leg boots its own worktree and venv with python litellm/proxy/proxy_cli.py --config config.yaml --port <random free port> --num_workers 2. Cases 1 to 5 and 7 run the customer's client, openai-python 2.33.0 (client_stream.py: OpenAI(base_url=<proxy>, api_key=$LITELLM_MASTER_KEY, max_retries=0, timeout=120).responses.create(model=<name>, input="Say hello in five words.", stream=True), printing every event with its sequence number, the response id on lifecycle events, and a final RESULT: line saying how the iteration ended). Case 6 sends the same request over raw curl and pulls the event type, response id, and sequence number out of every SSE event. Case 7's before leg was captured on the /live-pr-risk rig, the same config with a request recorder in front of backup, which that case never reaches

Before (8b68c3c)

Proxy booted from 8b68c3cd09 on port 20640 with --num_workers 2

1. primary-failed-event: provider sends response.failed before any output (the fallback path that predates PR #43133)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-failed-event
    
  2. Observed output

    openai-python 2.33.0; model=primary-failed-event
    +  0.47s seq=0 response.created id=resp_E9n9ZSMUT3koJst4b_ZHWbJtODSt1ZSPX46RGnVaFzEeNk_l3ryXeqSpDb3tfusnSP_afLBvNikVXYEXUqv2qKtICCQ_7updlM-QHnh-2NqJ906HelGJYhAg5RySHXJLjLTN6Tgw7PPAyLyQaoq_VQKxssEb3E6dSpBdkIYfLcgXQlOZJwoSgq_paXnQdYmKKnosP_Evir1YyspXHjfep58unvaO_lRppKhfFn-goF6p4BoZ9GXLLSbbre_oNpAKP9gWn03VassdD48V2Cb9vxVzOpaZwVg-USx_FBzeOxvLbRn-g5Z55NPC0amZV0xsXmMrlAKhXJz-Ejf-dxiTbyNGESthZX2HV_Dg55p8Vm4ogI4L1ZjxbKb_OlAdhx2sUsgnVYYH2yk274LTOpw=
    +  0.47s seq=1 response.in_progress id=resp_E9n9ZSMUT3koJst4b_ZHWbJtODSt1ZSPX46RGnVaFzEeNk_l3ryXeqSpDb3tfusnSP_afLBvNikVXYEXUqv2qKtICCQ_7updlM-QHnh-2NqJ906HelGJYhAg5RySHXJLjLTN6Tgw7PPAyLyQaoq_VQKxssEb3E6dSpBdkIYfLcgXQlOZJwoSgq_paXnQdYmKKnosP_Evir1YyspXHjfep58unvaO_lRppKhfFn-goF6p4BoZ9GXLLSbbre_oNpAKP9gWn03VassdD48V2Cb9vxVzOpaZwVg-USx_FBzeOxvLbRn-g5Z55NPC0amZV0xsXmMrlAKhXJz-Ejf-dxiTbyNGESthZX2HV_Dg55p8Vm4ogI4L1ZjxbKb_OlAdhx2sUsgnVYYH2yk274LTOpw=
    +  0.81s seq=0 response.created id=resp_VwJju5wFeL8ZqfZumlBcQWZfVbth1i8z_DqlmdeHz2EVglgy15KnuPQQHKmP1k0a2_A7EoICnZcKcC6Jut2LDT7rQ5KKu1nDZkKCsRY9jaciJRAmwSgyZOKBPRAw7-z8-VV9Z5uybD3FeqvYNH1HjDe56LTyBQ8xNitt6p9luS2yFocz4jGYifHbQHiXDxwMWpD40tIWgIvx9L0a8oWE_Crax-W9PoCQdnchhrJI7tN2GCwDYVVdIBHbZ3GTMYTNNTMO_Ki-zQeO3pS7m6pomA4mrW5xZ2yCjHjDtkJGewMxBeSAC35wtiVYUeL77-tkJk2fQmUhIy5O0zU1OrzSIlh89p3-DWNJBXEC6RzzLgzWxs-u2QBJFGfAodruxIJrQR0NeZRr90w7ip1tQ1TMg5tqd6Ppdq_fQo1K2sC_rfz-D27Kn8difWegqNYd0EY6wlYiK_mZUAbmqhpsoYixZjRT
    +  0.81s seq=1 response.in_progress id=resp_VwJju5wFeL8ZqfZumlBcQWZfVbth1i8z_DqlmdeHz2EVglgy15KnuPQQHKmP1k0a2_A7EoICnZcKcC6Jut2LDT7rQ5KKu1nDZkKCsRY9jaciJRAmwSgyZOKBPRAw7-z8-VV9Z5uybD3FeqvYNH1HjDe56LTyBQ8xNitt6p9luS2yFocz4jGYifHbQHiXDxwMWpD40tIWgIvx9L0a8oWE_Crax-W9PoCQdnchhrJI7tN2GCwDYVVdIBHbZ3GTMYTNNTMO_Ki-zQeO3pS7m6pomA4mrW5xZ2yCjHjDtkJGewMxBeSAC35wtiVYUeL77-tkJk2fQmUhIy5O0zU1OrzSIlh89p3-DWNJBXEC6RzzLgzWxs-u2QBJFGfAodruxIJrQR0NeZRr90w7ip1tQ1TMg5tqd6Ppdq_fQo1K2sC_rfz-D27Kn8difWegqNYd0EY6wlYiK_mZUAbmqhpsoYixZjRT
    +  1.24s seq=2 response.output_item.added
    +  1.24s seq=3 response.content_part.added
    +  1.24s seq=4 response.output_text.delta delta='Hello'
    +  1.24s seq=5 response.output_text.delta delta=' there'
    +  1.37s seq=6 response.output_text.delta delta=','
    +  1.37s seq=7 response.output_text.delta delta=' nice'
    +  1.37s seq=8 response.output_text.delta delta=' to'
    +  1.37s seq=9 response.output_text.delta delta=' meet'
    +  1.37s seq=10 response.output_text.delta delta=' you'
    +  1.37s seq=11 response.output_text.delta delta='.'
    +  1.40s seq=12 response.output_text.done
    +  1.41s seq=13 response.content_part.done
    +  1.41s seq=14 response.output_item.done
    +  1.52s seq=15 response.completed id=resp_VwJju5wFeL8ZqfZumlBcQWZfVbth1i8z_DqlmdeHz2EVglgy15KnuPQQHKmP1k0a2_A7EoICnZcKcC6Jut2LDT7rQ5KKu1nDZkKCsRY9jaciJRAmwSgyZOKBPRAw7-z8-VV9Z5uybD3FeqvYNH1HjDe56LTyBQ8xNitt6p9luS2yFocz4jGYifHbQHiXDxwMWpD40tIWgIvx9L0a8oWE_Crax-W9PoCQdnchhrJI7tN2GCwDYVVdIBHbZ3GTMYTNNTMO_Ki-zQeO3pS7m6pomA4mrW5xZ2yCjHjDtkJGewMxBeSAC35wtiVYUeL77-tkJk2fQmUhIy5O0zU1OrzSIlh89p3-DWNJBXEC6RzzLgzWxs-u2QBJFGfAodruxIJrQR0NeZRr90w7ip1tQ1TMg5tqd6Ppdq_fQo1K2sC_rfz-D27Kn8difWegqNYd0EY6wlYiK_mZUAbmqhpsoYixZjRT status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text='Hello there, nice to meet you.'
    RESULT: iteration ended normally after 18 events at +1.57s; text so far='Hello there, nice to meet you.'
    

2. primary-drop: socket closed after response.in_progress, before any output

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-drop
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop
    +  0.31s seq=0 response.created id=resp_ovNYU-lwDu05BzqGqL2fDTfnByBVVSEokbZVIAz2teJ8BRXvruzm6oxtzJcktp2nnDoKYorTMo3szptPGyI4OColigadOeD9VmR47aHT_puFluoZKzpeFh6q2mr1iPxYl2PF7JpnZFBvCmsjZkneVPOaereRuMKF14i9EqQKWYiKMLAgsECb4BgM04gAM73uSDRIIFQ0WVPCXyEycOR0c5c19jDBmOKpN7LlWVvGtvS5TTsFDuZnH68_yjg84JeMS2AtRZLpsyAVjc3x7405av3LYcj362Y5Kp_OHAGvz3mV7SP5oN0Ne3P8mbF2G2PAY3VSuhNNT-iNJzg5Yd1vxjBr_0g5Q89nGZuiftw7E-2-edcZCiBc8ErqD-8mQShjpimBNVgPvL-pqRfODLA=
    +  0.31s seq=1 response.in_progress id=resp_ovNYU-lwDu05BzqGqL2fDTfnByBVVSEokbZVIAz2teJ8BRXvruzm6oxtzJcktp2nnDoKYorTMo3szptPGyI4OColigadOeD9VmR47aHT_puFluoZKzpeFh6q2mr1iPxYl2PF7JpnZFBvCmsjZkneVPOaereRuMKF14i9EqQKWYiKMLAgsECb4BgM04gAM73uSDRIIFQ0WVPCXyEycOR0c5c19jDBmOKpN7LlWVvGtvS5TTsFDuZnH68_yjg84JeMS2AtRZLpsyAVjc3x7405av3LYcj362Y5Kp_OHAGvz3mV7SP5oN0Ne3P8mbF2G2PAY3VSuhNNT-iNJzg5Yd1vxjBr_0g5Q89nGZuiftw7E-2-edcZCiBc8ErqD-8mQShjpimBNVgPvL-pqRfODLA=
    +  0.68s seq=0 response.created id=resp_bkMGxoYeHWxYpGlpcUQfHOSAUDdzcDGZpneApZjSMY8q_UjTl7PBuPds8JO3ClWaVNLY3Hibs6XD3-oL-bXfBvY0u63rIFoWN3ZWmOu1ng0vZKdi0bqmRVGSO__JEMm8rNjlOFFxlLrN0u_1vl0q_jNJPrXCIne4mSWiBKi0haGlKcFZ5Zl1hF2Jhs2KXbuytNog_DwIT26VjrNhbQRJ-fpOwudBMz1gbRPD9ekpqMNTo1VB8sHsLjh55KFU1IcDaQXilWIOJn8Ps6R-yrMnS7naaysHmeOyanj05CB4h0Y__9Cl3rrTdhsRfKCjhcAQmTSafy9_DKdAmv3F-C4r_D6DHL-sopnmLoXkFpdqHn4VVVTYzaeoB1rDib9FA2ywSG3ullhvckE8zyD0OBmovQNfGcGP4atNOKmjD-6B1uiIT_GgcyzWfZc3pj6nK_p_65sW_wOyHiGkiZhIuVQXoa_s
    +  0.68s seq=1 response.in_progress id=resp_bkMGxoYeHWxYpGlpcUQfHOSAUDdzcDGZpneApZjSMY8q_UjTl7PBuPds8JO3ClWaVNLY3Hibs6XD3-oL-bXfBvY0u63rIFoWN3ZWmOu1ng0vZKdi0bqmRVGSO__JEMm8rNjlOFFxlLrN0u_1vl0q_jNJPrXCIne4mSWiBKi0haGlKcFZ5Zl1hF2Jhs2KXbuytNog_DwIT26VjrNhbQRJ-fpOwudBMz1gbRPD9ekpqMNTo1VB8sHsLjh55KFU1IcDaQXilWIOJn8Ps6R-yrMnS7naaysHmeOyanj05CB4h0Y__9Cl3rrTdhsRfKCjhcAQmTSafy9_DKdAmv3F-C4r_D6DHL-sopnmLoXkFpdqHn4VVVTYzaeoB1rDib9FA2ywSG3ullhvckE8zyD0OBmovQNfGcGP4atNOKmjD-6B1uiIT_GgcyzWfZc3pj6nK_p_65sW_wOyHiGkiZhIuVQXoa_s
    +  1.03s seq=2 response.output_item.added
    +  1.03s seq=3 response.content_part.added
    +  1.03s seq=4 response.output_text.delta delta='Hello'
    +  1.03s seq=5 response.output_text.delta delta=' from'
    +  1.04s seq=6 response.output_text.delta delta=' this'
    +  1.06s seq=7 response.output_text.delta delta=' friendly'
    +  1.07s seq=8 response.output_text.delta delta=' assistant'
    +  1.07s seq=9 response.output_text.delta delta='.'
    +  1.15s seq=10 response.output_text.done
    +  1.16s seq=11 response.content_part.done
    +  1.16s seq=12 response.output_item.done
    +  1.27s seq=13 response.completed id=resp_bkMGxoYeHWxYpGlpcUQfHOSAUDdzcDGZpneApZjSMY8q_UjTl7PBuPds8JO3ClWaVNLY3Hibs6XD3-oL-bXfBvY0u63rIFoWN3ZWmOu1ng0vZKdi0bqmRVGSO__JEMm8rNjlOFFxlLrN0u_1vl0q_jNJPrXCIne4mSWiBKi0haGlKcFZ5Zl1hF2Jhs2KXbuytNog_DwIT26VjrNhbQRJ-fpOwudBMz1gbRPD9ekpqMNTo1VB8sHsLjh55KFU1IcDaQXilWIOJn8Ps6R-yrMnS7naaysHmeOyanj05CB4h0Y__9Cl3rrTdhsRfKCjhcAQmTSafy9_DKdAmv3F-C4r_D6DHL-sopnmLoXkFpdqHn4VVVTYzaeoB1rDib9FA2ywSG3ullhvckE8zyD0OBmovQNfGcGP4atNOKmjD-6B1uiIT_GgcyzWfZc3pj6nK_p_65sW_wOyHiGkiZhIuVQXoa_s status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text='Hello from this friendly assistant.'
    RESULT: iteration ended normally after 16 events at +1.32s; text so far='Hello from this friendly assistant.'
    

3. primary-abort: TCP reset after response.in_progress, before any output

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-abort
    
  2. Observed output

    openai-python 2.33.0; model=primary-abort
    +  0.23s seq=0 response.created id=resp_dPLXhi7-jUELl1eKRWHP3lhOFwdZJV0k1tlGRIMW9ibPeUCTA53FFF0BltJyLA5r43Ht4WVc5JWLtoD_Gn4dUY8T5mcZrLaJxtbKPFHvJrwmRXmxqRdBkYrcqtQLj0sWzzGJAVizDkFQPzHcQ7LmybaXLBwCxfKs8HW5rtPP5klLKRToqU2c7IUEwzVPiIwIweL3NEyMTqkz1l5LqNfp0VGnE3KNwI9oYfwO08kWUBq4qSk-ib1PapyJR9k-CntwGcPcC7L3r4A0qTCWjzxMMXg9QBmaOWrYnhtIkybUULGWgrSvK4XPbdHV7a4pcsWwDYJkp36Lx_SJAX0j3OzSGYX4nv4wShQjzML_UUlEfJrga3PY-n79267VAgOGxO1k-ayYow61s99Pwv-6OLM=
    +  0.23s seq=1 response.in_progress id=resp_dPLXhi7-jUELl1eKRWHP3lhOFwdZJV0k1tlGRIMW9ibPeUCTA53FFF0BltJyLA5r43Ht4WVc5JWLtoD_Gn4dUY8T5mcZrLaJxtbKPFHvJrwmRXmxqRdBkYrcqtQLj0sWzzGJAVizDkFQPzHcQ7LmybaXLBwCxfKs8HW5rtPP5klLKRToqU2c7IUEwzVPiIwIweL3NEyMTqkz1l5LqNfp0VGnE3KNwI9oYfwO08kWUBq4qSk-ib1PapyJR9k-CntwGcPcC7L3r4A0qTCWjzxMMXg9QBmaOWrYnhtIkybUULGWgrSvK4XPbdHV7a4pcsWwDYJkp36Lx_SJAX0j3OzSGYX4nv4wShQjzML_UUlEfJrga3PY-n79267VAgOGxO1k-ayYow61s99Pwv-6OLM=
    +  0.57s seq=0 response.created id=resp_JZZDANkIXC4HTFInP2Ki358-G1RBs1c9ymKnms2lDxCayapSzrXWi5kpADDsVXbP9LN7hxI53GU-zom77qhbtnakakRDUTjWD5IS6A_KNuUfpZfZfeaFqm_kYOtT487V81Zbr9zbK0xk_-5wVMWUo3691y9nUZkIbwtDFRbhLu0Voo_uqq8ikm4l48FqmP1Z9ARhD_pwVf9k7mR9b9ZjcfDe2eSHI4UZeF_izjKAU2Goth_ibqcMZz8lXBrs8u1GBWxqOlazR3h7t3mkz1GTMGyKX0d2yl8foo-rpVr-QR_-qXtam0C1jBRuhGgwJ05KOBoznCQO75PbCijGT-SvQt1YKTll-u5d66n6RpFvRV_XFu9_AC0zgzCqdJntQzVvTHyHUCv0pG1FD342SmLprvVTcismeAzP1sPPDcldo0rsyQ8o_DCVNZPzTT4wNTFhZ3Xz2j-FQMRye5zjDCJx9sbN
    +  0.57s seq=1 response.in_progress id=resp_JZZDANkIXC4HTFInP2Ki358-G1RBs1c9ymKnms2lDxCayapSzrXWi5kpADDsVXbP9LN7hxI53GU-zom77qhbtnakakRDUTjWD5IS6A_KNuUfpZfZfeaFqm_kYOtT487V81Zbr9zbK0xk_-5wVMWUo3691y9nUZkIbwtDFRbhLu0Voo_uqq8ikm4l48FqmP1Z9ARhD_pwVf9k7mR9b9ZjcfDe2eSHI4UZeF_izjKAU2Goth_ibqcMZz8lXBrs8u1GBWxqOlazR3h7t3mkz1GTMGyKX0d2yl8foo-rpVr-QR_-qXtam0C1jBRuhGgwJ05KOBoznCQO75PbCijGT-SvQt1YKTll-u5d66n6RpFvRV_XFu9_AC0zgzCqdJntQzVvTHyHUCv0pG1FD342SmLprvVTcismeAzP1sPPDcldo0rsyQ8o_DCVNZPzTT4wNTFhZ3Xz2j-FQMRye5zjDCJx9sbN
    +  0.97s seq=2 response.output_item.added
    +  0.97s seq=3 response.content_part.added
    +  0.97s seq=4 response.output_text.delta delta='Hello'
    +  0.97s seq=5 response.output_text.delta delta=' there'
    +  0.98s seq=6 response.output_text.delta delta=','
    +  0.98s seq=7 response.output_text.delta delta=' hope'
    +  1.00s seq=8 response.output_text.delta delta=" you're"
    +  1.00s seq=9 response.output_text.delta delta=' well'
    +  1.05s seq=10 response.output_text.delta delta='.'
    +  1.24s seq=11 response.output_text.done
    +  1.24s seq=12 response.content_part.done
    +  1.24s seq=13 response.output_item.done
    +  1.35s seq=14 response.completed id=resp_JZZDANkIXC4HTFInP2Ki358-G1RBs1c9ymKnms2lDxCayapSzrXWi5kpADDsVXbP9LN7hxI53GU-zom77qhbtnakakRDUTjWD5IS6A_KNuUfpZfZfeaFqm_kYOtT487V81Zbr9zbK0xk_-5wVMWUo3691y9nUZkIbwtDFRbhLu0Voo_uqq8ikm4l48FqmP1Z9ARhD_pwVf9k7mR9b9ZjcfDe2eSHI4UZeF_izjKAU2Goth_ibqcMZz8lXBrs8u1GBWxqOlazR3h7t3mkz1GTMGyKX0d2yl8foo-rpVr-QR_-qXtam0C1jBRuhGgwJ05KOBoznCQO75PbCijGT-SvQt1YKTll-u5d66n6RpFvRV_XFu9_AC0zgzCqdJntQzVvTHyHUCv0pG1FD342SmLprvVTcismeAzP1sPPDcldo0rsyQ8o_DCVNZPzTT4wNTFhZ3Xz2j-FQMRye5zjDCJx9sbN status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text="Hello there, hope you're well."
    RESULT: iteration ended normally after 17 events at +1.41s; text so far="Hello there, hope you're well."
    

4. primary-drop-after-partial: two text deltas, then the socket closes (guard: nothing is replayed once output started)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-drop-after-partial
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop-after-partial
    +  0.31s seq=0 response.created id=resp_C0Pii2k8oVKHuV2sjsj5wg6wzUDjjH7j9lw0FC_1OYLvdd1y_ajArPyxl8A92iloa1Zo80ymtVDYkungyElqxszGUKjKjiXLqZjDfTpC7F-Wsep4hFzhIPUfGOCSMuh_yt2wHi3Sn_xTePxd3UkcjELjy_R0YtDJF6UNnvOqIVI4AfmnW_ip9ZpQyhx323zlL0iJsVt0BAPJsbG2uI35R6IAffTa8C4DsgpuWb3o1Nd-6cMLTZcPiYJDyggjf8YjvIglV0mJWG14uLtMTxlI8iXUjwYGH_O1hzQgmnmANU67WbXXoKM_VweWVyfhKy5fj13nVU4q9jM4947LMpqRDw8J--EOqskTuVMfrS9cvLwjQUNy6paGhoXRvIEvigfaa__XrQdGThckxAsr-VE=
    +  0.31s seq=1 response.in_progress id=resp_C0Pii2k8oVKHuV2sjsj5wg6wzUDjjH7j9lw0FC_1OYLvdd1y_ajArPyxl8A92iloa1Zo80ymtVDYkungyElqxszGUKjKjiXLqZjDfTpC7F-Wsep4hFzhIPUfGOCSMuh_yt2wHi3Sn_xTePxd3UkcjELjy_R0YtDJF6UNnvOqIVI4AfmnW_ip9ZpQyhx323zlL0iJsVt0BAPJsbG2uI35R6IAffTa8C4DsgpuWb3o1Nd-6cMLTZcPiYJDyggjf8YjvIglV0mJWG14uLtMTxlI8iXUjwYGH_O1hzQgmnmANU67WbXXoKM_VweWVyfhKy5fj13nVU4q9jM4947LMpqRDw8J--EOqskTuVMfrS9cvLwjQUNy6paGhoXRvIEvigfaa__XrQdGThckxAsr-VE=
    +  0.31s seq=2 response.output_item.added
    +  0.31s seq=3 response.content_part.added
    +  0.31s seq=4 response.output_text.delta delta='The first half of the answer, '
    +  0.31s seq=5 response.output_text.delta delta='and then the upstream '
    +  0.36s seq=6 response.failed id=resp_C0Pii2k8oVKHuV2sjsj5wg6wzUDjjH7j9lw0FC_1OYLvdd1y_ajArPyxl8A92iloa1Zo80ymtVDYkungyElqxszGUKjKjiXLqZjDfTpC7F-Wsep4hFzhIPUfGOCSMuh_yt2wHi3Sn_xTePxd3UkcjELjy_R0YtDJF6UNnvOqIVI4AfmnW_ip9ZpQyhx323zlL0iJsVt0BAPJsbG2uI35R6IAffTa8C4DsgpuWb3o1Nd-6cMLTZcPiYJDyggjf8YjvIglV0mJWG14uLtMTxlI8iXUjwYGH_O1hzQgmnmANU67WbXXoKM_VweWVyfhKy5fj13nVU4q9jM4947LMpqRDw8J--EOqskTuVMfrS9cvLwjQUNy6paGhoXRvIEvigfaa__XrQdGThckxAsr-VE= status=failed model=fault-model error=ResponseError(code='server_error', message="Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")
    RESULT: iteration ended normally after 7 events at +0.36s; text so far='The first half of the answer, and then the upstream '
    

5. primary-healthy: full stream, no fallback (control)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-healthy
    
  2. Observed output

    openai-python 2.33.0; model=primary-healthy
    +  0.24s seq=0 response.created id=resp_9mPcYG6H4t_Ss3VFd0JtQ3gChTSio4LRpmQevznpzicDq0wAoRDwNQm2nRJpeM_Rp852SCyyd9uvh5pY5ZYJCUSfqbWcVMjIVYZ3AvRuB_OIWiZ-BdN4HG1NF_ph8DDWQLq7piRLzAOglBNpiIsYsEgbfbFw1ynY9XRmwqGyWlgsXBjpAb6jSbAWszxafrSXmpD_WyfkmkVh40vRZbJhKGSNGVvfnLDgDuQ5ZVyKuAkGEN_U5437OQ8_Ryh1e3-SYsTo3OgTDNR8siAVXhUdbJd9a82lbiVMOGpfznVelnr7G5186xvb0UvEvySQnSMRw3sY04q6dSg95htlfK6zv1LlDlHCGzUUzWnBd1V12waWxvNMNFdQMIHbgHHbeOY1nX5FtNjbOhp6rBvEfO8=
    +  0.24s seq=1 response.in_progress id=resp_9mPcYG6H4t_Ss3VFd0JtQ3gChTSio4LRpmQevznpzicDq0wAoRDwNQm2nRJpeM_Rp852SCyyd9uvh5pY5ZYJCUSfqbWcVMjIVYZ3AvRuB_OIWiZ-BdN4HG1NF_ph8DDWQLq7piRLzAOglBNpiIsYsEgbfbFw1ynY9XRmwqGyWlgsXBjpAb6jSbAWszxafrSXmpD_WyfkmkVh40vRZbJhKGSNGVvfnLDgDuQ5ZVyKuAkGEN_U5437OQ8_Ryh1e3-SYsTo3OgTDNR8siAVXhUdbJd9a82lbiVMOGpfznVelnr7G5186xvb0UvEvySQnSMRw3sY04q6dSg95htlfK6zv1LlDlHCGzUUzWnBd1V12waWxvNMNFdQMIHbgHHbeOY1nX5FtNjbOhp6rBvEfO8=
    +  0.24s seq=2 response.output_item.added
    +  0.24s seq=3 response.content_part.added
    +  0.24s seq=4 response.output_text.delta delta='The first half of the answer, '
    +  0.24s seq=5 response.output_text.delta delta='and then the upstream '
    +  0.24s seq=6 response.output_text.done
    +  0.24s seq=7 response.content_part.done
    +  0.25s seq=8 response.output_item.done
    +  0.26s seq=9 response.completed id=resp_9mPcYG6H4t_Ss3VFd0JtQ3gChTSio4LRpmQevznpzicDq0wAoRDwNQm2nRJpeM_Rp852SCyyd9uvh5pY5ZYJCUSfqbWcVMjIVYZ3AvRuB_OIWiZ-BdN4HG1NF_ph8DDWQLq7piRLzAOglBNpiIsYsEgbfbFw1ynY9XRmwqGyWlgsXBjpAb6jSbAWszxafrSXmpD_WyfkmkVh40vRZbJhKGSNGVvfnLDgDuQ5ZVyKuAkGEN_U5437OQ8_Ryh1e3-SYsTo3OgTDNR8siAVXhUdbJd9a82lbiVMOGpfznVelnr7G5186xvb0UvEvySQnSMRw3sY04q6dSg95htlfK6zv1LlDlHCGzUUzWnBd1V12waWxvNMNFdQMIHbgHHbeOY1nX5FtNjbOhp6rBvEfO8= status=completed model=fault-model error=None output_text='The first half of the answer, and then the upstream finished normally.'
    RESULT: iteration ended normally after 10 events at +0.26s; text so far='The first half of the answer, and then the upstream '
    

6. primary-drop over raw curl: event types, response ids, and sequence numbers pulled out of the SSE body

  1. Run the request

    curl -sN http://127.0.0.1:20640/v1/responses -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"model":"primary-drop","input":"Say hello in five words.","stream":true}' | grep '^data:' | grep -oE '"type":"response\.[a-z_.]+"|"response":\{"id":"resp_[^"]{0,12}|"sequence_number":[0-9]+' | awk '/^"type"/{if(l)print l; l=$0; next}{l=l" "$0} END{print l}'
    
  2. Observed output

    "type":"response.created" "response":{"id":"resp_P54K7X22i_yq "sequence_number":0
    "type":"response.in_progress" "response":{"id":"resp_P54K7X22i_yq "sequence_number":1
    "type":"response.created" "response":{"id":"resp_US7E_STz0EkQ "sequence_number":0
    "type":"response.in_progress" "response":{"id":"resp_US7E_STz0EkQ "sequence_number":1
    "type":"response.output_item.added" "sequence_number":2
    "type":"response.content_part.added" "sequence_number":3
    "type":"response.output_text.delta" "sequence_number":4
    "type":"response.output_text.delta" "sequence_number":5
    "type":"response.output_text.delta" "sequence_number":6
    "type":"response.output_text.delta" "sequence_number":7
    "type":"response.output_text.delta" "sequence_number":8
    "type":"response.output_text.delta" "sequence_number":9
    "type":"response.output_text.delta" "sequence_number":10
    "type":"response.output_text.done" "sequence_number":11
    "type":"response.content_part.done" "sequence_number":12
    "type":"response.output_item.done" "sequence_number":13
    "type":"response.completed" "response":{"id":"resp_US7E_STz0EkQ "sequence_number":14
    

7. primary-drop-to-empty-backup: socket closed before any output, and the fallback deployment closes its socket before its first event (guard: the failure is reported under the announced id)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:20640 primary-drop-to-empty-backup
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop-to-empty-backup
    +  0.63s seq=0 response.created id=resp_SVFFVecTjVq2…
    +  0.63s seq=1 response.in_progress id=resp_SVFFVecTjVq2…
    +  1.02s seq=2 response.failed id=resp_SVFFVecTjVq2… status=failed model=fault-model error=ResponseError(code='server_error', message="Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")
    RESULT: iteration ended normally after 3 events at +1.02s; text so far=''
    

After (11387cc)

Proxy booted from 11387ccfed on port 23595 with --num_workers 2

1. primary-failed-event: provider sends response.failed before any output (the fallback path that predates PR #43133)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-failed-event
    
  2. Observed output

    openai-python 2.33.0; model=primary-failed-event
    +  0.97s seq=0 response.created id=resp_q5Hto_aZsFt-edalvIo7pxAb11PskpZi0WjaFfCRq6NANpgvleH0jqcSb6ccBjSADTroY0_Z5mno_PJlmH153YvfbEK_rMEPNkqDJM-ViBi0XQRVQXIYTDGu4Q1Y-hRbNvVyoGmzRvIODiY4wQORNvUzy2AmIL0N0pZnwyL88q0gF2mSZtnM1sc-k04eXiCKaDj42vuY3FwEiTIYGJhmFLi6FXbcyqUjPRnFYY6ijs1qZvCR5jDQqPs-RX8GdVHbbDRJB8mTyqQQPqC9Jokvhj25yMlslhjHsZ1DDpbzqNfmfRQZAJUOr3YlP9_ETXJVMUeb9ylYL4ph205PH6F5ZsbtHZNTrVPczogUtw6MMSw4HAZLOLRvc5tQAL1_S9TLlBL_fm8poJ5NshOIx9cQ-sDL2ctScqKXx1f2HDrA1V20ZV7VskUHHhN67VUc-6ZjoS5Pa0aiZmkG5raldJRcTta4
    +  0.97s seq=1 response.in_progress id=resp_q5Hto_aZsFt-edalvIo7pxAb11PskpZi0WjaFfCRq6NANpgvleH0jqcSb6ccBjSADTroY0_Z5mno_PJlmH153YvfbEK_rMEPNkqDJM-ViBi0XQRVQXIYTDGu4Q1Y-hRbNvVyoGmzRvIODiY4wQORNvUzy2AmIL0N0pZnwyL88q0gF2mSZtnM1sc-k04eXiCKaDj42vuY3FwEiTIYGJhmFLi6FXbcyqUjPRnFYY6ijs1qZvCR5jDQqPs-RX8GdVHbbDRJB8mTyqQQPqC9Jokvhj25yMlslhjHsZ1DDpbzqNfmfRQZAJUOr3YlP9_ETXJVMUeb9ylYL4ph205PH6F5ZsbtHZNTrVPczogUtw6MMSw4HAZLOLRvc5tQAL1_S9TLlBL_fm8poJ5NshOIx9cQ-sDL2ctScqKXx1f2HDrA1V20ZV7VskUHHhN67VUc-6ZjoS5Pa0aiZmkG5raldJRcTta4
    +  0.97s seq=2 response.output_item.added
    +  0.97s seq=3 response.content_part.added
    +  0.97s seq=4 response.output_text.delta delta='Hello'
    +  0.97s seq=5 response.output_text.delta delta=' there'
    +  0.97s seq=6 response.output_text.delta delta=','
    +  0.97s seq=7 response.output_text.delta delta=' hope'
    +  0.97s seq=8 response.output_text.delta delta=' you'
    +  0.97s seq=9 response.output_text.delta delta='’re'
    +  0.97s seq=10 response.output_text.delta delta=' well'
    +  0.97s seq=11 response.output_text.delta delta='!'
    +  0.97s seq=12 response.output_text.done
    +  0.97s seq=13 response.content_part.done
    +  0.98s seq=14 response.output_item.done
    +  1.09s seq=15 response.completed id=resp_q5Hto_aZsFt-edalvIo7pxAb11PskpZi0WjaFfCRq6NANpgvleH0jqcSb6ccBjSADTroY0_Z5mno_PJlmH153YvfbEK_rMEPNkqDJM-ViBi0XQRVQXIYTDGu4Q1Y-hRbNvVyoGmzRvIODiY4wQORNvUzy2AmIL0N0pZnwyL88q0gF2mSZtnM1sc-k04eXiCKaDj42vuY3FwEiTIYGJhmFLi6FXbcyqUjPRnFYY6ijs1qZvCR5jDQqPs-RX8GdVHbbDRJB8mTyqQQPqC9Jokvhj25yMlslhjHsZ1DDpbzqNfmfRQZAJUOr3YlP9_ETXJVMUeb9ylYL4ph205PH6F5ZsbtHZNTrVPczogUtw6MMSw4HAZLOLRvc5tQAL1_S9TLlBL_fm8poJ5NshOIx9cQ-sDL2ctScqKXx1f2HDrA1V20ZV7VskUHHhN67VUc-6ZjoS5Pa0aiZmkG5raldJRcTta4 status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text='Hello there, hope you’re well!'
    RESULT: iteration ended normally after 16 events at +1.14s; text so far='Hello there, hope you’re well!'
    

2. primary-drop: socket closed after response.in_progress, before any output

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-drop
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop
    +  0.74s seq=0 response.created id=resp_8b0cl-SyubbbuGoRU40gZbhBiwv-BVAradz3NoGbbWgDa5XVmZiPIq96qBfUqZVgsSis6u004Y40O4emDmHB1Q2vRBTKLOsrXy3aNAxR-CV9Ur0RqCsVJTBc2GPjcD59G3H7jE3IAWlcZBuxPh05HKyUUhaD1O8My9ZHicFqU70cYdmEx1Yyiv71f7eV216inpOSs_3-vcDSFPgKw-2qFO6TCehbOk31wIlRtGiQcTP-B-Vi73QV6DTHIrFAKvo4Nqcsi5QtUFY8rzWgRp-KF4ILBrGrG6pYZ161f4flQYbS3xqpWGAccqZ-9xIXwo9ncz7JmwNWVbfAZFMR5NF9Ah4ZNJ5UdYZLaHfSn96dagpGrEdVr_akmlq1_jwxqyJFth9O1hND2DiNLoIum_rsB9UoYcKeSdkOJLa7L_xQi6LjqUrjuAhpW0J6C3FQw-JQXBHpwh--uoy-aPMetiqtAv_w
    +  0.74s seq=1 response.in_progress id=resp_8b0cl-SyubbbuGoRU40gZbhBiwv-BVAradz3NoGbbWgDa5XVmZiPIq96qBfUqZVgsSis6u004Y40O4emDmHB1Q2vRBTKLOsrXy3aNAxR-CV9Ur0RqCsVJTBc2GPjcD59G3H7jE3IAWlcZBuxPh05HKyUUhaD1O8My9ZHicFqU70cYdmEx1Yyiv71f7eV216inpOSs_3-vcDSFPgKw-2qFO6TCehbOk31wIlRtGiQcTP-B-Vi73QV6DTHIrFAKvo4Nqcsi5QtUFY8rzWgRp-KF4ILBrGrG6pYZ161f4flQYbS3xqpWGAccqZ-9xIXwo9ncz7JmwNWVbfAZFMR5NF9Ah4ZNJ5UdYZLaHfSn96dagpGrEdVr_akmlq1_jwxqyJFth9O1hND2DiNLoIum_rsB9UoYcKeSdkOJLa7L_xQi6LjqUrjuAhpW0J6C3FQw-JQXBHpwh--uoy-aPMetiqtAv_w
    +  0.74s seq=2 response.output_item.added
    +  0.74s seq=3 response.content_part.added
    +  0.74s seq=4 response.output_text.delta delta='Hello'
    +  0.74s seq=5 response.output_text.delta delta=' there'
    +  0.74s seq=6 response.output_text.delta delta=','
    +  0.76s seq=7 response.output_text.delta delta=' hope'
    +  0.77s seq=8 response.output_text.delta delta=" you're"
    +  0.78s seq=9 response.output_text.delta delta=' well'
    +  0.78s seq=10 response.output_text.delta delta='.'
    +  0.86s seq=11 response.output_text.done
    +  0.87s seq=12 response.content_part.done
    +  0.87s seq=13 response.output_item.done
    +  0.95s seq=14 response.completed id=resp_8b0cl-SyubbbuGoRU40gZbhBiwv-BVAradz3NoGbbWgDa5XVmZiPIq96qBfUqZVgsSis6u004Y40O4emDmHB1Q2vRBTKLOsrXy3aNAxR-CV9Ur0RqCsVJTBc2GPjcD59G3H7jE3IAWlcZBuxPh05HKyUUhaD1O8My9ZHicFqU70cYdmEx1Yyiv71f7eV216inpOSs_3-vcDSFPgKw-2qFO6TCehbOk31wIlRtGiQcTP-B-Vi73QV6DTHIrFAKvo4Nqcsi5QtUFY8rzWgRp-KF4ILBrGrG6pYZ161f4flQYbS3xqpWGAccqZ-9xIXwo9ncz7JmwNWVbfAZFMR5NF9Ah4ZNJ5UdYZLaHfSn96dagpGrEdVr_akmlq1_jwxqyJFth9O1hND2DiNLoIum_rsB9UoYcKeSdkOJLa7L_xQi6LjqUrjuAhpW0J6C3FQw-JQXBHpwh--uoy-aPMetiqtAv_w status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text="Hello there, hope you're well."
    RESULT: iteration ended normally after 15 events at +0.99s; text so far="Hello there, hope you're well."
    

3. primary-abort: TCP reset after response.in_progress, before any output

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-abort
    
  2. Observed output

    openai-python 2.33.0; model=primary-abort
    +  0.89s seq=0 response.created id=resp_-nr0mWocl477edvSS1uCrwncOc5YHH81lwmvy1JR6U2cRycBkQglnL377eucgdSv-j6mqXZT-WsPS1-otPK-Gd7TKhynNb2Nzm4F_0eBqfCBMcSbMfQ-3eVIJ_dYQzK5bt2TSaOSz3GqtGZBjem5XZyp6rOz_QAsNNVd7y7x9oAD6PovPUyXUL55no5F5wdtRJHuMq20p1BsuNBjBu-JeR2f55hXfZriSGJABQfPDGMdf1kIFHiJQ-BngPCCjfcbNp5CTmqnKV4ghDEuqrPnBbUucbpIIgunUgNdm9hwVxmADPoTGV89pgtOAEVLgvNyvr6hFx0LS2HwW4ntGhQB40YD60chsDqU7l9khmD-eG2fVymfTS3UHR5qp5l77WeQiC4rhjXehLDiVsr7VUux1cdH-U1xqwVkaU1SCZiuvK-NfZc1OMuwXwYA1ekG-e6k0ksLiJT11sJcSSSl11ZgeRv5
    +  0.89s seq=1 response.in_progress id=resp_-nr0mWocl477edvSS1uCrwncOc5YHH81lwmvy1JR6U2cRycBkQglnL377eucgdSv-j6mqXZT-WsPS1-otPK-Gd7TKhynNb2Nzm4F_0eBqfCBMcSbMfQ-3eVIJ_dYQzK5bt2TSaOSz3GqtGZBjem5XZyp6rOz_QAsNNVd7y7x9oAD6PovPUyXUL55no5F5wdtRJHuMq20p1BsuNBjBu-JeR2f55hXfZriSGJABQfPDGMdf1kIFHiJQ-BngPCCjfcbNp5CTmqnKV4ghDEuqrPnBbUucbpIIgunUgNdm9hwVxmADPoTGV89pgtOAEVLgvNyvr6hFx0LS2HwW4ntGhQB40YD60chsDqU7l9khmD-eG2fVymfTS3UHR5qp5l77WeQiC4rhjXehLDiVsr7VUux1cdH-U1xqwVkaU1SCZiuvK-NfZc1OMuwXwYA1ekG-e6k0ksLiJT11sJcSSSl11ZgeRv5
    +  0.89s seq=2 response.output_item.added
    +  0.89s seq=3 response.content_part.added
    +  0.89s seq=4 response.output_text.delta delta='Hello'
    +  0.89s seq=5 response.output_text.delta delta=' there'
    +  0.89s seq=6 response.output_text.delta delta=','
    +  0.89s seq=7 response.output_text.delta delta=' nice'
    +  0.90s seq=8 response.output_text.delta delta=' to'
    +  0.93s seq=9 response.output_text.delta delta=' meet'
    +  0.93s seq=10 response.output_text.delta delta=' you'
    +  0.94s seq=11 response.output_text.delta delta='.'
    +  1.02s seq=12 response.output_text.done
    +  1.02s seq=13 response.content_part.done
    +  1.03s seq=14 response.output_item.done
    +  1.21s seq=15 response.completed id=resp_-nr0mWocl477edvSS1uCrwncOc5YHH81lwmvy1JR6U2cRycBkQglnL377eucgdSv-j6mqXZT-WsPS1-otPK-Gd7TKhynNb2Nzm4F_0eBqfCBMcSbMfQ-3eVIJ_dYQzK5bt2TSaOSz3GqtGZBjem5XZyp6rOz_QAsNNVd7y7x9oAD6PovPUyXUL55no5F5wdtRJHuMq20p1BsuNBjBu-JeR2f55hXfZriSGJABQfPDGMdf1kIFHiJQ-BngPCCjfcbNp5CTmqnKV4ghDEuqrPnBbUucbpIIgunUgNdm9hwVxmADPoTGV89pgtOAEVLgvNyvr6hFx0LS2HwW4ntGhQB40YD60chsDqU7l9khmD-eG2fVymfTS3UHR5qp5l77WeQiC4rhjXehLDiVsr7VUux1cdH-U1xqwVkaU1SCZiuvK-NfZc1OMuwXwYA1ekG-e6k0ksLiJT11sJcSSSl11ZgeRv5 status=completed model=gpt-5.4-mini-2026-03-17 error=None output_text='Hello there, nice to meet you.'
    RESULT: iteration ended normally after 16 events at +1.28s; text so far='Hello there, nice to meet you.'
    

4. primary-drop-after-partial: two text deltas, then the socket closes (guard: nothing is replayed once output started)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-drop-after-partial
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop-after-partial
    +  0.25s seq=0 response.created id=resp_OUQR6FQNO19u2nEH27YUE47JdNKd0LkXujlGVVViiSEZB37olqt53MwPlZ8CLZK-WgRu5-jV2vAsJhrylILYY8E2NMINoTABb8cWYhhRQIQBJU1hGnbff3G9RBOPCdBSYxi6tyHjSSOO-sGzccm8pvg5vm7GHS1FAIjUcTMj5m0NK1O8XiHsbzBSgBlI7OSOIrikk7Mo9cV5lxfmpmD_bflyOZO4WD_54YdM6d4Mn1m2SiDlcAp2KEJSKQq20hRU4dJ1l-v5UmdRApLKozON_T3VA0-cUpIkRv2Vhs61e-UYhm_3dxcjYA5HaZcQ7qwG1lex5O8lLk7k9bdeTzS-u-XKjl6th41-m5MBTQsorivAFLbUlSM_5DeAi1dxMxR83xNdCSLQ8NnY2RsBYow=
    +  0.25s seq=1 response.in_progress id=resp_OUQR6FQNO19u2nEH27YUE47JdNKd0LkXujlGVVViiSEZB37olqt53MwPlZ8CLZK-WgRu5-jV2vAsJhrylILYY8E2NMINoTABb8cWYhhRQIQBJU1hGnbff3G9RBOPCdBSYxi6tyHjSSOO-sGzccm8pvg5vm7GHS1FAIjUcTMj5m0NK1O8XiHsbzBSgBlI7OSOIrikk7Mo9cV5lxfmpmD_bflyOZO4WD_54YdM6d4Mn1m2SiDlcAp2KEJSKQq20hRU4dJ1l-v5UmdRApLKozON_T3VA0-cUpIkRv2Vhs61e-UYhm_3dxcjYA5HaZcQ7qwG1lex5O8lLk7k9bdeTzS-u-XKjl6th41-m5MBTQsorivAFLbUlSM_5DeAi1dxMxR83xNdCSLQ8NnY2RsBYow=
    +  0.25s seq=2 response.output_item.added
    +  0.25s seq=3 response.content_part.added
    +  0.25s seq=4 response.output_text.delta delta='The first half of the answer, '
    +  0.25s seq=5 response.output_text.delta delta='and then the upstream '
    +  0.32s seq=6 response.failed id=resp_OUQR6FQNO19u2nEH27YUE47JdNKd0LkXujlGVVViiSEZB37olqt53MwPlZ8CLZK-WgRu5-jV2vAsJhrylILYY8E2NMINoTABb8cWYhhRQIQBJU1hGnbff3G9RBOPCdBSYxi6tyHjSSOO-sGzccm8pvg5vm7GHS1FAIjUcTMj5m0NK1O8XiHsbzBSgBlI7OSOIrikk7Mo9cV5lxfmpmD_bflyOZO4WD_54YdM6d4Mn1m2SiDlcAp2KEJSKQq20hRU4dJ1l-v5UmdRApLKozON_T3VA0-cUpIkRv2Vhs61e-UYhm_3dxcjYA5HaZcQ7qwG1lex5O8lLk7k9bdeTzS-u-XKjl6th41-m5MBTQsorivAFLbUlSM_5DeAi1dxMxR83xNdCSLQ8NnY2RsBYow= status=failed model=fault-model error=ResponseError(code='server_error', message="Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")
    RESULT: iteration ended normally after 7 events at +0.32s; text so far='The first half of the answer, and then the upstream '
    

5. primary-healthy: full stream, no fallback (control)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-healthy
    
  2. Observed output

    openai-python 2.33.0; model=primary-healthy
    +  0.30s seq=0 response.created id=resp_q0IZe3s2mcxHvhDG8ncw5qRVoVoFCQHZMYhp65SUpXfaz_HTeDy3YjTWSCp92LYcidMj-BftVXbQoLr2ZFVg6Jj40MYmWjNadc3Hk-H-3yJW85seA0Ta8R64M01Ra1FwgHTAS4cazgG89yR-uCsTbbWVpjT2ABS202fA_mKK-nIImyWlXMOijKhd444InlahopCX5qZQplSD_lDKgn1yn4tO55CJwrvz1u5-6FKfXOErBWY8PjUDhQXKg5p2V12ra2T5MDoKSC5mNUIBQEvQX6boOHN745MgRZ5V875t-z0Mpr2C882ryfvxIY88A7YZIyUVZZ3jeFs0xiMxCq6LdFZwFOpwinZAJf2agvHxWrBEJeLGE3jgrsRn4ERzxNybiQ8Wv827LF-1_F_zgIY=
    +  0.30s seq=1 response.in_progress id=resp_q0IZe3s2mcxHvhDG8ncw5qRVoVoFCQHZMYhp65SUpXfaz_HTeDy3YjTWSCp92LYcidMj-BftVXbQoLr2ZFVg6Jj40MYmWjNadc3Hk-H-3yJW85seA0Ta8R64M01Ra1FwgHTAS4cazgG89yR-uCsTbbWVpjT2ABS202fA_mKK-nIImyWlXMOijKhd444InlahopCX5qZQplSD_lDKgn1yn4tO55CJwrvz1u5-6FKfXOErBWY8PjUDhQXKg5p2V12ra2T5MDoKSC5mNUIBQEvQX6boOHN745MgRZ5V875t-z0Mpr2C882ryfvxIY88A7YZIyUVZZ3jeFs0xiMxCq6LdFZwFOpwinZAJf2agvHxWrBEJeLGE3jgrsRn4ERzxNybiQ8Wv827LF-1_F_zgIY=
    +  0.30s seq=2 response.output_item.added
    +  0.30s seq=3 response.content_part.added
    +  0.30s seq=4 response.output_text.delta delta='The first half of the answer, '
    +  0.30s seq=5 response.output_text.delta delta='and then the upstream '
    +  0.30s seq=6 response.output_text.done
    +  0.30s seq=7 response.content_part.done
    +  0.30s seq=8 response.output_item.done
    +  0.32s seq=9 response.completed id=resp_q0IZe3s2mcxHvhDG8ncw5qRVoVoFCQHZMYhp65SUpXfaz_HTeDy3YjTWSCp92LYcidMj-BftVXbQoLr2ZFVg6Jj40MYmWjNadc3Hk-H-3yJW85seA0Ta8R64M01Ra1FwgHTAS4cazgG89yR-uCsTbbWVpjT2ABS202fA_mKK-nIImyWlXMOijKhd444InlahopCX5qZQplSD_lDKgn1yn4tO55CJwrvz1u5-6FKfXOErBWY8PjUDhQXKg5p2V12ra2T5MDoKSC5mNUIBQEvQX6boOHN745MgRZ5V875t-z0Mpr2C882ryfvxIY88A7YZIyUVZZ3jeFs0xiMxCq6LdFZwFOpwinZAJf2agvHxWrBEJeLGE3jgrsRn4ERzxNybiQ8Wv827LF-1_F_zgIY= status=completed model=fault-model error=None output_text='The first half of the answer, and then the upstream finished normally.'
    RESULT: iteration ended normally after 10 events at +0.32s; text so far='The first half of the answer, and then the upstream '
    

6. primary-drop over raw curl: event types, response ids, and sequence numbers pulled out of the SSE body

  1. Run the request

    curl -sN http://127.0.0.1:23595/v1/responses -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d '{"model":"primary-drop","input":"Say hello in five words.","stream":true}' | grep '^data:' | grep -oE '"type":"response\.[a-z_.]+"|"response":\{"id":"resp_[^"]{0,12}|"sequence_number":[0-9]+' | awk '/^"type"/{if(l)print l; l=$0; next}{l=l" "$0} END{print l}'
    
  2. Observed output

    "type":"response.created" "response":{"id":"resp_z-cMrfq5IOie "sequence_number":0
    "type":"response.in_progress" "response":{"id":"resp_z-cMrfq5IOie "sequence_number":1
    "type":"response.output_item.added" "sequence_number":2
    "type":"response.content_part.added" "sequence_number":3
    "type":"response.output_text.delta" "sequence_number":4
    "type":"response.output_text.delta" "sequence_number":5
    "type":"response.output_text.delta" "sequence_number":6
    "type":"response.output_text.delta" "sequence_number":7
    "type":"response.output_text.delta" "sequence_number":8
    "type":"response.output_text.delta" "sequence_number":9
    "type":"response.output_text.delta" "sequence_number":10
    "type":"response.output_text.delta" "sequence_number":11
    "type":"response.output_text.done" "sequence_number":12
    "type":"response.content_part.done" "sequence_number":13
    "type":"response.output_item.done" "sequence_number":14
    "type":"response.completed" "response":{"id":"resp_z-cMrfq5IOie "sequence_number":15
    

7. primary-drop-to-empty-backup: socket closed before any output, and the fallback deployment closes its socket before its first event (guard: the failure is reported under the announced id)

  1. Run the client

    LITELLM_MASTER_KEY=<master key> .venv/bin/python client_stream.py http://127.0.0.1:23595 primary-drop-to-empty-backup
    
  2. Observed output

    openai-python 2.33.0; model=primary-drop-to-empty-backup
    +  0.67s seq=0 response.created id=resp_yBdQK5yrdyTk…
    +  0.67s seq=1 response.in_progress id=resp_yBdQK5yrdyTk…
    +  0.67s seq=2 response.failed id=resp_yBdQK5yrdyTk… status=failed model=fault-model error=ResponseError(code='server_error', message="Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")
    RESULT: iteration ended normally after 3 events at +0.67s; text so far=''
    
  3. The same request at 3399cb8, the tip Bugbot reviewed, before 11387cc: the primary's announcement was dropped and the proxy synthesized a terminal event under a fresh id

    openai-python 2.33.0; model=primary-drop-to-empty-backup
    +  1.03s seq=0 response.failed id=resp_933b1ce27481… status=failed model=None error=ResponseError(code='server_error', message="Response payload is not completed: <TransferEncodingError: 400, message='Not enough data to satisfy transfer length header.'>")
    RESULT: iteration ended normally after 1 events at +1.03s; text so far=''
    

Observations from the run:

  • Before, sequence numbers restart at 0 on the fallback; after, contiguous
  • Lifecycle events now arrive with the first output event; by design
  • Case 4 ends in response.failed on both legs; leaves alone
  • response.completed names the fallback's model, not the alias; pre-existing, leaves alone
  • Healthy stream byte-identical in shape on both legs; unchanged
  • Case 7 ends in response.failed under the primary's id on both legs; at 3399cb8 it was a lone response.failed under a synthesized id, fixed in 11387cc

Type

🐛 Bug Fix

Caveats (if any)

Low

  • Past 200 held lifecycle events a fallback replays the pair again
  • Held events surface with the first output event, never earlier
  • The wrapper's two stream-close guards now catch Exception instead of BaseException (CodeQL), so a cancellation raised inside a stream's aclose during teardown propagates instead of being logged at debug; no live case exercises that path
  • CircleCI pipeline 90439 at the tip has seven reds, each also red on main: local_testing_part1 fails only test_get_model_info_bedrock_cross_region_capability_parity (job 2218680), the same single failure as main pipelines 90403 (job 2216868) and 90441 (job 2218778); langfuse_logging_unit_tests times out at its Run tests step (job 2218658) as main's jobs 2216878 and 2218773 do; using_litellm_on_windows timed out at Install Python (job 2218649) before any repo code ran, and main's runs time out on the same executor at Install Dependencies (job 2216869) and at the MAX_PATH guard step (job 2216487); unit (job 2218663) has 34 failures, 32 of them in main 90441's 36 (job 2218776), the other two being one more instance of the MCP conftest teardown task_done() called too many times error main hits on three sibling tests and test_the_repo_as_it_stands_has_every_shard_child_assigned, which CircleCI runs on the branch head that predates test: move tests/test_litellm core utils, routing, responses, caching and rust_bridge into tests/unit #43199's shard move and which the tip's misc / Run tests GitHub Actions run on the merge commit passes; proxy_store_model_in_db_tests fails only test_e2e_langfuse_callbacks_in_db (job 2218668), the same single failure as main's jobs 2216860 and 2218794; build_and_test fails the same two test_implemented_namespace_case_modules_remain_importable OCR parity cases (job 2218684) as main's job 2216862; integration-extensions ended in a CircleCI infrastructure_fail with its Run owned integration contracts step canceled and no test run (job 2218632), the same shape as main's job 2216848

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

  • 11387cc passes /live-pr-risk

@devin-ai-integration

devin-ai-integration Bot commented Sep 25, 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

@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/router.py 92.06% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Changes how streaming responses handle lifecycle events during fallback.

The PR appears safe to merge; no new actionable issue or outstanding previous finding remains.

Findings

  1. P1 First stream event is delayed ▶
  2. P2 Event cap announces two responses ▶

Summary

The PR holds early Responses lifecycle events until output begins, so a successful pre-output fallback announces only the response that continues. It also adds fallback-stream cleanup and tests for event ordering and failure paths.

Reviews (4) · Last reviewed commit: "fix(router): replay the held lifecycle e..."

Comment thread litellm/router.py
Comment on lines +3349 to +3351
if _responses_stream_holds_event(item, len(held_lifecycle_events)):
held_lifecycle_events = (*held_lifecycle_events, item)
continue

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 First stream event is delayed If the provider sends response.created but takes a long time to produce output, this holds the event while the proxy waits for its first chunk. With keepalive disabled, the client receives no SSE data during that gap and can time out despite the request still progressing

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.

The client's longest silence grows only by the provider's accept latency: before this PR it already waited time-to-first-output between in_progress and the first output event

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.

That’s true for the interval after response.in_progress: the provider’s time to first output is unchanged. The behavioral difference is that the client now receives no event during that interval—both response.created and response.in_progress are held—so a client-side first-event/read timeout can cover the entire accept-to-output period. Previously, those lifecycle events arrived before output and could reset or satisfy such a timeout.

So the delay itself is not added to provider TTFO, but suppressing the earlier events remains a compatibility tradeoff for clients with an event timeout.

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.

The silence becomes accept latency plus time-to-first-output instead of the larger of the two, and the /v1/messages wrapper already holds message_start the same way

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 — I misstated the timing. With lifecycle events held, silence from request start can span accept latency plus time-to-first-output; previously, the first lifecycle event arrived after accept latency, so the longest individual silence was the larger of those intervals.

The /v1/messages wrapper is a relevant precedent, and this is an explicitly documented compatibility tradeoff of associating lifecycle events with the response that ultimately completes. I withdraw the timeout finding; the remaining behavior is intentional and covered by the stated design.

Comment thread litellm/router.py
def _responses_stream_holds_event(item: object, held_event_count: int) -> bool:
from litellm.responses.streaming_iterator import PRE_OUTPUT_LIFECYCLE_EVENT_TYPES

if held_event_count >= MAX_HELD_PRE_OUTPUT_RESPONSES_EVENTS:

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.

P2 Event cap announces two responses If more than 200 lifecycle events arrive before output, the cap sends the primary response's events even though a pre-output fallback remains possible. A fallback that sends its own response.created then gives the client two response IDs. The cap test uses a fallback without that event, so it misses this case

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.

The cap is a memory bound: providers send two or three lifecycle events before output, and the Messages wrapper commits at its cap too

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.

That’s fair. The cap is a defensive memory bound, not an expected lifecycle threshold: a normal Responses stream emits only two or three pre-output lifecycle events, so reaching 200 would require pathological provider behavior. Given the existing /v1/messages precedent and the explicit caveat in the PR, I’m withdrawing this as a blocking finding; the cap behavior is acceptable here.

Comment thread tests/unit/test_router/test_router.py Outdated
Comment thread litellm/router.py Fixed
@codspeed

codspeed Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_responses_fallback_single_created (11387cc) with main (cd1107a)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (474ab91) during the generation of this report, so cd1107a 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

Comment thread tests/unit/test_router/test_router.py Outdated
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 25, 2026
@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.

Stale Bugbot comment from a previous run.

Comment thread litellm/router.py
@mateo-berri mateo-berri added run-ci and removed run-ci labels Sep 26, 2026
@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 11387cc. 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 4adbc13 into main Sep 26, 2026
150 of 160 checks passed
@mateo-berri
mateo-berri deleted the litellm_responses_fallback_single_created branch September 26, 2026 01:16
jibanez-staticduo added a commit to jibanez-staticduo/litellm that referenced this pull request Sep 30, 2026
Upstream added 185 commits since the previous sync, including the owned
`Client` pool refactor (BerriAI#43245), the Responses lifecycle-event hold (BerriAI#43238)
and the driver preflight split (BerriAI#43259). One conflict, in the
`get_litellm_params` import block in `litellm/main.py`: upstream added
`InvalidControlOption`, `parse_control_options` and `with_control_options`
where this branch added `REASONING_TRANSPORT_KWARGS_KEYS`. Both stay, in the
order the import sorter wants.

Reasoning normalization still runs ahead of the configured system-message
ordering, and the net diff against upstream is the same 16 paths and 1005
added lines as before the sync.

Verified on this merge: 1574 passed across the relocated hosted_vllm, openai
chat and litellm_params files, 32 passed and 8 skipped in the router suite,
32 in caching, 308 in the Responses-to-Chat bridge, 337 in model management
endpoints. ruff_strict_gate, type_discipline_gate and test_quality_gate pass
with --base upstream/main.
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