Skip to content

feat(google-genai): retry transient chat requests before the response starts - #243

Draft
erzaehlsalex wants to merge 3 commits into
agdevhq:mainfrom
erzaehlsalex:fix/google-genai-request-retry
Draft

erzaehlsalex wants to merge 3 commits into
agdevhq:mainfrom
erzaehlsalex:fix/google-genai-request-retry

Conversation

@erzaehlsalex

Copy link
Copy Markdown
Contributor

What

Google GenAI and Vertex chat models now retry transient request failures before the response starts. Network failures without an HTTP status are now classified as ServiceUnavailableError.

  • @core-ai/core-ai: new isTransportError(). It walks the cause chain, including AggregateError.errors, for socket and Node fetch codes: UND_ERR_SOCKET, UND_ERR_HEADERS_TIMEOUT, UND_ERR_BODY_TIMEOUT, UND_ERR_CONNECT_TIMEOUT, ECONNRESET, ETIMEDOUT and EPIPE.
  • @core-ai/google-genai:
    • wrapGoogleError maps transport failures to ServiceUnavailableError, right after the abort check. Before this they became a generic ProviderError.
    • generateContent and generateContentStream go through a new withRequestRetry.
      • It retries RetryableProviderErrors (rate limit, overload, 5xx, transport) up to maxRetries times, with a default of 2.
      • Backoff is 1 s, then 2 s, with ±20 % jitter, or the provider's Retry-After when that is at most 20 s. A longer Retry-After fails fast.
      • An abort during the backoff rejects immediately with AbortedError.
      • When retries run out, the last typed error is thrown unchanged.
    • New maxRetries provider option; 0 disables retries. It also applies to a supplied client.
  • @core-ai/google-vertex: passes maxRetries through.

Only the call that opens the request is retried. The SDK resolves generateContentStream once response headers arrive, so errors raised while consuming a stream are never retried and output can't be duplicated. The existing in-band stream error test now also asserts a single call.

Why

  • Gemini is the only provider here that never retries. The OpenAI and Anthropic SDKs retry failed requests twice by default, and their core-ai providers inherit that. @google/genai only retries when httpOptions.retryOptions is set, and core-ai never sets it.
  • Production impact: over one week, a production app on Vertex Gemini lost 23 chat turns to failures that a retry would have recovered: 19 × 429 RESOURCE_EXHAUSTED, 3 × UND_ERR_HEADERS_TIMEOUT and 1 × socket reset.
  • Why not the SDK's built-in retryOptions (p-retry 4.6.2):
    • It retries a caller's abort, a DOMException AbortError, with 1 s and 2 s backoff, so cancelling lags about 3 s.
    • Once attempts run out it throws Error('Retryable HTTP Error: …') instead of the response. The status is lost, and wrapGoogleError can no longer produce RateLimitError.

Tests

  • Unit tests:
    • isTransportError: own code, nested cause, AggregateError, unrelated errors, cyclic causes.
    • wrapGoogleError: TypeError('fetch failed') with an UND_ERR_HEADERS_TIMEOUT cause → ServiceUnavailableError.
    • withRequestRetry:
      • exponential backoff timing
      • exhausted retries keep the last typed error
      • honoured and capped Retry-After
      • no retry for generic, quota, context-length and abort errors
      • maxRetries: 0
      • abort during the backoff and an already-aborted signal
    • Chat model: a stream retried after a pre-response failure, and abort during the backoff.
    • Vertex: maxRetries passthrough and the default retry.
  • npm run test passes for @core-ai/core-ai, @core-ai/google-genai and @core-ai/google-vertex.
  • npm run release:check (build, lint, check-types) passes.
  • The live e2e run (npm run test:e2e:google-vertex) did not run, because no Vertex credentials are configured locally.

Not in this PR (follow-ups)

  • mapStreamErrors passes every TypeError through as a language error. undici reports mid-stream socket and body timeouts as TypeError: terminated, so those still reach callers unmapped, for all providers.
  • The other providers' wrap*Error helpers could use isTransportError as well.

🤖 Generated with Claude Code

Alex Augustin and others added 3 commits October 22, 2026 11:48
Walks the cause chain (including AggregateError.errors) for undici and socket failure codes, so provider wrappers can classify network failures that carry no HTTP status as transient.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Node fetch failure such as a headers timeout or socket reset reached callers as a generic ProviderError, so it could not be told apart from a rejected request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… starts

The OpenAI and Anthropic SDKs retry failed requests twice by default; the Google SDK only does so when retryOptions is set, and its built-in retry delays aborts and loses the error status. Retry rate limits, overload, 5xx and network failures here instead, up to maxRetries (default 2), honouring Retry-After up to 20 s. generateContentStream resolves on the response headers, so stream errors are never retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit 07a7e1b.

Approve preview

This branch has not been deployed

No deployments
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.

1 participant