Scorecard · 2026-09-09 · profile full · methodology v0.1 · failoverbench 0.1.0

Failover Bench

A crash-test lab for LLM gateways and SDKs. One fake provider breaks on purpose in sixteen documented ways; every system below is pushed through the same sixteen faults; each cell is what the caller actually got.

Next scorecard: Monday 5 October 2026. Harness, methodology and raw logs: github.com/failoverbench/failoverbench.

passcaller got the complete answer, conduct cleanpartialrescued, advisory check failedsafenot rescued, but a bounded clean errorfailhang, truncated answer as success, or a conduct check failed
Scenario Bifrost v2.1.0 (Docker) No gateway (direct, no retries) DeepSeek Harness + dsh-llm-fallbacks (SDK) LangChain ChatOpenAI + with_fallbacks LiteLLM proxy (Docker) LiteLLM Router, tuned (allowed_fails + cooldown) LiteLLM Router (in-process) openai-python SDK (built-in retries) Portkey Gateway (Docker, OSS) Reference client (httpx)
S01 429 with Retry-AfterFirst attempt gets HTTP 429 with `Retry-After: 2`; every later attempt succeeds. partial2p/0f · gap 0.6s · 1.5s safe1p/0f · 0.0s partial1p/1f · 2.6s pass2p/0f · gap 2.0s · 3.0s pass2p/0f · gap 2.4s · 3.4s pass2p/0f · gap 2.8s · 4.1s pass2p/0f · gap 2.7s · 4.0s pass2p/0f · gap 2.0s · 3.3s pass2p/0f · gap 2.0s · 3.0s pass2p/0f · gap 2.0s · 3.0s
S02 429 without Retry-After, persistentEvery attempt gets HTTP 429 and no Retry-After header. pass3p/1f · gap 0.4s · 2.3s safe1p/0f · 0.0s pass1p/1f · 1.9s pass3p/1f · gap 0.4s · 2.3s pass3p/1f · gap 1.1s · 5.8s pass2p/1f · gap 1.1s · 2.1s pass3p/1f · gap 0.6s · 5.3s safe3p/0f · gap 0.5s · 1.4s pass3p/1f · gap 1.0s · 4.0s pass3p/1f · gap 0.5s · 2.4s
S03 500 once, then healthyFirst attempt gets HTTP 500; the second succeeds. pass2p/0f · gap 0.4s · 1.4s safe1p/0f · 0.0s partial1p/1f · 1.9s pass2p/0f · gap 0.5s · 1.5s pass2p/0f · gap 0.8s · 1.8s pass2p/0f · gap 1.3s · 2.2s pass2p/0f · gap 0.7s · 1.7s pass2p/0f · gap 0.5s · 1.5s pass2p/0f · gap 1.0s · 2.0s pass2p/0f · gap 0.5s · 1.5s
S04 503 persistentEvery attempt gets HTTP 503 (overloaded). pass3p/1f · gap 0.6s · 2.4s safe1p/0f · 0.0s pass1p/1f · 2.0s pass3p/1f · gap 0.5s · 2.4s pass3p/1f · gap 0.8s · 5.5s pass2p/1f · gap 1.0s · 2.0s pass3p/1f · gap 0.9s · 5.8s safe3p/0f · gap 0.5s · 1.4s pass3p/1f · gap 1.0s · 4.0s pass3p/1f · gap 0.4s · 2.2s
S05 TCP reset before headersFirst attempt: the connection is reset before any response bytes. Later attempts succeed. pass2p/0f · gap 0.0s · 1.0s safe1p/0f · 0.0s pass1p/1f · 1.9s pass2p/0f · gap 0.4s · 1.3s pass2p/0f · gap 0.6s · 1.6s pass2p/0f · gap 1.2s · 2.2s pass2p/0f · gap 0.8s · 1.8s pass2p/0f · gap 0.4s · 1.4s pass2p/0f · gap 1.0s · 2.0s pass2p/0f · gap 0.5s · 1.5s
S06 No response at allThe provider accepts the request and never answers (up to 120 s). fail2p/0f · gap 120.0s · 130.0s safe1p/0f · 30.0s pass1p/1f · 31.9s partial3p/1f · gap 30.4s · 92.3s partial3p/1f · gap 30.8s · 95.5s partial3p/1f · gap 31.2s · 96.8s partial3p/1f · gap 30.8s · 96.2s safe3p/0f · gap 30.5s · 91.6s pass1p/1f · 31.0s pass1p/1f · 11.0s
S07 Slow first tokenHeaders arrive at once; the first token arrives after 20 s; the rest is normal. partial1p/0f · 21.0s pass1p/0f · 21.0s partial1p/0f · 22.0s partial1p/0f · 21.0s partial1p/0f · 21.0s partial1p/0f · 21.0s partial1p/0f · 21.0s pass1p/0f · 21.0s partial1p/0f · 21.0s pass1p/1f · 11.0s
S08 Stream cut mid-answerThe stream dies after 40 of 60 tokens with no terminating chunk. safe1p/0f · 0.7s safe1p/0f · 0.6s pass1p/1f · 2.6s safe1p/0f · 0.6s safe1p/0f · 0.7s safe1p/0f · 0.7s safe1p/0f · 0.7s safe1p/0f · 0.6s fail1p/0f · 0.7s · TRUNCATED pass3p/1f · gap 1.2s · 4.3s
S09 Stream stallsAfter 20 tokens the connection stays open and goes silent for 30 s, then closes. safe1p/0f · 30.3s safe1p/0f · 30.3s pass1p/1f · 32.3s safe1p/0f · 30.3s safe1p/0f · 30.3s safe1p/0f · 30.3s safe1p/0f · 30.3s safe1p/0f · 30.3s fail1p/0f · 30.3s · TRUNCATED pass1p/1f · 11.3s
S10 Malformed chunk in the streamThe tenth SSE event is invalid JSON; the stream otherwise completes normally. pass1p/0f · 1.0s pass1p/0f · 1.0s safe1p/0f · 1.2s safe1p/0f · 0.2s safe1p/0f · 0.2s safe1p/0f · 0.2s safe1p/0f · 0.2s safe1p/0f · 0.2s pass1p/0f · 1.0s pass1p/0f · 1.0s
S11 Stream ends without [DONE]A complete stream, correctly terminated at the HTTP level, but the `data: [DONE]` sentinel is missing. pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.9s pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.0s pass1p/0f · 1.0s
S12 Context length exceededHTTP 400 with code `context_length_exceeded` on every attempt. pass1p/1f · 1.0s safe1p/0f · 0.0s pass1p/1f · 1.9s pass1p/1f · 1.0s pass1p/1f · 1.0s pass1p/1f · 1.0s pass1p/1f · 1.0s safe1p/0f · 0.0s pass1p/1f · 1.0s pass1p/1f · 1.0s
S13 Content-filter rejectionHTTP 400 with code `content_policy_violation` on every attempt. partial1p/1f · 1.0s pass1p/0f · 0.0s pass1p/0f · 1.0s partial1p/1f · 1.0s partial1p/1f · 1.0s partial1p/1f · 1.0s partial1p/1f · 1.0s pass1p/0f · 0.0s partial1p/1f · 1.0s pass1p/0f · 0.0s
S14 Fallback target rejects prefillThe primary stream dies after 40 tokens. The fallback model rejects any request whose last message is an assistant prefill. safe1p/0f · 0.6s safe1p/0f · 0.6s pass1p/1f · 2.7s safe1p/0f · 0.6s safe1p/0f · 0.7s safe1p/0f · 0.7s safe1p/0f · 0.7s safe1p/0f · 0.6s fail1p/0f · 0.7s · TRUNCATED pass3p/1f · gap 1.2s · 4.6s
S15 Primary flaps: down, then healthyThe primary answers 503 for the first 60 s after reset, then is healthy. Twenty requests, one every 4 s. pass15/15 served in outage · back on primary +0s safe0/15 served in outage · back on primary +0s pass15/15 served in outage · back on primary +0s pass15/15 served in outage · back on primary +0s pass11/11 served in outage · back on primary +0s pass15/15 served in outage · back on primary +4s pass11/11 served in outage · back on primary +2s safe0/15 served in outage · back on primary +0s pass15/15 served in outage · back on primary +0s pass15/15 served in outage · back on primary +0s
S16 Fail-over accountingEvery primary attempt gets HTTP 503; the fallback answers. pass3p/1f · gap 0.5s · 2.5s safe1p/0f · 0.0s pass1p/1f · 2.0s pass3p/1f · gap 0.5s · 2.3s pass3p/1f · gap 0.9s · 5.3s pass2p/1f · gap 0.9s · 1.9s pass3p/1f · gap 0.9s · 5.7s safe3p/0f · gap 0.4s · 1.3s pass3p/1f · gap 1.0s · 4.0s pass3p/1f · gap 0.6s · 2.7s

Contrast rows

A subset of scenarios re-run under a different setting, shown next to the row they contrast with.

DSH, shared session — S15 fail 15/15 served in outage · back on primary never

Systempasspartialsafefailrun
Bifrost v2.1.0 (Docker)9331275s
No gateway (direct, no retries)40120162s
DSH, shared session000179s
DeepSeek Harness + dsh-llm-fallbacks (SDK)12310204s
LangChain ChatOpenAI + with_fallbacks9340238s
LiteLLM proxy (Docker)9340252s
LiteLLM Router, tuned (allowed_fails + cooldown)9340243s
LiteLLM Router (in-process)9340254s
openai-python SDK (built-in retries)60100232s
Portkey Gateway (Docker, OSS)11203184s
Reference client (httpx)16000136s

Check-level detail

Every check the runner scored, and what the fake provider saw. Advisory checks change a pass to partial; required checks change it to fail.

Bifrost v2.1.0 (Docker) — adapter endpoint, fallback=yes, streaming=yes

Bifrost with a custom provider `fake` (base_provider_type openai, base_url = the wall, allow_private_network on). Baseline settings: max_retries 2 (vendor default is 0), request timeout 30 s, stream idle timeout 30 s, one fallback passed per request (`fallbacks: ["fake/fb-ok"]`). Bifrost does not honour Retry-After (backoff 500 ms → 5 s with jitter). Per docs, fallback covers HTTP-level errors and an error in the first SSE event; later stream errors are not retried.

params: {"base_url": "http://127.0.0.1:18080/v1", "api_key": "failoverbench", "model_format": "fake/{model}", "extra_body": {"fallbacks": ["{fallback}"]}}

S01 429 with Retry-Afterpartial rescued; advisory: retry_after_honoured

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allfail failed: within_budget

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answersafe not rescued — clean error: in-band error event after 40 chunk(s): provider closed the stream before sending

S09 Stream stallssafe not rescued — clean error: in-band error event after 20 chunk(s): Error reading stream: stream idle timeout

S10 Malformed chunk in the streampass rescued

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillsafe not rescued — clean error: in-band error event after 40 chunk(s): provider closed the stream before sending

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

No gateway (direct, no retries) — adapter direct, fallback=no, streaming=yes

Control row: one request straight at the provider with no retries, no fallback, a 30 s read timeout. What an app gets with zero protection.

params: {"read_timeout_s": 30}

S01 429 with Retry-Aftersafe not rescued — clean error: HTTP 429: Rate limit reached for fb-s01-429-retry-after. Please retry after 2 se

S02 429 without Retry-After, persistentsafe not rescued — clean error: HTTP 429: Rate limit reached for fb-s02-429-persistent.

S03 500 once, then healthysafe not rescued — clean error: HTTP 500: The server had an error while processing your request.

S04 503 persistentsafe not rescued — clean error: HTTP 503: The engine is currently overloaded. Please try again later.

S05 TCP reset before headerssafe not rescued — clean error: ReadError:

S06 No response at allsafe not rescued — clean error: ReadTimeout:

S07 Slow first tokenpass rescued

S08 Stream cut mid-answersafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S09 Stream stallssafe not rescued — clean error: ReadTimeout:

S10 Malformed chunk in the streampass rescued

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededsafe not rescued — clean error: HTTP 400: This model's maximum context length is 8192 tokens. However, your mess

S13 Content-filter rejectionpass rescued

S14 Fallback target rejects prefillsafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S15 Primary flaps: down, then healthysafe not rescued — clean error: HTTP 503: The engine is currently overloaded. Please try again later.

S16 Fail-over accountingsafe not rescued — clean error: HTTP 503: The engine is currently overloaded. Please try again later.

DSH, shared session — adapter dsh, fallback=yes, streaming=yes

Contrast row for S15 only: identical to the DSH row except session_per_request: false, so all twenty requests share one harness session. Shows whether the primary is never probed again (plugin) or the replaced model sticks to the session (harness).

params: {"dsh_home": "gateways/dsh/home-default", "dsh_home_noprefill": "gateways/dsh/home-noprefill", "workspace": "gateways/dsh/workspace", "provider": "fake", "request_timeout_s": 60, "session_per_request": false}

S15 Primary flaps: down, then healthyfail failed: recovered

DeepSeek Harness + dsh-llm-fallbacks (SDK) — adapter dsh, fallback=yes, streaming=yes

EXPERIMENTAL. DeepSeek Harness 0.1.2-rc.1 driven through the official Python SDK, with dsh-llm-fallbacks 0.4.2 (chain primary → fb-ok, cooldown 30 s, half-open recovery). Baseline: llm-retry maxRetries 2, stream idle timeout 30 s. The harness sends its own system prompt and tool roster on every call; the wall ignores both. Run gateways/dsh/setup.sh first.

params: {"dsh_home": "gateways/dsh/home-default", "dsh_home_noprefill": "gateways/dsh/home-noprefill", "workspace": "gateways/dsh/workspace", "provider": "fake", "request_timeout_s": 60, "session_per_request": true}

S01 429 with Retry-Afterpartial rescued; advisory: retried, retry_after_honoured

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypartial rescued; advisory: same_provider_retry

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpass rescued

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answerpass rescued

S09 Stream stallspass rescued

S10 Malformed chunk in the streamsafe not rescued — clean error: dsh finish_reason='error', final_response=empty

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpass rescued

S14 Fallback target rejects prefillpass rescued

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

LangChain ChatOpenAI + with_fallbacks — adapter langchain, fallback=yes, streaming=yes

langchain-openai ChatOpenAI(max_retries=2, timeout=30) with .with_fallbacks([ChatOpenAI(fallback)]). LangChain documents that streaming fallbacks only cover errors before the first chunk.

params: {"max_retries": 2, "timeout_s": 30}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpartial rescued; advisory: silent_route_not_retried

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answersafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S09 Stream stallssafe not rescued — clean error: ReadTimeout:

S10 Malformed chunk in the streamsafe not rescued — clean error: JSONDecodeError: Expecting value: line 1 column 94 (char 93)

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillsafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

LiteLLM proxy (Docker) — adapter endpoint, fallback=yes, streaming=yes

LiteLLM proxy container configured by `python -m failoverbench litellm-config` (see gateways/litellm). The proxy owns retries and fallbacks.

params: {"base_url": "http://127.0.0.1:4000/v1", "api_key": "sk-failoverbench"}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpartial rescued; advisory: silent_route_not_retried

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answersafe not rescued — clean error: in-band error event after 41 chunk(s): litellm.APIConnectionError: APIConnection

S09 Stream stallssafe not rescued — clean error: in-band error event after 21 chunk(s): litellm.APIConnectionError: APIConnection

S10 Malformed chunk in the streamsafe not rescued — clean error: in-band error event after 11 chunk(s): litellm.APIConnectionError: APIConnection

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillsafe not rescued — clean error: in-band error event after 41 chunk(s): litellm.APIConnectionError: APIConnection

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

LiteLLM Router, tuned (allowed_fails + cooldown) — adapter litellm-router, fallback=yes, streaming=yes

Same as the LiteLLM Router row plus allowed_fails=1 and cooldown_time=30, the settings LiteLLM needs before it will cool down a single-deployment model group (PR #8668). Shown so readers can see what one config line changes on S15.

params: {"num_retries": 2, "timeout_s": 30, "context_window_fallbacks": true, "allowed_fails": 1, "cooldown_time": 30}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpartial rescued; advisory: silent_route_not_retried

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answersafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S09 Stream stallssafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S10 Malformed chunk in the streamsafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillsafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

LiteLLM Router (in-process) — adapter litellm-router, fallback=yes, streaming=yes

litellm.Router with one primary and one fallback deployment per scenario, num_retries=2, timeout=30 s, context_window_fallbacks on. Defaults otherwise.

params: {"num_retries": 2, "timeout_s": 30, "context_window_fallbacks": true}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpartial rescued; advisory: silent_route_not_retried

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answersafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S09 Stream stallssafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S10 Malformed chunk in the streamsafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillsafe not rescued — clean error: APIConnectionError: litellm.APIConnectionError: APIConnectionError: OpenAIExcept

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

openai-python SDK (built-in retries) — adapter openai-python, fallback=no, streaming=yes

The official SDK with its defaults except timeout (600 s → 30 s so a run stays bounded). No fallback concept, so fail-over checks are n/a.

params: {"max_retries": 2, "timeout_s": 30}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentsafe not rescued — clean error: RateLimitError: Error code: 429 - {'error': {'message': 'Rate limit reached for

S03 500 once, then healthypass rescued

S04 503 persistentsafe not rescued — clean error: InternalServerError: Error code: 503 - {'error': {'message': 'The engine is curr

S05 TCP reset before headerspass rescued

S06 No response at allsafe not rescued — clean error: APITimeoutError: Request timed out.

S07 Slow first tokenpass rescued

S08 Stream cut mid-answersafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S09 Stream stallssafe not rescued — clean error: ReadTimeout:

S10 Malformed chunk in the streamsafe not rescued — clean error: JSONDecodeError: Expecting value: line 1 column 94 (char 93)

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededsafe not rescued — clean error: BadRequestError: Error code: 400 - {'error': {'message': "This model's maximum c

S13 Content-filter rejectionpass rescued

S14 Fallback target rejects prefillsafe not rescued — clean error: RemoteProtocolError: peer closed connection without sending complete message bod

S15 Primary flaps: down, then healthysafe not rescued — clean error: InternalServerError: Error code: 503 - {'error': {'message': 'The engine is curr

S16 Fail-over accountingsafe not rescued — clean error: InternalServerError: Error code: 503 - {'error': {'message': 'The engine is curr

Portkey Gateway (Docker, OSS) — adapter endpoint, fallback=yes, streaming=yes

Open-source Portkey AI Gateway. Everything is per request in x-portkey-config: a fallback strategy with two openai targets on custom_host = the wall (host must include /v1). Baseline: primary target retry attempts 2 on 429/5xx with use_retry_after_header, request_timeout 30 s; fallback on any non-2xx (strategy.on_status_codes omitted). Per source, a cut upstream stream is closed to the client with no error event and no retry.

params: {"base_url": "http://127.0.0.1:8787/v1", "api_key": "failoverbench", "headers_json": {"x-portkey-config": {"strategy": {"mode": "fallback"}, "request_timeout": 30000, "targets": [{"provider": "openai", "api_key": "failoverbench-primary", "custom_host": "http://host.docker.internal:8401/v1", "override_params": {"model": "{primary}"}, "retry": {"attempts": 2, "on_status_codes": [429, 500, 502, 503, 504], "use_retry_after_header": true}}, {"provider": "openai", "api_key": "failoverbench-fallback", "custom_host": "http://host.docker.internal:8401/v1", "override_params": {"model": "{fallback}"}}]}}}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpass rescued

S07 Slow first tokenpartial rescued; advisory: ttft_limit

S08 Stream cut mid-answerfail truncated answer presented as success

S09 Stream stallsfail truncated answer presented as success

S10 Malformed chunk in the streampass rescued

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpartial answered by routing around the rejection

S14 Fallback target rejects prefillfail truncated answer presented as success

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued

Reference client (httpx) — adapter reference, fallback=yes, streaming=yes

A small client written to pass: honours Retry-After, backs off with jitter, retries 5xx/resets/cut streams up to 3 times, fails over on timeouts and context-length errors, never retries content-filter 400s, restarts without prefill. It has no circuit breaker, so S15 shows what stateless retrying looks like.

params: {"max_attempts": 3, "base_backoff_s": 0.5, "stall_limit_s": 10, "ttft_limit_s": 10, "connect_timeout_s": 5}

S01 429 with Retry-Afterpass rescued

S02 429 without Retry-After, persistentpass rescued

S03 500 once, then healthypass rescued

S04 503 persistentpass rescued

S05 TCP reset before headerspass rescued

S06 No response at allpass rescued

S07 Slow first tokenpass rescued

S08 Stream cut mid-answerpass rescued

S09 Stream stallspass rescued

S10 Malformed chunk in the streampass rescued

S11 Stream ends without [DONE]pass rescued

S12 Context length exceededpass rescued

S13 Content-filter rejectionpass rescued

S14 Fallback target rejects prefillpass rescued

S15 Primary flaps: down, then healthypass rescued

S16 Fail-over accountingpass rescued