Repository navigation
fix(google): distinguish Antigravity SDK prompt refusals from quota limits - #63
Conversation
PBJ-2
left a comment
There was a problem hiding this comment.
Thanks for this. The reproduction is careful, and turning a misleading 429 into an explanation is the right direction. Tests pass locally (the two new files, plus the google-adapter, antigravity, pool and translate suites). Two things need to change before merge, plus a few small ones.
1. The predicate may not separate this refusal from real quota exhaustion
SDK_PARAGRAPH is not a narrowing condition in practice. Agent SDK-based clients, which covers Code tab sessions, send it on every request, and I found it in outbound requests on my machine. What remains is "429 + the generic Resource has been exhausted (e.g. check quota). body + no details + no Retry-After". As the PR itself says, Google can send that same body for other causes, and other projects have reported this exact body on genuine exhaustion. A misclassified real quota 429 now becomes a 400: the client stops retrying, and the account's peers are never tried.
Could the classification also require evidence that the account still has quota? For example, the account's last usage snapshot (remainingFraction in providers/google/usage.ts) showing quota left for that model, falling back to the ordinary 429 path when there is no snapshot or it is exhausted.
2. Route fallback stops working for this request
Before this change, the 429 cooled the provider and the next request went to the route's fallback target. A 400 is non-retryable in classify, so provider health never changes, and chooseTarget keeps picking the same Antigravity slot. Every turn then fails with the same 400, and the configured fallback is never used. That collides with one of our acceptance rules: remote-control workers must survive upstream failure.
The refusal happens before any byte is written, so the turn is still replaceable. Please return it to the proxy as a distinct outcome and retry the same request once on the route's fallback target, sending the explanatory 400 only when there is no fallback. If you would rather keep that out of scope, say so explicitly in ARCHITECTURE §4d-ii and I can follow up.
Smaller
docs/ARCHITECTURE.md: the §4d intro still says no Antigravity subscription exists in this environment, which the new §4d-ii measurement contradicts. Please scope it to when it was written.- "Antigravity Claude Opus 5.5" is not an id in the static table in
antigravity.ts. Please record the wire id you actually sent so the measurement can be reproduced. ANTIGRAVITY_PROMPT_REFUSAL_MESSAGEsays changing Google accounts will not fix it, but only the same account was measured. Something like "retrying or waiting on this account will not fix it" would match the evidence.prompt-refusal.ts: parenthesisee.details === undefined || (Array.isArray(e.details) && e.details.length === 0)for readability.
Require fresh account/project quota evidence for the requested model group before distinguishing generic Antigravity 429 responses from real limits. Return prompt refusals unwritten and retry one configured route fallback with the original request, preserving account health and final telemetry. Document measured wire IDs and evidence limits, narrow the error wording, and add quota and end-to-end fallback regression coverage. Co-Authored-By: Claude Code <noreply@anthropic.com>
PBJ-2
left a comment
There was a problem hiding this comment.
Thanks — both blocking points are addressed: the refusal now needs fresh quota evidence for the account and model, and an unwritten refusal retries once on the route's fallback. Typecheck and the full suite pass here (1084 passed, 0 failed). I'll follow up on main to clone the request only when the Google path can re-enter, and to give the retry loop its own indentation.
Antigravity can return
429 RESOURCE_EXHAUSTEDfor a Claude Agent SDK system prompt while the same account immediately answers compatible requests. ClaudeRipple currently treats that refusal as a rate limit: it cools the account, tries the same rejected prompt on its peers, and sends a retryable 429 back to Claude Code.This change recognizes the narrowly reproduced HTTP refusal and returns
400 invalid_request_errorwith an explanation. It leaves the account available for compatible requests and recordsrefusal: "prompt-incompatible"together with the upstream 429 in the request note.Scope
The classification applies only to the Antigravity account path, after an upstream HTTP 429, when all of the following are present:
You are a Claude agent, built on Anthropic's Claude Agent SDK.429, statusRESOURCE_EXHAUSTED, and messageResource has been exhausted (e.g. check quota).Retry-Afterheader and no nonempty error details.The request is forwarded unchanged; this is not a prompt rewrite. Explicit quota/reset information, unknown error shapes, other status codes, and API-key Google requests keep their existing handling. HTTP-200 error frames are outside this change because they were not part of the live reproduction. Google may use an indistinguishable generic response for another cause; the predicate is deliberately conservative and does not claim to know the backend's internal policy.
Live reproduction and verification
Tested on Windows with Node 24.16.0 and Claude Code 2.1.289, using an isolated adapter and the same authorized Google account. The installed daily router was not replaced or restarted.
coolingprompt-incompatible; accountreadyapi_retryevents or rate-limit backoff; two HTTP requests were observed, so this is not a claim of a single client requestThe first reproduction also checked OAuth, project identity, model discovery, short generation, streaming, and output-token limits. Compatible requests succeeded; reducing the original request's output ceiling alone did not resolve the refusal. No credentials, account identifiers, captured user prompts, or private transcripts are included here.
Automated validation
429 !== 400; both pass with the change.0600file-mode assertion ingoogle-antigravity.test.ts; unmodified base0980586has the identical failure (71 passed, 1 failed).npm run typecheck: passed, including remote web/relay configs.npm run buildfails withspawnSync npm.cmd EINVALon both base and branch under this Windows/Node environment; the repository build script is unchanged.git diff --check: passed.The architecture notes distinguish the new live observation from the older source-derived wire assumptions. The implementation is original TypeScript; the related projects were used as behavioral references only.
Related evidence: OpenCodex #2899, OpenCodex PR #2961, Magpie #666, and Magpie PR #873. The OpenCodex patch rewrites one model's prompt; this change instead fixes refusal classification and account handling.