Skip to content

llm: retry an error the server reports mid-stream - #220

Merged
shellygr merged 3 commits into
masterfrom
shelly/anthropic-midstream-error-retry
Sep 7, 2026
Merged

llm: retry an error the server reports mid-stream#220
shellygr merged 3 commits into
masterfrom
shelly/anthropic-midstream-error-retry

Conversation

@shellygr

@shellygr shellygr commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

should_retry decides whether a provider failure is transient, and the run-wide retry floor
resumes a graph from its checkpoint when it is. For an APIStatusError that decision reads the
status code: 408/409/429 and every 5xx are retryable.

An error the server reports part-way through a stream never gets a status code of its own. It
arrives as an SSE error event on the already-open 200 response, and the SDK builds the exception
from that response. _make_status_error finds no mapping for 200, so it hands back the base
APIStatusError, carrying status_code == 200 and naming the real condition (overloaded_error,
api_error) only in the body. should_retry sees the 200, calls the failure deterministic, and it
propagates on the first attempt.

The damage goes past the one task. The property-extraction fan-out is gathered without
return_exceptions, so the first such error ends the run before formalization and discards the
component analysis, harness and autosetup work already done.

How

When the status code cannot speak for the failure, read the payload's error.type and accept the
same conditions the status roster accepts: overloaded, api_error, rate limit, timeout. A body of any
other shape stays non-retryable (the SDK leaves it as raw text when the payload does not parse), and
so does a mid-stream invalid_request_error, which is as deterministic as its 400 twin.

Builds on #217, which covers the other half of the same gap: a connection that drops mid-stream.
Merge that one first; this PR targets its branch.

Testing

tests/test_anthropic_retry.py covers both mid-stream conditions, the mid-stream request error, an
unparsed body, and a real 529 to keep the status-code path honest. Local: pytest 1189 passed (the
test_rag_db.py errors are a postgres container that will not start on this machine), pyright clean
on the CI target set.

jar-ben and others added 2 commits September 6, 2026 12:36
An error event that arrives inside an open stream rides the 200 response the SDK
is already reading, so the SDK builds a plain APIStatusError from it.
should_retry keys on the status code, sees 200, and treats the failure as
deterministic. An overloaded or internal-server error delivered that way then
takes the whole run down on its first attempt, without the run-wide retry floor
ever being consulted.

Classify those by the payload's error.type instead, accepting the same four
conditions the status roster already accepts. A status code that can speak for
itself still decides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shellygr
shellygr changed the base branch from jaroslav/anthropic-stream-retry to master September 7, 2026 18:24
@shellygr
shellygr enabled auto-merge (squash) September 7, 2026 18:24
…dstream-error-retry

# Conflicts:
#	composer/llm/anthropic.py
#	tests/test_anthropic_retry.py
@shellygr
shellygr merged commit 580a808 into master Sep 7, 2026
2 checks passed
@shellygr
shellygr deleted the shelly/anthropic-midstream-error-retry branch September 7, 2026 20:15
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.

2 participants