Skip to content

fix(llmcore): route OpenAI Responses SSE coded transient errors to _stream_with_retry (closes #753) - #767

Open
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-753-responses-sse-overload-retry
Open

fix(llmcore): route OpenAI Responses SSE coded transient errors to _stream_with_retry (closes #753)#767
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-753-responses-sse-overload-retry

Conversation

@Kailigithub

@Kailigithub Kailigithub commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #753.

OpenAI Responses can terminate an HTTP-200 stream with an error or response.failed event whose retry hint lives in a structured code / type field, not in prose like "overloaded". Codes such as server_error, rate_limit_error, service_unavailable, api_error, engine_overloaded, upstream_error, plus numeric 429 / 5xx, were slipping past the prose-only regex in _raise_if_retryable_overload and surfacing to callers as a successful stream that emitted !!!Error: ... text instead of triggering retry.

Why

Issue #753 is explicit: "Route explicit transient stream errors through _stream_with_retry when no content, reasoning, or tool output has been produced. Keep permanent failures and partial streams terminal to avoid retry storms or duplicated output." This PR implements both clauses.

How

  • New _RETRYABLE_STREAM_ERR_CODES frozenset lists the transient categories: rate_limit_error, server_error, service_unavailable, api_error, overloaded, engine_overloaded, timeout, request_timeout, upstream_error, temporary_error, too_many_requests, plus cohere-style tokens_exceeded_retry.
  • Widened prose regex now catches server (is )?busy|unavailable, temporarily unable / unavailable, engine (is )?overloaded|busy, capacity, try again later in addition to the original concurrency|retry later|overloaded|rate.?limit.
  • New _is_retryable_stream_err(err=, emsg=) accepts both shapes (OpenAI Responses nests under {error: {code, type, message}}; some providers put fields at the top level). Numeric codes 408/409/425/429/500-527/529 also retry.
  • _raise_if_retryable_overload widened to accept either a string or a dict; raises ConnectionError so _stream_with_retry's exponential backoff loop kicks in.
  • Permanent failures (invalid_request_error, context_length_exceeded, authentication_error, permission_denied, not_found, 400/404) explicitly do not retry — they remain visible as !!!Error: text in the stream. Partial streams (already-emitted content / tool calls) also stay terminal via the existing break after the helper.
  • Updated the four call sites:
    • _parse_claude_sse error event
    • _parse_openai_sse error event (api_mode=responses)
    • _parse_openai_sse response.failed event (api_mode=responses)
    • _parse_openai_json status == "failed" branch
      ...to pass the full err dict into the helper so the new coded path is taken.

Tests

tests/test_llmcore_sse_retry_codes.py (9 cases, all passing). I ran the parent-commit verification: 7/9 tests fail before the fix (matching the bug exactly — error events with coded server_error / rate_limit_error and the new "Server is busy, retry" prose pattern all slip through), 9/9 pass after.

Coverage:

  • All 13 coded transient codes route to retry via ConnectionError.
  • All 6 permanent failure codes (incl. 400/404 numeric) stay visible as text.
  • Numeric 408/409/425/429 + every 5xx in the existing _RETRYABLE set retry.
  • Nested {error: {code, ...}} shape (OpenAI Responses) retry.
  • _parse_openai_sse error + response.failed paths retry mid-stream without ever emitting the !!!Error: text chunk.
  • _parse_openai_sse permanent invalid_request_error response.failed still surfaces as !!!Error: text in the stream.
  • _parse_claude_sse error event now retries the previously-missed prose ("Server is busy, retry").
  • Pre-OpenAI Responses SSE overload errors bypass max_retries #753 prose ("overloaded") still retries — regression guard.
$ python3 -m pytest tests/test_llmcore_sse_retry_codes.py -v
... 9 passed in 0.24s

Risk / blast radius

  • Touches llmcore.py only (a single helper + 4 call sites) and a new test file.
  • The new _raise_if_retryable_overload accepts both string and dict; legacy string-only callers (none in tree) keep working via the same regex path.
  • Permanent failures remain visible. No risk of retry storm on bad requests.
  • No version-bump or interface change required.

…tream_with_retry (closes lsdefine#753)

OpenAI Responses can terminate an HTTP-200 stream with an `error` or
`response.failed` event whose retry hint lives in a structured
`code` / `type` field, not in prose like "overloaded". Codes such
as `server_error`, `rate_limit_error`, `service_unavailable`,
`api_error`, `engine_overloaded`, `upstream_error`, plus numeric 429
/ 5xx, were slipping past the prose-only regex in
`_raise_if_retryable_overload` and surfacing to callers as a successful
stream that emitted `!!!Error: ...` text instead of triggering retry.

This change:

* Introduces `_RETRYABLE_STREAM_ERR_CODES` (frozenset of coded
  transient categories) and a widened prose regex (covers
  "server is busy", "temporarily unavailable", "engine overloaded",
  "capacity", "try again later").
* Adds `_is_retryable_stream_err(err=, emsg=)` that reads `code` /
  `type` from either top-level or nested `{error: {...}}` shape and
  falls back to the regex over `message`.
* Widens `_raise_if_retryable_overload` to accept either a string
  (legacy) or a dict (SSE error payload).
* Updates the three `_parse_openai_sse` call sites
  (`error`, `response.failed`) and the `_parse_openai_json`
  `status == "failed"` branch to pass the full `err` dict so the new
  coded path is taken.
* Permanent failures (`invalid_request_error`,
  `context_length_exceeded`, `authentication_error`, `permission_denied`,
  `not_found`, 400/404) explicitly do NOT retry — they remain visible
  as `!!!Error:` text, matching the issue's expected-behavior clause.

Regression test (tests/test_llmcore_sse_retry_codes.py) verifies:

* All 13 coded transient codes route to retry via `ConnectionError`.
* All 6 permanent failures stay visible as text (no retry).
* `_parse_openai_sse` with an `error` event carrying
  `code: "server_error"` raises `ConnectionError` mid-stream.
* `_parse_openai_sse` with `response.failed` carrying
  `code: "rate_limit_error"` raises `ConnectionError` mid-stream.
* `_parse_claude_sse` `error` event now also retries the new prose
  patterns ("Server is busy, retry").
* Pre-lsdefine#753 prose path ("overloaded") still retries — regression guard.

Tested via parent-commit verification: 7/9 tests fail before the fix
(demonstrating the bug), 9/9 pass after.
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.

OpenAI Responses SSE overload errors bypass max_retries

1 participant