fix(ai): match retryable status codes only as standalone numbers

Status codes inside request ids or other numbers no longer make an error retryable.

closes #11057
This commit is contained in:
David Brailovsky 2026-10-21 11:59:38 +02:00
parent 29057ece3e
commit 6e491cfa95
3 changed files with 11 additions and 7 deletions

View file

@ -23,6 +23,7 @@
- Fixed whitespace-only tool output (for example a bare `\r\n` from bash) being sent as-is to OpenAI Completions, OpenAI Responses, and Google providers; it now uses the same placeholder as empty output, since some providers reject whitespace-only tool content with a 400 ([#8720](https://github.com/earendil-works/pi/issues/8720))
- Fixed automatic retry not triggering for gateway upstream failures such as `upstream_error: Upstream request failed`, `upstream_stream_read_error`, and `Upstream HTTP/2 stream failed` ([#10920](https://github.com/earendil-works/pi/issues/10920))
- Fixed automatic retry not triggering for `stream disconnected before completion` errors relayed by proxies that wrap the Codex CLI ([#11057](https://github.com/earendil-works/pi/issues/11057))
- Fixed automatic retry treating digits inside request ids or other numbers (for example `req_011CU503xyz` or `5242880 bytes`) as retryable HTTP status codes ([#11057](https://github.com/earendil-works/pi/issues/11057))
- Fixed OpenAI Responses deferred tool loads rewriting the top-level tool list, and losing the prompt cache, on every load after a tool removal. Models with `additional_tools` now declare all tools in `additional_tools` items, starting with one at the head of the input, so removals (announced as text) and redefinitions no longer rewrite the prefix. Tool search and Kimi tool messages rewrite the top-level tools once per removal or redeclaration and keep later loads anchored ([#10940](https://github.com/earendil-works/pi/issues/10940))
## [1.3.0] - 2026-10-15

View file

@ -39,13 +39,8 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
"model is at capacity",
"rate.?limit",
"too many requests",
"429",
"500",
"502",
"503",
"504",
"520",
"524",
// Status codes must not be part of a longer word or number, e.g. a request id or byte count (#11057).
"(?<![a-z0-9])(?:429|500|502|503|504|520|524)(?![a-z0-9])",
// Also matches "The service is temporarily unavailable" (#10965).
"service.?(?:is.?)?(?:temporarily.?)?unavailable",
"server.?error",

View file

@ -156,6 +156,14 @@ describe("provider retry classification", () => {
).toBe(true);
expect(isRetryableAssistantError(fauxAssistantMessage("not an error"))).toBe(false);
});
it.each([
// Regression for #11057: status codes inside ids or other numbers are not status codes.
'401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"},"request_id":"req_011CU503xyz"}',
"400 image exceeds 5 MB maximum: 5316784 bytes > 5242880 bytes",
])("does not match status codes inside ids or longer numbers: %s", (errorMessage) => {
expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(false);
});
});
describe("retryDelayMs", () => {