fix(ai): retry node-fetch 'request to ... failed' errors

Google Vertex ADC token refresh failures are reported by node-fetch as
'request to <url> failed, reason: ...' and were never auto-retried.

fixes #10996
This commit is contained in:
David Brailovsky 2026-10-16 10:47:57 +02:00
parent 790271cd02
commit 0fbdbd3f8a
3 changed files with 12 additions and 0 deletions

View file

@ -25,6 +25,7 @@
- Fixed OpenCode Go MiniMax M2.7 failing with "Model does not support this protocol" by using the Anthropic Messages API
- Fixed automatic retry not triggering for gateway `Unable to forward upstream stream` errors ([#10920](https://github.com/earendil-works/pi/issues/10920))
- Fixed automatic retry not triggering for `The service is temporarily unavailable` errors, which also made compaction fail on the first attempt ([#10965](https://github.com/earendil-works/pi/issues/10965))
- Fixed automatic retry not triggering when the Google Vertex OAuth token refresh fails with a transient network error such as `request to https://oauth2.googleapis.com/token failed, reason:` ([#10996](https://github.com/earendil-works/pi/issues/10996))
- Fixed OpenAI-compatible responses showing thinking after the answer when a proxy such as LiteLLM sends reasoning and content in the same stream chunk ([#10926](https://github.com/earendil-works/pi/issues/10926))
- Fixed Gemini 4 Argon requests on Google Vertex failing with "Thinking budget is not supported", and its thinking level being ignored on the Gemini API, by sending `thinkingLevel` for Gemini 4 and later models ([#10890](https://github.com/earendil-works/pi/issues/10890))
- Fixed Xiaomi MiMo models failing with a 400 at the `minimal` thinking level by sending only the documented thinking on/off toggle and exposing only the `off` and `high` levels ([#10818](https://github.com/earendil-works/pi/issues/10818))

View file

@ -66,6 +66,9 @@ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
"connection.?lost",
"other side closed",
"fetch failed",
// node-fetch system errors, e.g. the Google ADC OAuth token refresh used by
// google-vertex. The reason is often empty (#10996).
"request to \\S+ failed, reason:",
"getaddrinfo",
"ENOTFOUND",
"EAI_AGAIN",

View file

@ -79,6 +79,14 @@ describe("provider retry classification", () => {
expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true);
});
it.each([
// Regression for #10996: node-fetch errors from the google-vertex OAuth token refresh.
"request to https://oauth2.googleapis.com/token failed, reason:",
"request to https://oauth2.googleapis.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:9",
])("matches node-fetch request failure wording: %s", (errorMessage) => {
expect(isRetryableAssistantError(fauxAssistantMessage("", { stopReason: "error", errorMessage }))).toBe(true);
});
it("matches OpenAI Responses streams that end before terminal events", () => {
expect(
isRetryableAssistantError(