Skip to content

fix(models): retry 200 responses that carry a gateway error payload - #1673

Merged
Yunnglin merged 2 commits into
modelscope:mainfrom
seroze:fix/retry-choiceless-gateway-response
Aug 31, 2026
Merged

fix(models): retry 200 responses that carry a gateway error payload#1673
Yunnglin merged 2 commits into
modelscope:mainfrom
seroze:fix/retry-choiceless-gateway-response

Conversation

@seroze

@seroze seroze commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #1674

Summary

Some OpenAI-compatible gateways return an error payload with 200 OK. The OpenAI SDK deserializes it as a ChatCompletion with choices=None, so the error previously surfaced outside EvalScope's retry boundary with limited diagnostics.

This PR:

  • validates non-streaming completions inside the existing sync and async retry boundary;
  • reports the gateway error preserved in model_extra;
  • retries transient or unclassified gateway failures;
  • fails fast for deterministic 400, 401, 402, 403, 404, and 422 errors;
  • preserves the existing ValueError compatibility and streaming behavior.

Tests

Added deterministic, network-free coverage for:

  • OpenAI SDK deserialization of gateway error payloads;
  • sync and async transient-error retries;
  • retry exhaustion and missing error details;
  • non-retryable gateway client errors;
  • valid completions without unnecessary retries.

Some gateways commit `200 OK` headers before the upstream has produced
anything, holding the connection open with whitespace padding so the request
does not time out. When the request then fails, the status line is already on
the wire and cannot be retracted, so the error is appended to the body of an
otherwise successful response:

    HTTP/1.1 200 OK
    content-type: application/json

    \n         \n ... (whitespace keepalive) ...
    {"error":{"message":"Insufficient balance","code":402}}

The OpenAI SDK deserializes that into a `ChatCompletion` whose `choices` is
None, keeping the real error in `model_extra`. Nothing raises, so `retry_call`
sees a successful call and returns. `chat_choices_from_openai` then raises
ValueError — outside the retry boundary — which propagates through
`_predict_sample` and, with the default `ignore_errors=False`, discards the
entire run. Observed on OpenRouter, where a single transient upstream blip
ended a 1319-sample GSM8K run at sample 44; the same failure recurred three
times in a later 200-sample run.

Check for a choiceless completion inside `_create_and_collect`, so the failure
is raised where `retry_call` can act on it, and surface the gateway's own error
message instead of the current `id=None, model=None` report.

Responses that fail fast still return honest status codes and are unaffected:
they continue to be handled by the SDK's own retry and by
NON_RETRYABLE_OPENAI_ERRORS. Only the choices=None shape changes behaviour.

Claude-Session: https://claude.ai/code/session_0195nPGwxPjJGtQukF28BJfS

@Yunnglin Yunnglin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Yunnglin
Yunnglin merged commit 29b054d into modelscope:main Aug 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One transient gateway error aborts the whole run: choices=None raised outside the retry boundary

2 participants