Repository navigation
[bug] Retryable pre-answer SSE errors bypass candidate fallback #1132
Description
Symptom
An OpenAI-compatible upstream can commit HTTP 200 for a streaming request and then reject it in an SSE frame with {"error":{"code":503,"message":"provider overloaded"}}. Switchyard treats the returned stream as a successful model call. An ordered route never tries its healthy next candidate, and the caller receives HTTP 200 plus an error event instead of an answer.
Reproduction
Configure a stage_router with picker = "efficient_first", efficient_target = "bad", and capable_target = "good". Use OpenAI Chat backends with max_retries = 0. Send a Chat Completions request with "model":"stage" and "stream":true.
Have the first upstream return HTTP 200, Content-Type: text/event-stream, and this first frame:
data: {"id":"c1","object":"chat.completion.chunk","choices":[],"error":{"code":503,"message":"provider overloaded"}}
Have the second upstream return a valid streamed answer. In a local server-to-loopback-upstream test, Switchyard returned HTTP 200 with the error frame and called only the first target (3/3 repetitions). A role-only frame followed by the 503 error also failed without reaching the fallback (3/3). When the first upstream instead returned an ordinary HTTP 503, the same streaming route called both targets and returned the good answer (3/3).
Expected behavior
Classify a retryable provider error delivered before answer content as a failed candidate and let the existing retry/fallback rules select the next target. Do not retry non-retryable request/auth errors or replay a request after answer content has been exposed.
Cause and scope
prepare_response_stream preflights transport errors and context overflow, but returns other first-event StreamError values as a successful stream. The OpenAI Chat decoder preserves the error message but not its numeric code in the neutral chunk; the original frame remains available in the event's preservation data. The fix should use a recognized retryable status code rather than treating every stream error as retryable.
Related: #531 addressed first-event context overflow. This is the separate retryable provider-failure path.
Environment
- Reproduced with the native Rust server on macOS arm64 at
37b78e74using a loopback OpenAI-compatible mock. The relevant path was still present on upstreammainate1c1b031. - No live-provider call was used.
Acceptance criteria
- First-event SSE 429/5xx errors use normal retry and candidate-fallback rules. No failed-target frame reaches the caller when fallback succeeds.
- A role-only frame before a retryable error can still fall back without duplicating output.
- SSE authentication/validation errors and unrecognized codes do not trigger fallback.
- An error after answer content starts does not retry another target.
- Valid streams and ordinary HTTP-error fallback retain their behavior.
Activity