Skip to content

[bug] Retryable pre-answer SSE errors bypass candidate fallback #1132

Open

Description

@cpakkamisaac-sae

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 37b78e74 using a loopback OpenAI-compatible mock. The relevant path was still present on upstream main at e1c1b031.
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions