Classify an upstream 4xx as a request error, not a transport failure #118
No reviewers
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-proxy!118
Loading…
Reference in a new issue
No description provided.
Delete branch "upstream-error-classification"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
Fixes #114.
One trace. LiteLLM answered
400 Bad Request. agent-proxy logged it asdispatch.transport_error, retried twice more with a byte-identical body, and answered the caller:Nothing about the transport had failed. LiteLLM answered promptly on every attempt and served a different request successfully 7.5 seconds later. Its own log recorded
No fallback model group found, so no fallback was attempted either. One backend rejected one payload, and the response pointed the operator at capacity.Root cause
upstream.pyflattened everyhttpx.HTTPErrorinto a bareUpstreamError, so a status-bearing rejection and a refused connection were indistinguishable downstream. The dispatcher's only policy forUpstreamErroris retry-then-fail-over-then-AllBackendsFailed.Fix
UpstreamStatusErrorcarriesstatus_codeand a boundedbody.is_retryable_statusdraws the line where the protocol draws it: every 5xx, plus408,425,429.UpstreamRequestRejected, logged asdispatch.request_rejected. Both streaming and non-streaming paths.upstream_statusadded. A 400 is answered 400.AllBackendsFailedbecomes a subclass of a newBackendUnavailable, raised only when the chain actually offered more than one backend. The routes catch the base, so genuine chain exhaustion is still a 502.http.response.status_codeon the upstream span and records an error. That is the span-fidelity half of #106 - a 500 that left every agent-proxy span readinghas_error: falseis why the reported 0.74% error rate could not be trusted.How to verify
ward exec test- 9 new cases.test_settled_4xx_is_not_retriedasserts exactly one upstream call for a 400 withmax_retries=2.test_settled_4xx_leaves_the_breaker_closedruns four rejections against a threshold of 2 and asserts the breaker never opens.test_retryable_statuses_still_retryis parametrized over 408/425/429/500/502/503.test_upstream_rejection_reaches_the_caller_as_itselfasserts the route answers 400 with the provider's message and no "all backends failed".Test plan
ward exec format-check,lint,typecheck,pre-commitall cleanRisk
Medium, and concentrated in one place: callers that treated every failure as a 502 will now see 4xx pass through. That is the point of the change, but it is a response-contract change for anything downstream that branches on status.
sirens-echocurrently surfaces "model backend unavailable, retry shortly" for these, which will become the provider's own message.Three existing tests were updated rather than added to: a single-backend chain now raises
BackendUnavailableinstead ofAllBackendsFailed, which is the acceptance criterion asking for it.Also changed
upstream_request_rejected,upstream_5xx. Doc and cardinality assertion updated (13 -> 15).RequestOutcomevalue,upstream_rejected, kept apart fromupstream_failedin the trajectory record.docs/upstream-error-classification.md.Note
Branched off
main, independent of #116 and #117.