Repository navigation
fix: Antigravity provider — model registry, 429 cascade, non-streaming requests - #885
christopher-s wants to merge 9 commits into
Conversation
…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
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
The current implementation of collectStreamToResponse has several areas for improvement:
- Efficiency: A new
Promiseand event listener are created for the timeout in every iteration of thewhileloop. This can be optimized by creating the timeout promise once outside the loop. - Robustness: If the stream fails for reasons other than a timeout (e.g., a network error), the
catchblock 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. - Timeout Detection: Using
timeout.abortedis 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, |
There was a problem hiding this comment.
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.
| alias: undefined, | |
| alias: "ag", |
| 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>(); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } |
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Overview
Changes Since Previous Review (3fad847 → ff15828)This commit continues the cleanup of non-viable Antigravity models:
Both changes are appropriate cleanup based on findings from earlier commits in this PR. Previous Issues Carried Forward
These remain unresolved from previous reviews but are not blockers for this commit. Files Reviewed (2 files)
|
…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.
Model Cleanup — Tested and VerifiedAfter testing all models against a live Antigravity account, several models were found to be non-viable and have been removed: Removed from available models listThese either return empty responses, have permanently exhausted quota, or are not functional for chat completions:
Hidden from quota UI (in addition to above)
Final viable model list (7 models)
|
- 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.
# Conflicts: # open-sse/executors/antigravity.ts
|
Merged manually into |
# Conflicts: # open-sse/executors/antigravity.ts
# Conflicts: # open-sse/executors/antigravity.ts
Summary
lockModel()for passthrough providers-lowtier (matching OpenClaw convention)Changes
Model Registry (
providerRegistry.ts)fetchAvailableModelsAPIgetPassthroughProviders()helper for provider type detection429 Cascade Fix (
chatCore.ts,auth.ts)lockModel()instead of connection-widerateLimitedUntilon 429markAccountUnavailableCOOLDOWN_MS.rateLimit)Non-Streaming Requests (
antigravity.ts)collectStreamToResponse()method parses Gemini SSE and assembles OpenAI JSON responsefinish_reason: "length"on timeout instead of silent 200Quota Display (
usage.ts)ANTIGRAVITY_EXCLUDED_MODELSset matching CLIProxyAPI's hardcoded listTest plan
gemini-3-pronormalizes togemini-3-pro-low🤖 Generated with Claude Code