Skip to content

fix(google): distinguish Antigravity SDK prompt refusals from quota limits - #63

Merged
PBJ-2 merged 2 commits into
PBJ-2:mainfrom
csa906:fix/antigravity-sdk-prompt-refusal
Oct 19, 2026
Merged

PBJ-2 merged 2 commits into
PBJ-2:mainfrom
csa906:fix/antigravity-sdk-prompt-refusal

Conversation

@csa906

@csa906 csa906 commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Antigravity can return 429 RESOURCE_EXHAUSTED for 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_error with an explanation. It leaves the account available for compatible requests and records refusal: "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:

  • The exact standalone SDK identity paragraph in the outbound system instruction: You are a Claude agent, built on Anthropic's Claude Agent SDK.
  • The observed JSON error: code 429, status RESOURCE_EXHAUSTED, and message Resource has been exhausted (e.g. check quota).
  • No Retry-After header 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.

Check Observed result
Original SDK request, Antigravity Claude Opus 5.5 Upstream 429
Same request with only the SDK identity paragraph removed, diagnostic control HTTP 200
SDK identity paragraph alone in the system instruction Upstream 429
Original / paragraph-removed pair on Gemini 3.8 Flash 429 / 200
Unmodified baseline adapter, original Opus request Client 429; account cooling
Patched adapter, original Opus and Gemini requests Client 400; prompt-incompatible; account ready
Compatible request immediately after each patched refusal HTTP 200 on the same account
Real Claude Code through the patched adapter Exited with the explanatory 400, no api_retry events or rate-limit backoff; two HTTP requests were observed, so this is not a claim of a single client request

The 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

  • Added 11 regression tests across the new refusal suite and the existing CONNECT/TLS proxy suite. They cover streaming/non-streaming HTTP errors, preservation of the outbound prompt, one account attempt per adapter call, no account cooldown, immediate compatible reuse, request-log metadata, malformed/unknown responses, genuine quota/reset signals, and the API-key path.
  • Red/green check before implementation: the two initial refusal regressions failed with 429 !== 400; both pass with the change.
  • New refusal suite plus proxy suite: 14 passed, 0 failed.
  • Google, proxy, pool, provider-health, and request-log suites: 82 passed, 1 failed. The one failure is the Windows 0600 file-mode assertion in google-antigravity.test.ts; unmodified base 0980586 has the identical failure (71 passed, 1 failed).
  • npm run typecheck: passed, including remote web/relay configs.
  • Package build: passed using a local build-only launcher shim that invokes npm's JavaScript entry point. Plain npm run build fails with spawnSync npm.cmd EINVAL on both base and branch under this Windows/Node environment; the repository build script is unchanged.
  • Full-suite attempts under isolated user profiles did not finish on either base or branch and were terminated. Both emitted the same 11 failure names before hanging; this PR does not claim a passing full suite.
  • 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.

@PBJ-2 PBJ-2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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_MESSAGE says 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: parenthesise e.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 PBJ-2 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

@PBJ-2
PBJ-2 merged commit 4fbec6b into PBJ-2:main Oct 19, 2026
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.

2 participants