Client or integration
Other
Area
Streaming
Summary
When a request exhausts its shared send budget, the three dispatch paths disagree about what the client is told.
The passthrough path returns 429 request_send_budget_exhausted and explicitly declines to blame the provider (src/server/responses/passthrough-dispatch.ts:693-700). The adapter path does not special-case SendBudgetExhaustedError: it falls through describeUpstreamConnectFailure and surfaces 502 upstream_error — "Provider unreachable" — for a refusal this proxy made itself (src/server/responses/adapter-dispatch.ts:331-335, :483-488). The runTurn path pushes a generic error event into the SSE bridge, so the client sees response.failed under HTTP 200 (src/server/responses/run-turn-execution.ts:167-180).
Separately, the terminal-guard continuation loop never consults sendBudgetExhausted() (src/server/responses/adapter-continuation.ts:260-264) while the main recovery loop does (adapter-dispatch.ts:593), so a spent budget can still same-key 429-replay on a live stream.
Both predate 2.56.0; they are recorded here so the disagreement is tracked rather than rediscovered.
Reproduction
- Configure a routed provider whose adapter runs inner retries (Kiro or Cursor, or any provider with a transient retry policy).
- Drive a request until the shared request send budget is spent, then force one more recovery leg.
- Observe
502 upstream_error from the adapter path where the passthrough path would have returned 429 request_send_budget_exhausted.
Suggested fix: in both adapter catch sites, check err instanceof SendBudgetExhaustedError before describeUpstreamConnectFailure and return the same 429 the passthrough path returns; map the runTurn case to a 429 before the SSE response is committed; and gate the continuation 429 loop on sendBudgetExhausted().
Version
2.56.0 (e4a8539)
Operating system
macOS 15.5
Provider and model
kiro / any; also reproduces on providers with a transient retry policy
Logs or error output
Provider unreachable: <host> (observed where a 429 request_send_budget_exhausted was expected)
Checks
Client or integration
Other
Area
Streaming
Summary
When a request exhausts its shared send budget, the three dispatch paths disagree about what the client is told.
The passthrough path returns
429 request_send_budget_exhaustedand explicitly declines to blame the provider (src/server/responses/passthrough-dispatch.ts:693-700). The adapter path does not special-caseSendBudgetExhaustedError: it falls throughdescribeUpstreamConnectFailureand surfaces502 upstream_error— "Provider unreachable" — for a refusal this proxy made itself (src/server/responses/adapter-dispatch.ts:331-335,:483-488). The runTurn path pushes a generic error event into the SSE bridge, so the client seesresponse.failedunder HTTP 200 (src/server/responses/run-turn-execution.ts:167-180).Separately, the terminal-guard continuation loop never consults
sendBudgetExhausted()(src/server/responses/adapter-continuation.ts:260-264) while the main recovery loop does (adapter-dispatch.ts:593), so a spent budget can still same-key 429-replay on a live stream.Both predate 2.56.0; they are recorded here so the disagreement is tracked rather than rediscovered.
Reproduction
502 upstream_errorfrom the adapter path where the passthrough path would have returned429 request_send_budget_exhausted.Suggested fix: in both adapter catch sites, check
err instanceof SendBudgetExhaustedErrorbeforedescribeUpstreamConnectFailureand return the same 429 the passthrough path returns; map the runTurn case to a 429 before the SSE response is committed; and gate the continuation 429 loop onsendBudgetExhausted().Version
2.56.0 (e4a8539)
Operating system
macOS 15.5
Provider and model
kiro / any; also reproduces on providers with a transient retry policy
Logs or error output
Checks