Skip to content

fix(auth): cooldown Antigravity credentials on invalid_grant instead of silent retry loop#4130

Open
fengshao1227 wants to merge 2 commits into
router-for-me:devfrom
fengshao1227:fix/antigravity-invalid-grant-cooldown
Open

fix(auth): cooldown Antigravity credentials on invalid_grant instead of silent retry loop#4130
fengshao1227 wants to merge 2 commits into
router-for-me:devfrom
fengshao1227:fix/antigravity-invalid-grant-cooldown

Conversation

@fengshao1227

Copy link
Copy Markdown

Summary

  • Antigravity OAuth credentials returning HTTP 400 with invalid_grant (banned/revoked accounts) were never cooled down, causing the conductor to repeatedly select the bad credential
  • isRequestInvalidError now excludes invalid_grant errors so the conductor tries other credentials instead of returning immediately
  • MarkResult and applyAuthFailureState now treat invalid_grant like 401 unauthorized (30-minute cooldown + model suspension)

Root cause

Two independent bugs conspire:

Bug 1 — isRequestInvalidError false positive. The Antigravity API wraps invalid_grant inside an invalid_request_error envelope. isRequestInvalidError matched the outer wrapper, returning true — this told the conductor to stop immediately without trying other credentials in the pool.

Bug 2 — no cooldown in MarkResult. When the error did reach MarkResult, the default case in the statusCode switch applied no cooldown (NextRetryAfter = time.Time{}). The bad credential was immediately eligible for reselection on the next request.

Combined effect: with multiple credentials, one valid and one banned, the banned credential kept being selected and blocking requests.

Changes

File Change
conductor.go isOAuthInvalidGrantMessage + isOAuthInvalidGrantResultError helpers
conductor.go:4155 isRequestInvalidError returns false early when message contains invalid_grant
conductor.go:3736 MarkResult default case checks for invalid_grant and applies 401-like cooldown
conductor.go:4272 applyAuthFailureState default case applies same cooldown for invalid_grant
cooldown_backoff_test.go 3 new tests: MarkResult cooldown, applyAuthFailureState cooldown, isRequestInvalidError exclusion

Tests

  • go test -run "Cooldown\|Backoff\|InvalidGrant\|IsRequestInvalid" ./sdk/cliproxy/auth/ — all 22 tests pass
  • go build -o test-output ./cmd/server — compiles cleanly

Closes #4120

…trying them indefinitely

Antigravity OAuth credentials that return HTTP 400 with invalid_grant
(revoked/banned accounts) were never cooled down because:

1. isRequestInvalidError matched the outer invalid_request_error wrapper,
   causing the conductor to return immediately without trying other creds
2. MarkResult's switch on statusCode fell into the default case for 400,
   applying no cooldown — the bad credential was reselected immediately

Now:
- isRequestInvalidError returns false when the message contains
  invalid_grant, so the conductor falls through to try other credentials
- MarkResult and applyAuthFailureState treat invalid_grant like 401
  (30-minute cooldown + model suspension)

Closes router-for-me#4120
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@github-actions github-actions Bot changed the base branch from main to dev July 7, 2026 06:58

@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 introduces handling for OAuth invalid_grant errors, treating them as credential-level issues that trigger a 30-minute cooldown rather than standard invalid request payloads. It updates error classification, state management, and adds corresponding unit tests. The review feedback suggests making the invalid_grant substring check case-insensitive, simplifying the error message check using err.Error(), and explicitly clearing auth.NextRetryAfter when disableCooling is enabled to avoid preserving stale cooldowns.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +4000 to +4002
func isOAuthInvalidGrantMessage(msg string) bool {
return strings.Contains(msg, "invalid_grant")
}

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.

high

We should perform a case-insensitive check when looking for the "invalid_grant" substring. Upstream OAuth providers or intermediate API wrappers might return error codes or messages in uppercase (e.g., "INVALID_GRANT") or mixed case. Converting the message to lowercase first ensures robust detection.

Suggested change
func isOAuthInvalidGrantMessage(msg string) bool {
return strings.Contains(msg, "invalid_grant")
}
func isOAuthInvalidGrantMessage(msg string) bool {
return strings.Contains(strings.ToLower(msg), "invalid_grant")
}

Comment on lines +4004 to +4009
func isOAuthInvalidGrantResultError(err *Error) bool {
if err == nil {
return false
}
return isOAuthInvalidGrantMessage(err.Message) || isOAuthInvalidGrantMessage(err.Code)
}

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.

medium

Instead of manually checking both err.Message and err.Code separately, we can leverage the err.Error() method. This is cleaner, more maintainable, and consistent with how we check errors in other parts of the codebase (such as in applyAuthFailureState).

Suggested change
func isOAuthInvalidGrantResultError(err *Error) bool {
if err == nil {
return false
}
return isOAuthInvalidGrantMessage(err.Message) || isOAuthInvalidGrantMessage(err.Code)
}
func isOAuthInvalidGrantResultError(err *Error) bool {
if err == nil {
return false
}
return isOAuthInvalidGrantMessage(err.Error())
}

Comment on lines +4272 to +4276
if resultErr != nil && isOAuthInvalidGrantMessage(resultErr.Error()) {
auth.StatusMessage = "unauthorized"
if !disableCooling {
auth.NextRetryAfter = now.Add(30 * time.Minute)
}

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.

medium

When disableCooling is true, auth.NextRetryAfter is not explicitly cleared, which means any pre-existing cooldown timestamp might be preserved. To be consistent with other status code cases (like 401, 402, 403, 404, etc.) and to ensure cooling is correctly disabled, we should explicitly set auth.NextRetryAfter = time.Time{} when disableCooling is true.

Suggested change
if resultErr != nil && isOAuthInvalidGrantMessage(resultErr.Error()) {
auth.StatusMessage = "unauthorized"
if !disableCooling {
auth.NextRetryAfter = now.Add(30 * time.Minute)
}
if resultErr != nil && isOAuthInvalidGrantMessage(resultErr.Error()) {
auth.StatusMessage = "unauthorized"
if disableCooling {
auth.NextRetryAfter = time.Time{}
} else {
auth.NextRetryAfter = now.Add(30 * time.Minute)
}
}

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fd4df3b6b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +3741 to +3744
next := now.Add(30 * time.Minute)
state.NextRetryAfter = next
suspendReason = "unauthorized"
shouldSuspendModel = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cool down the whole invalid_grant credential

When a revoked Antigravity refresh token returns invalid_grant for one model, this only records the cooldown on that model state and suspends that single auth/model pair. isAuthBlockedForModel treats the same auth as available for a different requested model when that model has no state, so the banned credential can still be selected for every other model and recreate the retry/failover loop per model. Since invalid_grant is credential-level, block the auth across models rather than only result.Model.

Useful? React with 👍 / 👎.

Comment thread sdk/cliproxy/auth/conductor.go Outdated
Comment on lines +4274 to +4275
if !disableCooling {
auth.NextRetryAfter = now.Add(30 * time.Minute)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale retry time when cooling is disabled

When disable_cooling is true and an auth-level invalid_grant is recorded after the auth already has a retry deadline, this branch leaves auth.NextRetryAfter unchanged because it only assigns in the !disableCooling path. The other auth-level cooldown cases above explicitly zero this field when cooling is disabled, otherwise selector checks can keep blocking the auth on a stale deadline despite cooling being disabled.

Useful? React with 👍 / 👎.

- Case-insensitive invalid_grant check (strings.ToLower)
- Simplify isOAuthInvalidGrantResultError to use err.Error()
- Explicit auth.NextRetryAfter = time.Time{} when disableCooling
- Set auth-level NextRetryAfter alongside model-level so the banned
  credential is blocked across all models, not just result.Model
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.

Antigravity 凭证遇到 invalid_grant 400 后不会 cooldown,导致反复命中坏凭证

1 participant