fix(llmcore): route OpenAI Responses SSE coded transient errors to _stream_with_retry (closes #753) - #767
Open
Kailigithub wants to merge 1 commit into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes #753.
OpenAI Responses can terminate an HTTP-200 stream with an
errororresponse.failedevent whose retry hint lives in a structuredcode/typefield, not in prose like "overloaded". Codes such asserver_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_overloadand 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_retrywhen 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
_RETRYABLE_STREAM_ERR_CODESfrozenset 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-styletokens_exceeded_retry.server (is )?busy|unavailable,temporarily unable / unavailable,engine (is )?overloaded|busy,capacity,try again laterin addition to the originalconcurrency|retry later|overloaded|rate.?limit._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_overloadwidened to accept either a string or a dict; raisesConnectionErrorso_stream_with_retry's exponential backoff loop kicks in.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 existingbreakafter the helper._parse_claude_sseerrorevent_parse_openai_sseerrorevent (api_mode=responses)_parse_openai_sseresponse.failedevent (api_mode=responses)_parse_openai_jsonstatus == "failed"branch...to pass the full
errdict 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 codedserver_error/rate_limit_errorand the new "Server is busy, retry" prose pattern all slip through), 9/9 pass after.Coverage:
ConnectionError._RETRYABLEset retry.{error: {code, ...}}shape (OpenAI Responses) retry._parse_openai_sseerror+response.failedpaths retry mid-stream without ever emitting the!!!Error:text chunk._parse_openai_ssepermanentinvalid_request_errorresponse.failedstill surfaces as!!!Error:text in the stream._parse_claude_sseerrorevent now retries the previously-missed prose ("Server is busy, retry").Risk / blast radius
llmcore.pyonly (a single helper + 4 call sites) and a new test file._raise_if_retryable_overloadaccepts both string and dict; legacy string-only callers (none in tree) keep working via the same regex path.