Skip to content

fix: Antigravity provider — model registry, 429 cascade, non-streaming requests - #885

Closed
christopher-s wants to merge 9 commits into
cheaper-inference:mainfrom
christopher-s:antigravity-audit
Closed

christopher-s wants to merge 9 commits into
cheaper-inference:mainfrom
christopher-s:antigravity-audit

Conversation

@christopher-s

Copy link
Copy Markdown
Contributor

Summary

  • Fix Antigravity model registry to match actual Google Cloud Code API model IDs (gemini-3-pro-high/low, gemini-3-flash, etc.)
  • Prevent 429 on one model from locking out entire provider connection — use model-only lockModel() for passthrough providers
  • Fix non-streaming requests by always streaming upstream and collecting SSE into JSON response
  • Filter internal/special-purpose models from quota display (matching CLIProxyAPI exclusion list)
  • Normalize bare Pro model IDs to -low tier (matching OpenClaw convention)
  • Address SSE collection timeout: return 504 instead of silent 200 with empty content

Changes

Model Registry (providerRegistry.ts)

  • Replace stale/wrong model IDs with correct ones from fetchAvailableModels API
  • Add High/Low tier variants for Pro models
  • Remove non-existent models (gpt-5, gpt-5-mini, gemini-2.0-flash)
  • Remove internal-only models (gemini-2.5-pro — browser subagent)
  • Add getPassthroughProviders() helper for provider type detection

429 Cascade Fix (chatCore.ts, auth.ts)

  • chatCore: For passthrough providers, use lockModel() instead of connection-wide rateLimitedUntil on 429
  • auth.ts: Same pattern as secondary defense in markAccountUnavailable
  • Both use 2-minute default cooldown (aligned with COOLDOWN_MS.rateLimit)

Non-Streaming Requests (antigravity.ts)

  • Always use streaming endpoint upstream to avoid 400 errors from Cloud Code API
  • New collectStreamToResponse() method parses Gemini SSE and assembles OpenAI JSON response
  • Returns 504 Gateway Timeout with finish_reason: "length" on timeout instead of silent 200

Quota Display (usage.ts)

  • Add ANTIGRAVITY_EXCLUDED_MODELS set matching CLIProxyAPI's hardcoded list
  • Filters: chat placeholders, tab-completion, thinking variant, browser subagent

Test plan

  • Tested all 11 Antigravity models: 9 working, 2 quota-exhausted (but don't cascade)
  • Verified 429 on one model doesn't lock out other models on same connection
  • Verified bare gemini-3-pro normalizes to gemini-3-pro-low
  • Verified non-streaming requests return proper JSON
  • Verified SSE timeout returns 504 instead of silent 200

🤖 Generated with Claude Code

…avity/

- Replace stale model IDs (gemini-3.1-pro-preview, gemini-3.1-flash-lite-preview)
  with correct High/Low tier variants from fetchAvailableModels API
  (gemini-3-pro-high, gemini-3-pro-low, gemini-3.1-pro-high, gemini-3.1-pro-low, etc.)
- Remove ag/ alias prefix in favor of antigravity/ across registry, providers,
  model capabilities, combos, docs, and static model providers
- Make provider alias optional in Zod schema and guard ALIAS_TO_ID/ID_TO_ALIAS maps
- Show raw model IDs in quota display instead of unmapped display names
- Update T28 model catalog test to assert new High/Low tier models
…veUserQuota

retrieveUserQuota only returns Gemini model quotas. fetchAvailableModels
returns all models (including Claude) with per-model quotaInfo.
Models without quota data (e.g. tab-completion models) were showing 0%
because remainingFraction defaulted to 0 when absent. Now defaults to 1
so they show 100% remaining instead.
…ing requests

- Update model list to match CLIProxyAPI filtered models (remove stale
  gemini-2.5-pro, claude-sonnet-4-5, claude-sonnet-4, gemini-2.0-flash;
  sort alphabetically)
- Extend 404 model-only lockout to passthrough providers so one missing
  model doesn't lock out the entire Antigravity connection
- Always use streaming upstream endpoint (generateContent causes upstream
  400 for some models that internally convert to OpenAI format with
  stream_options); collect SSE into JSON for non-streaming clients
- Fix unlimited quota models showing 0% by checking q.unlimited before
  recalculating percentage
A 429 from one Antigravity model was marking the entire provider connection
as rate-limited, blocking ALL other models on the same account. This happened
in two places: chatCore's error classification (primary) and
markAccountUnavailable (secondary). Both now use model-only lockModel() for
passthrough providers instead of connection-wide rateLimitedUntil.

Also adds:
- Bare Pro model ID normalization (gemini-3-pro → gemini-3-pro-low)
  matching OpenClaw convention
- Internal model exclusion list for quota display, matching CLIProxyAPI
- Standardize cooldown fallback to 2 min (COOLDOWN_MS.rateLimit) in both
  chatCore and auth.ts instead of inconsistent 60s/undefined
- Return 504 Gateway Timeout instead of 200 OK when SSE collection times
  out, with finish_reason "length" to signal incomplete response

@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 antigravity provider by removing its ag alias, updating the model catalog, and transitioning to an upstream streaming-only execution model where non-streaming requests are handled via local SSE collection. It also introduces logic for 'passthrough' providers to allow model-specific rate limiting and error handling. Feedback includes suggestions to optimize the SSE collection loop for efficiency and robustness, restore the ag alias to prevent breaking changes, and simplify the initialization of the passthrough provider registry.

Comment on lines +225 to +247
const collect = async () => {
const chunks: string[] = [];
let timedOut = false;
const timeout = AbortSignal.timeout(SSE_COLLECT_TIMEOUT_MS);
try {
// eslint-disable-next-line no-constant-condition
while (true) {
if (signal?.aborted) throw new Error("Request aborted during SSE collection");
const { done, value } = await Promise.race([
reader.read(),
new Promise<never>((_, reject) =>
timeout.addEventListener("abort", () => reject(new Error("SSE collection timed out")), { once: true })
),
]);
if (done) break;
chunks.push(decoder.decode(value, { stream: true }));
}
} catch (err) {
const msg = err?.message || String(err);
timedOut = msg.includes("timed out");
log?.warn?.("SSE_COLLECT", `Error collecting SSE stream: ${msg}`);
// Fall through — return whatever was collected so far
}

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

The current implementation of collectStreamToResponse has several areas for improvement:

  1. Efficiency: A new Promise and event listener are created for the timeout in every iteration of the while loop. This can be optimized by creating the timeout promise once outside the loop.
  2. Robustness: If the stream fails for reasons other than a timeout (e.g., a network error), the catch block logs the error but allows the function to "fall through" and return a 200 status with potentially incomplete or malformed JSON. It would be more robust to rethrow non-timeout errors so they can be handled as upstream failures (e.g., 502 Bad Gateway) by the caller.
  3. Timeout Detection: Using timeout.aborted is a more reliable way to detect if the timeout signal was triggered compared to string matching on the error message.
    const collect = async () => {
      const chunks: string[] = [];
      let timedOut = false;
      const timeout = AbortSignal.timeout(SSE_COLLECT_TIMEOUT_MS);
      const timeoutPromise = new Promise<never>((_, reject) =>
        timeout.addEventListener("abort", () => reject(new Error("SSE collection timed out")), { once: true })
      );

      try {
        // eslint-disable-next-line no-constant-condition
        while (true) {
          if (signal?.aborted) throw new Error("Request aborted during SSE collection");
          const { done, value } = await Promise.race([
            reader.read(),
            timeoutPromise,
          ]);
          if (done) break;
          chunks.push(decoder.decode(value, { stream: true }));
        }
      } catch (err) {
        timedOut = timeout.aborted;
        const msg = err?.message || String(err);
        log?.warn?.("SSE_COLLECT", `Error collecting SSE stream: ${msg}`);
        if (!timedOut) throw err;
      }

antigravity: {
id: "antigravity",
alias: "ag",
alias: undefined,

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

Removing the ag alias for the antigravity provider is a breaking change. Any existing client configurations, environment variables, or scripts relying on the ag/ prefix will stop working. Unless there is a specific conflict or a strong requirement to enforce the full name, it is recommended to keep the alias for backward compatibility.

Suggested change
alias: undefined,
alias: "ag",

Comment on lines +1553 to +1565
const _passthroughProviderIds: Set<string> | null = (() => {
try {
const ids = new Set<string>();
for (const entry of Object.values(REGISTRY)) {
if (entry.passthroughModels) ids.add(entry.id);
}
return ids;
} catch { return null; }
})();

export function getPassthroughProviders(): Set<string> {
return _passthroughProviderIds ?? new Set<string>();
}

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

The initialization of _passthroughProviderIds can be simplified. The try-catch block is unnecessary as REGISTRY is defined in the same module and Object.values is a safe operation here. Additionally, if the initialization were to fail and return null, the current implementation would create a new Set on every call to getPassthroughProviders(), which is inefficient.

Suggested change
const _passthroughProviderIds: Set<string> | null = (() => {
try {
const ids = new Set<string>();
for (const entry of Object.values(REGISTRY)) {
if (entry.passthroughModels) ids.add(entry.id);
}
return ids;
} catch { return null; }
})();
export function getPassthroughProviders(): Set<string> {
return _passthroughProviderIds ?? new Set<string>();
}
/** Set of provider IDs with passthroughModels enabled — 404s are model-specific, not account-level. */
const _passthroughProviderIds = new Set(
Object.values(REGISTRY)
.filter((entry) => entry.passthroughModels)
.map((entry) => entry.id)
);
export function getPassthroughProviders(): Set<string> {
return _passthroughProviderIds;
}

@kilo-code-bot

kilo-code-bot Bot commented Apr 1, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Changes Since Previous Review (3fad847 → ff15828)

This commit continues the cleanup of non-viable Antigravity models:

  1. Image Registry - Removed gemini-2.5-flash-preview-image-generation and gemini-3.1-flash-image-preview from the Antigravity provider's model list. These models return empty responses and are not usable for image generation.

  2. Model Specs - Reduced maxOutputTokens for gemini-3.1-pro-high and gemini-3.1-pro-low from 131072 to 65535. This aligns with the actual API limits for these models.

Both changes are appropriate cleanup based on findings from earlier commits in this PR.

Previous Issues Carried Forward
File Line Status
open-sse/executors/antigravity.ts 247 Open - SSE collection optimization suggestion
open-sse/config/providerRegistry.ts 362 Open - alias removal breaking change note
open-sse/config/providerRegistry.ts 1561 Open - getPassthroughProviders simplification suggestion

These remain unresolved from previous reviews but are not blockers for this commit.

Files Reviewed (2 files)
  • open-sse/config/imageRegistry.ts - Removed non-viable image models from Antigravity
  • src/shared/constants/modelSpecs.ts - Corrected maxOutputTokens limits for Gemini 3.1 models

…play

Models removed from available list (not usable via chat completions):
- gemini-3-pro-high/low — returns empty responses, quota unusable
- gemini-2.5-flash/flash-lite — quota always exhausted on free tier
- gemini-3.1-flash-image-preview — preview variant, not functional

Models hidden from quota UI (in addition to above):
- gemini-3-flash-agent — internal agent model
- gemini-3.1-flash-lite — not usable for chat

Kept gemini-3.1-flash-image in available models (confirmed working).
Removed dead gemini-3-pro from bare Pro ID normalization.
@christopher-s

Copy link
Copy Markdown
Contributor Author

Model Cleanup — Tested and Verified

After testing all models against a live Antigravity account, several models were found to be non-viable and have been removed:

Removed from available models list

These either return empty responses, have permanently exhausted quota, or are not functional for chat completions:

  • gemini-3-pro-high/low — returns empty responses, quota unusable
  • gemini-2.5-flash/flash-lite — quota always exhausted on free tier accounts
  • gemini-3.1-flash-image-preview — preview variant, not functional

Hidden from quota UI (in addition to above)

  • gemini-3-flash-agent — internal agent model, not user-callable
  • gemini-3.1-flash-lite — not usable for chat

Final viable model list (7 models)

Model Confirmed Working
claude-opus-4-6-thinking Yes
claude-sonnet-4-6 Yes
gemini-3-flash Yes
gemini-3.1-flash-image Yes
gemini-3.1-pro-high Yes
gemini-3.1-pro-low Yes
gpt-oss-120b-medium Yes

- Reduce maxOutputTokens from 131072 to 65535 for gemini-3.1-pro-high
  and gemini-3.1-pro-low, fixing 400 "invalid argument" errors from
  Open WebUI when no max_tokens is specified (upstream limit is 65535)
- Filter non-viable models (gemini-3.1-flash-image-preview,
  gemini-2.5-flash-preview-image-generation, gemini-3-pro-high/low)
  from the live upstream API response in /api/providers/[id]/models
The models gemini-2.5-flash-preview-image-generation and
gemini-3.1-flash-image-preview were surfacing in the model catalog
via getAllImageModels() from imageRegistry.ts, not from the live
upstream API. Removed them from the image provider registry.
diegosouzapw added a commit that referenced this pull request Apr 1, 2026
# Conflicts:
#	open-sse/executors/antigravity.ts
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Merged manually into release/v3.4.2. Thanks for the contribution!

@christopher-s
christopher-s deleted the antigravity-audit branch April 8, 2026 18:38
@diegosouzapw diegosouzapw mentioned this pull request Apr 22, 2026
This was referenced Apr 30, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
# Conflicts:
#	open-sse/executors/antigravity.ts
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
# Conflicts:
#	open-sse/executors/antigravity.ts
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