Skip to content

fix(core): honor a RetryInfo delay of zero when classifying quota errors - #29532

Open
Linxiushen wants to merge 1 commit into
google-gemini:mainfrom
Linxiushen:fix/classifygoogleerror-drops-a-retryinfo-re
Open

Linxiushen wants to merge 1 commit into
google-gemini:mainfrom
Linxiushen:fix/classifygoogleerror-drops-a-retryinfo-re

Conversation

@Linxiushen

Copy link
Copy Markdown

Summary

classifyGoogleError() discarded a server-supplied RetryInfo delay of zero,
so a routine rate limit that the server said to retry immediately was
misclassified as a terminal quota error. That aborted retries and fired the
terminal quota / model-fallback / credits flow instead of simply retrying. This
changes two truthiness checks into comparisons against the documented sentinel
values.

Details

Root cause

parseDurationInSeconds() in packages/core/src/utils/googleQuotaErrors.ts
documents null as its "cannot parse" sentinel and returns a number otherwise,
so "0s" and "0ms" parse to 0. But classifyGoogleError() guarded the
result with a truthiness check:

const parsedDelay = parseDurationInSeconds(retryInfo.retryDelay);
if (parsedDelay) {          // 0 is falsy
  delaySeconds = parsedDelay;
}

A delay of zero — "retry immediately" — was therefore thrown away and
delaySeconds stayed undefined, indistinguishable from a malformed duration.
Two call sites downstream then took the wrong branch:

  1. Cloud Code RATE_LIMIT_EXCEEDED explicitly returns a TerminalQuotaError
    when delaySeconds === undefined, so a routine rate limit became terminal.
  2. The generic RetryInfo path was guarded by
    retryInfo?.retryDelay && delaySeconds, so a zero delay skipped the
    retryable branch and fell through to the generic tail, losing the
    Suggested retry after ... message and the delay.

The sibling Please retry in X fallback in the same function already compares
against the documented sentinel (if (retryDelaySeconds !== null)), so this was
an internal inconsistency rather than intended behaviour.

Fix

Compare against the documented sentinels at both sites: parsedDelay !== null
when reading RetryInfo, and delaySeconds !== undefined at the RetryInfo
classification branch. Two lines plus a comment; no other behaviour changes.

Unparsable durations still leave delaySeconds undefined and keep their current
behaviour (Cloud Code RATE_LIMIT_EXCEEDED with a garbage duration stays
terminal), and the 5-minute terminal threshold is untouched.

Note on the resulting delay: RetryableQuotaError's constructor maps a 0 s
delay to retryDelayMs === undefined, so retryWithBackoff uses its normal
exponential backoff rather than retrying with no pause. The behavioural change
is purely in the classification — terminal to retryable — which is the point; it
deliberately does not introduce a zero-delay hot retry loop.

Related Issues

Fixes #29049.

How to Validate

From the repository root:

# The directly affected suite
npx vitest run src/utils/googleQuotaErrors.test.ts --root packages/core

# Plus the consumers that branch on TerminalQuotaError vs RetryableQuotaError
npx vitest run src/utils/googleQuotaErrors.test.ts src/utils/retry.test.ts \
  src/utils/googleErrors.test.ts --root packages/core

What actually ran here (node v24.20.0, vitest 3.2.4, macOS):

  • googleQuotaErrors.test.ts — 52 tests, all pass.
  • googleQuotaErrors.test.ts + retry.test.ts + googleErrors.test.ts —
    3 files, 118 tests, all pass.
  • prettier --check, eslint, and tsc --noEmit -p packages/core are clean on
    the changed files.

Four cases were added to googleQuotaErrors.test.ts: Cloud Code
RATE_LIMIT_EXCEEDED with retryDelay: "0s" and with "0ms" (both now
RetryableQuotaError), a bare RetryInfo with "0s" (now
RetryableQuotaError carrying Suggested retry after 0s.), and a guard case
asserting that an unparsable duration still yields a TerminalQuotaError with
reason RATE_LIMIT_EXCEEDED.

To confirm the first three are genuine regression tests, revert only the source
file and re-run:

git stash push -- packages/core/src/utils/googleQuotaErrors.ts   # keep the tests
npx vitest run src/utils/googleQuotaErrors.test.ts --root packages/core
git stash pop

They fail on unmodified main with
expected TerminalQuotaError: Rate limit exceeded to be an instance of RetryableQuotaError
and
expected 'Too many requests' to be 'Too many requests\nSuggested retry after 0s.'.
The fourth (unparsable duration) passes on both sides by design — it is a guard
against over-correction, not a regression test.

Not run: the full npm run preflight and the integration test suites.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed) — n/a, no
    user-facing surface change beyond the corrected error classification
  • Added/updated tests (if needed)
  • Noted breaking changes (if any) — none
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

Left unchecked deliberately: this is a pure classification-logic change inside
packages/core, exercised by the unit tests above on macOS. It was not
validated by driving the CLI end-to-end on each platform/method, so I have not
claimed those boxes.

🤖 Generated with Claude Code

@Linxiushen
Linxiushen requested a review from a team as a code owner September 28, 2026 02:19
@github-actions github-actions Bot added the size/m A medium sized PR label Sep 28, 2026
@github-actions

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 101
  • Additions: +99
  • Deletions: -2
  • Files changed: 2

@google-cla

google-cla Bot commented Sep 28, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a bug where a server-provided retry delay of zero was being discarded due to an incorrect truthiness check. This caused routine rate-limit errors to be misclassified as terminal errors, prematurely aborting the retry flow. The changes ensure that zero-second delays are honored, allowing the system to properly attempt retries instead of falling back to terminal error handling.

Highlights

  • Corrected Error Classification Logic: Updated the classifyGoogleError function to correctly handle a retry delay of zero, preventing it from being incorrectly treated as a terminal error.
  • Improved Sentinel Value Checks: Replaced truthiness checks with explicit comparisons against null and undefined to ensure zero-value delays are preserved during error parsing.
  • Expanded Test Coverage: Added new test cases to verify that both '0s' and '0ms' retry delays are correctly classified as retryable, while maintaining existing behavior for unparsable durations.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the classifyGoogleError function in packages/core/src/utils/googleQuotaErrors.ts to correctly handle a zero retry delay (e.g., 0s or 0ms) instead of discarding it as a falsy value. It replaces the implicit falsy checks with explicit comparisons against null and undefined. Additionally, comprehensive unit tests have been added in googleQuotaErrors.test.ts to verify the classification of errors with zero delays, unparsable delays, and retryable quota errors. There are no review comments, and the changes look correct and well-tested.

@gemini-cli gemini-cli Bot added the area/platform Issues related to Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt label Sep 28, 2026
@Linxiushen
Linxiushen force-pushed the fix/classifygoogleerror-drops-a-retryinfo-re branch from a6a19ca to dfd9d18 Compare September 28, 2026 03:08
`parseDurationInSeconds()` documents `null` as its "cannot parse" sentinel
and returns a number otherwise, so "0s" / "0ms" parse to `0`. But
`classifyGoogleError()` guarded the result with a truthiness check
(`if (parsedDelay)`), so a valid zero delay was discarded and
`delaySeconds` stayed `undefined`.

For Cloud Code `RATE_LIMIT_EXCEEDED` that lands in the
`delaySeconds === undefined` branch, which returns a `TerminalQuotaError`:
a server telling the client to retry immediately aborted retries and
triggered the terminal quota/fallback flow instead. The same truthiness
guard on the generic RetryInfo path (`retryInfo?.retryDelay && delaySeconds`)
skipped the retryable branch for a zero delay.

Compare against the documented sentinels instead (`parsedDelay !== null`
and `delaySeconds !== undefined`). Unparsable durations still fall through
to the existing behavior.

Fixes google-gemini#29049
@Linxiushen
Linxiushen force-pushed the fix/classifygoogleerror-drops-a-retryinfo-re branch from dfd9d18 to e3b42f0 Compare September 28, 2026 03:39
@Linxiushen

Copy link
Copy Markdown
Author

The cla/google failure is in the commit metadata rather than the code. On the previous commit the check listed the author (@Linxiushen) as covered and flagged only a Co-Authored-By: trailer whose address has no CLA on file.

Commit e3b42f0a1 drops that trailer. The content is unchanged: the amended commit has the same tree hash (798a64a1e34cb5f7a22ecbdddc0c3357e7c6f15c) as the previous one, so the diff, the fix and the added tests are identical.

Re-verified locally on the amended commit: googleQuotaErrors.test.ts, retry.test.ts and googleErrors.test.ts pass (3 files, 118 tests), and prettier --check, eslint and tsc --noEmit -p packages/core are clean on the changed files.

cla/google should pass on the new commit; if it reports a stale result, the "New Contributors" rescan link in the check output refreshes it.

@chrikrah chrikrah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Linxiushen I would merge this. I reverted googleQuotaErrors.ts to 2fe7c2d at e3b42f0 and kept your tests. The three zero-delay cases then fail, and the other 49 still pass.

$ cd packages/core && npx vitest run src/utils/googleQuotaErrors.test.ts   # at e3b42f0, node v22.23.3
Tests  52 passed (52)
# src/utils/googleQuotaErrors.ts reverted to 2fe7c2d, tests kept
Tests  3 failed | 49 passed (52)
# not run: src/utils/retry.test.ts and the cli package

The limit: 0 fail-fast from #27698 runs before the RetryInfo step, so a zero-quota response that also carries retryDelay: "0s" stays terminal.

@luisfelipe-alt, you wrote the last three changes to this classifier. #29049 is still status/need-triage, and gemini-lifecycle-manager.cjs closes a pull request without help wanted a week after its nudge. Can this one get the label?

@gemini-cli

gemini-cli Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

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

area/platform Issues related to Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: RetryInfo delay of '0s' is treated as missing, misclassifying retryable rate limits as terminal

2 participants