From 77f39478c0331a0b94ccc5dfa45701bafedda6fc Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 14 Oct 2026 01:58:03 +0900 Subject: [PATCH 1/5] feat(google): add Gemini 4 Argon (gemini-4-argon) --- .../src/content/docs/guides/providers.md | 5 +++++ scripts/model-metadata.source.json | 19 +++++++++++++++++++ src/adapters/cursor/catalog.ts | 8 ++++++++ src/adapters/cursor/effort-map.ts | 3 +++ src/generated/model-metadata.ts | 2 +- src/providers/registry/entries-core.ts | 9 ++++++--- src/usage/expected-prices.ts | 13 +++++++++++++ .../adapters/google/google-hardening.test.ts | 5 ++++- tests/usage/usage-cost.test.ts | 5 +++++ 9 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 87f7a28b3e6..dd6e09324eb 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -146,6 +146,11 @@ login-only too: `ocx login google-antigravity` signs in with your Google account Assist wire, and the `google` preset beside it is the AI Studio Gemini API — a different product reached with its own key, not a key mode for the same login. +Gemini 4 Argon (`gemini-4-argon`) is listed on the `google` preset with low, medium (the +default), and high thinking levels. Google offers it on the paid Gemini API tier only, not the free +tier. On `google-antigravity`, Google currently limits Argon to AI Ultra and eligible Enterprise +accounts, so OpenCodex shows it there only when live model discovery returns it for your account. + To check which mode a provider is actually using, open it on the Providers page: the **Connection** block's **Authentication** row reads `OAuth`, `API key`, `ChatGPT passthrough`, `Local`, or `No key needed`. It is a provider-level setting, so the account rows below it do not repeat it. diff --git a/scripts/model-metadata.source.json b/scripts/model-metadata.source.json index 12a1bac4f79..df69425f420 100644 --- a/scripts/model-metadata.source.json +++ b/scripts/model-metadata.source.json @@ -13059,6 +13059,25 @@ "maxLevel": "high" } }, + "gemini-4-argon": { + "id": "gemini-4-argon", + "name": "Gemini 4 Argon", + "api": "google-generative-ai", + "provider": "google", + "baseUrl": "https://generativelanguage.googleapis.com/v1beta", + "reasoning": true, + "input": [ + "text", + "image" + ], + "contextWindow": 1048576, + "maxTokens": 65536, + "thinking": { + "mode": "google-level", + "minLevel": "low", + "maxLevel": "high" + } + }, "gemini-flash-latest": { "id": "gemini-flash-latest", "name": "Gemini Flash Latest", diff --git a/src/adapters/cursor/catalog.ts b/src/adapters/cursor/catalog.ts index 726e7870c5c..a936f71312e 100644 --- a/src/adapters/cursor/catalog.ts +++ b/src/adapters/cursor/catalog.ts @@ -244,6 +244,14 @@ export const CURSOR_CAPABILITIES: Record = { defaultVariant: "regular", variants: { regular: { levels: ["low", "medium", "high"] } }, }, + // 261014 preemptive, like gemini-3.8-flash: Cursor's models page lists Gemini 4 Argon, but the + // live GetUsableModels id is unverified. Inert until the live roster lists this id. + "gemini-4-argon": { + displayName: "Gemini 4 Argon", + window: CONTEXT_GEMINI, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high"] } }, + }, // Seeded from the live GetUsableModels roster attached to #4820, which advertises six // muse-spark-1.3 effort variants. The ladder stops at xhigh on purpose: see the matching // effort-map entry for why Cursor advertising `-max` is not evidence that it runs. diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index 11b3b0c8330..2cd9630df24 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -56,6 +56,9 @@ const CURSOR_MODEL_EFFORT_TIERS: Record = { // glm-5.3 was. Google documents low/medium/high with no `minimal` for this generation, // unlike 3.6. The seed is inert until Cursor's live roster lists the id. "gemini-3.8-flash": ["low", "medium", "high"], + // 261014 preemptive: Gemini 4 Argon, low/medium/high with no `minimal` (a documented 400). + // Inert until Cursor's live roster lists the id. + "gemini-4-argon": ["low", "medium", "high"], // Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually // lists for each family, which is not always the same set the non-thinking id carries: // 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium. diff --git a/src/generated/model-metadata.ts b/src/generated/model-metadata.ts index fc6aef86654..d710d14d90f 100644 --- a/src/generated/model-metadata.ts +++ b/src/generated/model-metadata.ts @@ -47,7 +47,7 @@ const DATA: Record = { "azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]], "cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]], "deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]], - "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-3.8-flash",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]], + "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-3.8-flash",1048576,65536,"text,image",1],["gemini-4-argon",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]], "minimax": [["MiniMax-M2",196608,128000,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.5",204800,131072,"text",1,null,0.3,1.2,0.03,0.375],["MiniMax-M2.5-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["MiniMax-M2.5-lightning",204800,32000,"text",1,null,0.3,2.4,0,0],["MiniMax-M2.7",204800,131072,"text",1,null,0.3,1.2,0.06,0.375],["MiniMax-M2.7-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["minimax-m3",512000,128000,"text,image",1,null,0.6,2.4,0.12,0],["MiniMax-M3",1000000,128000,"text,image,video",1,null,0.3,1.2,0.06,0],["MiniMax-M3.1-Flash-Preview",1000000,null,"text,image,video",1]], "mistral": [["codestral-latest",256000,4096,"text",0,null,0.3,0.9,0,0],["devstral-2512",262144,262144,"text",0,null,0.4,2,0,0],["devstral-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-medium-2507",128000,128000,"text",0,null,0.4,2,0,0],["devstral-medium-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-small-2505",128000,128000,"text",0,null,0.1,0.3,0,0],["devstral-small-2507",128000,128000,"text",0,null,0.1,0.3,0,0],["labs-devstral-small-2512",256000,256000,"text,image",0,null,0,0,0,0],["magistral-medium-latest",128000,16384,"text",1,null,2,5,0,0],["magistral-small",128000,128000,"text",1,null,0.5,1.5,0,0],["ministral-3b-latest",128000,128000,"text",0,null,0.04,0.04,0,0],["ministral-8b-latest",128000,128000,"text",0,null,0.1,0.1,0,0],["mistral-large-2411",131072,16384,"text",0,null,2,6,0,0],["mistral-large-2512",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-large-latest",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-medium-2505",131072,131072,"text,image",0,null,0.4,2,0,0],["mistral-medium-2508",262144,262144,"text,image",0,null,0.4,2,0,0],["mistral-medium-2604",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-medium-latest",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["mistral-small-2506",128000,16384,"text,image",0,null,0.1,0.3,0,0],["mistral-small-2603",256000,256000,"text,image",1,null,0.15,0.6,0,0],["mistral-small-latest",256000,256000,"text,image",1,null,0.15,0.6,0,0],["open-mistral-7b",8000,8000,"text",0,null,0.25,0.25,0,0],["open-mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["open-mixtral-8x22b",64000,64000,"text",0,null,2,6,0,0],["open-mixtral-8x7b",32000,32000,"text",0,null,0.7,0.7,0,0],["pixtral-12b",128000,128000,"text,image",0,null,0.15,0.15,0,0],["pixtral-large-latest",128000,128000,"text,image",0,null,2,6,0,0]], "moonshot": [["kimi-k2.5",262144,65536,"text,image",1,null,0,0,0,0]], diff --git a/src/providers/registry/entries-core.ts b/src/providers/registry/entries-core.ts index e18d6d3430f..aeb886ab7d3 100644 --- a/src/providers/registry/entries-core.ts +++ b/src/providers/registry/entries-core.ts @@ -1170,13 +1170,16 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [ // devlog/_plan/260710_provider_hardening/001_research_frontier.md. { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, - dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"], - modelContextWindows: { "gemini-3.8-flash": 1_048_576, "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 }, - modelInputModalities: { "gemini-3.8-flash": ["text", "image"], "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] }, + dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-4-argon", "gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"], + modelContextWindows: { "gemini-4-argon": 1_048_576, "gemini-3.8-flash": 1_048_576, "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 }, + modelInputModalities: { "gemini-4-argon": ["text", "image"], "gemini-3.8-flash": ["text", "image"], "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] }, modelReasoningEfforts: { // 3.7 and 3.8 omit `minimal`: Google documents it as a validation error on both model // pages, so advertising it hands the user a rung the API rejects. 3.5/3.6 keep theirs — // their pages still list it, and this unit has no evidence to change them. + // Gemini 4 Argon (GA 2026-10-13): low/medium(default)/high; `minimal` and thinking_budget + // return HTTP 400 per ai.google.dev/gemini-api/docs/models/gemini-4-argon. + "gemini-4-argon": ["low", "medium", "high"], "gemini-3.8-flash": ["low", "medium", "high"], "gemini-3.7-flash": ["low", "medium", "high"], "gemini-3.6-flash": ["minimal", "low", "medium", "high"], diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index ff3ad23f3d5..39c42aa7a09 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -88,6 +88,10 @@ const GEMINI_37_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, ca // rising to $1.50 / $7.50 on 2027-01-01. A SEPARATE constant on purpose: equal today, but // aliasing them would silently drag 3.8 along if 3.7's row is ever re-verified differently. const GEMINI_38_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }; +// Gemini 4 Argon introductory paid-tier rate: $2 in / $10 out (thinking included) / $0.10 cached +// per 1M through 2026-11-30, stepping up to $4 / $20 / $0.20 on 2026-12-01. Revisit this row +// then. No free tier. Implicit caching only, so there is no per-token cache-write price. +const GEMINI_4_ARGON: Cost4 = { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 0 }; const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 }; const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 }; const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 }; @@ -200,6 +204,7 @@ export function cursorFastPriceSource(provider: string, modelId: string): string const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token"; const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-08-14); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; +const GEMINI_4_ARGON_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-10-14); introductory rate through 2026-11-30, rises to 4/20 (0.20 cached) on 2026-12-01; cacheWrite=0: implicit caching only"; const GEMINI_38_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-09-03); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo"; const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing"; @@ -346,6 +351,13 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is // not published, so this is `verified-derived` rather than `verified`: the number is // proven, the claim that Antigravity charges it is inferred. + // Gemini 4 Argon on Antigravity (AI Ultra / Enterprise only). Google documents one id with + // low/medium/high thinking levels; the suffix rows cover a discovered -low/-medium/-high ladder, + // which discovery collapses onto the base id. Billing equivalence to the API price is inferred. + { provider: "google-antigravity", modelId: "gemini-4-argon", cost4: GEMINI_4_ARGON, source: `derived: Gemini 4 Argon introductory rate through 2026-11-30 ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-4-argon-low", cost4: GEMINI_4_ARGON, source: `derived: gemini-4-argon ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-4-argon-medium", cost4: GEMINI_4_ARGON, source: `derived: gemini-4-argon ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-4-argon-high", cost4: GEMINI_4_ARGON, source: `derived: gemini-4-argon ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: `derived: Gemini 3.8 Flash promotional rate through 2026-12-31 ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-3.8-flash-low", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-3.8-flash-medium", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, @@ -403,6 +415,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ // Developer API row: the price IS published for this surface, so `verified`. { provider: "google", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: GEMINI_37_PRICING, verifiedAt: "2026-08-14", status: "verified" }, { provider: "google", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: GEMINI_38_PRICING, verifiedAt: "2026-09-03", status: "verified" }, + { provider: "google", modelId: "gemini-4-argon", cost4: GEMINI_4_ARGON, source: GEMINI_4_ARGON_PRICING, verifiedAt: "2026-10-14", status: "verified" }, { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" }, // Antigravity-bundled third-party models — derived from the underlying vendor's // official API price (Antigravity itself bills via subscription quota). diff --git a/tests/adapters/google/google-hardening.test.ts b/tests/adapters/google/google-hardening.test.ts index 3e6d909a312..dab2f5a08e7 100644 --- a/tests/adapters/google/google-hardening.test.ts +++ b/tests/adapters/google/google-hardening.test.ts @@ -774,12 +774,15 @@ describe("google provider hardening", () => { const vertex = PROVIDER_REGISTRY.find(entry => entry.id === "google-vertex"); expect(google?.defaultModel).toBe("gemini-3.5-flash"); - expect(google?.models).toEqual(["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"]); + expect(google?.models).toEqual(["gemini-4-argon", "gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"]); expect(google?.modelContextWindows?.["gemini-3.8-flash"]).toBe(1_048_576); expect(google?.modelInputModalities?.["gemini-3.8-flash"]).toEqual(["text", "image"]); // `minimal` is a documented validation error on this generation, so the ladder must not // inherit the shape its 3.5/3.6/3.7 neighbours carry. expect(google?.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); + expect(google?.modelContextWindows?.["gemini-4-argon"]).toBe(1_048_576); + expect(google?.modelInputModalities?.["gemini-4-argon"]).toEqual(["text", "image"]); + expect(google?.modelReasoningEfforts?.["gemini-4-argon"]).toEqual(["low", "medium", "high"]); expect(google?.modelContextWindows?.["gemini-3.6-flash"]).toBe(1_048_576); expect(google?.modelContextWindows?.["gemini-3.5-flash"]).toBe(1_000_000); expect(google?.modelContextWindows?.["gemini-3.7-flash"]).toBe(1_048_576); diff --git a/tests/usage/usage-cost.test.ts b/tests/usage/usage-cost.test.ts index d44fa6d77f6..2ce91c458b0 100644 --- a/tests/usage/usage-cost.test.ts +++ b/tests/usage/usage-cost.test.ts @@ -507,6 +507,11 @@ describe("resolveMatchedPrice", () => { "meta-muse/muse-spark-1.3-contributor", "google-antigravity/gemini-3.8-flash-high", "google/gemini-3.8-flash", + "google/gemini-4-argon", + "google-antigravity/gemini-4-argon", + "google-antigravity/gemini-4-argon-low", + "google-antigravity/gemini-4-argon-medium", + "google-antigravity/gemini-4-argon-high", "google-antigravity/gemini-3.1-pro-low", "google-antigravity/gemini-3.1-pro-high", "google-antigravity/gemini-pro-agent", From ffc60d6f4dbeb0572a855d3ad5601d56bcf4f100 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 14 Oct 2026 03:22:55 +0900 Subject: [PATCH 2/5] feat(antigravity): serve Gemini 4 Argon with the CLI User-Agent Adds gemini-4-argon to the google-antigravity picker with low/medium/high mapped to the verified gemini-4-argon-{low,medium,high} wire ids. CCA serves those ids only to the Antigravity CLI User-Agent, so Argon inference and model discovery use antigravity/cli/1.3.4 while every other model keeps the IDE User-Agent. --- .../src/content/docs/guides/providers.md | 7 +- scripts/test-layout/layout.json | 1 + src/adapters/client-fingerprint.ts | 24 +++ src/adapters/google-antigravity-wire.ts | 23 ++- src/adapters/google.ts | 4 +- src/oauth/index.ts | 6 +- src/providers/antigravity-models.ts | 29 ++- src/usage/expected-prices.ts | 5 +- src/web-search/gemini-executor.ts | 4 +- .../google/gemini-4-argon-antigravity.test.ts | 180 ++++++++++++++++++ .../google/google-antigravity-wire.test.ts | 3 +- tests/fixtures/test-layout-expected.json | 2 +- tests/oauth/oauth-provider-reconcile.test.ts | 1 + 13 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 tests/adapters/google/gemini-4-argon-antigravity.test.ts diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index dd6e09324eb..6f5ba60a4e7 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -148,8 +148,11 @@ reached with its own key, not a key mode for the same login. Gemini 4 Argon (`gemini-4-argon`) is listed on the `google` preset with low, medium (the default), and high thinking levels. Google offers it on the paid Gemini API tier only, not the free -tier. On `google-antigravity`, Google currently limits Argon to AI Ultra and eligible Enterprise -accounts, so OpenCodex shows it there only when live model discovery returns it for your account. +tier. On `google-antigravity`, Argon is listed with the same three levels, which map to the +`gemini-4-argon-low`, `-medium`, and `-high` wire models. Google limits it there to AI Ultra and +eligible Enterprise accounts. Cloud Code Assist serves Argon only to the Antigravity CLI client, so +OpenCodex sends Argon requests and model discovery with the CLI User-Agent +(`antigravity/cli/1.3.4`) and keeps the IDE User-Agent for every other model. To check which mode a provider is actually using, open it on the Providers page: the **Connection** block's **Authentication** row reads `OAuth`, `API key`, `ChatGPT passthrough`, `Local`, or diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 24e152003e0..69269a79aa0 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -957,6 +957,7 @@ "gather-routed-models-single-flight.test.ts": "codex-integration", "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", + "gemini-4-argon-antigravity.test.ts": "adapters/google", "gemini-inline.test.ts": "images", "gemini-web-search.test.ts": "adapters/google", "generic-oauth-failover.test.ts": "oauth", diff --git a/src/adapters/client-fingerprint.ts b/src/adapters/client-fingerprint.ts index 2bf5b118621..081061be552 100644 --- a/src/adapters/client-fingerprint.ts +++ b/src/adapters/client-fingerprint.ts @@ -55,6 +55,8 @@ const ANTIGRAVITY_IDE_PLATFORM = "windows/amd64"; * Must be the IDE client family (`antigravity/ide/...`): Cloud Code Assist backend gates * newer agent models (e.g. `gemini-3.7-flash`) by User-Agent and answers 404 NOT_FOUND to * CLI-shaped UAs even with a valid OAuth token. Only `antigravity/ide/` unlocks them. + * The one exception is Gemini 4 Argon, which CCA serves ONLY to the CLI family; see + * `antigravityCliUserAgent`. * A `GOOGLE_ANTIGRAVITY_USER_AGENT` override (set by the caller) takes precedence upstream. */ export function antigravityUserAgent(version = ANTIGRAVITY_IDE_VERSION, authMethod = "oauth"): string { @@ -63,3 +65,25 @@ export function antigravityUserAgent(version = ANTIGRAVITY_IDE_VERSION, authMeth const [osType, arch] = ANTIGRAVITY_IDE_PLATFORM.split("/"); return `antigravity/ide/${version} (os_type=${osType}; arch=${arch}; ${ANTIGRAVITY_IDE_CLIENT_NAME}; auth_method=${authMethod})`; } + +// ── Antigravity CLI ── +/** Antigravity CLI version whose User-Agent CCA was verified to serve Gemini 4 Argon to. */ +export const ANTIGRAVITY_CLI_VERSION = "1.3.4"; +const ANTIGRAVITY_CLI_PLATFORM = "darwin/arm64"; + +/** + * Antigravity CLI User-Agent: + * `antigravity/cli/${version} (os_type=${osType}; arch=${arch}; aidev_client; auth_method=oauth)`. + * + * Verified 2026-10-14 against live CCA: `gemini-4-argon-{low,medium,high}` answer only to this + * family and return 404 "Requested entity was not found" to the IDE UA above, while + * `gemini-3.7-flash-tiered` and `gemini-3.1-pro-low` 404 under this one. So it is used ONLY for + * Argon requests and for model discovery, never as a general replacement for the IDE UA. + * + * `GOOGLE_ANTIGRAVITY_USER_AGENT` deliberately does not apply here: that override names the IDE + * header, and letting it replace this one would re-lock Argon for anyone who set it. + */ +export function antigravityCliUserAgent(version = ANTIGRAVITY_CLI_VERSION, authMethod = "oauth"): string { + const [osType, arch] = ANTIGRAVITY_CLI_PLATFORM.split("/"); + return `antigravity/cli/${version} (os_type=${osType}; arch=${arch}; ${ANTIGRAVITY_IDE_CLIENT_NAME}; auth_method=${authMethod})`; +} diff --git a/src/adapters/google-antigravity-wire.ts b/src/adapters/google-antigravity-wire.ts index 223d95293e4..54708887b46 100644 --- a/src/adapters/google-antigravity-wire.ts +++ b/src/adapters/google-antigravity-wire.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; import type { OcxContentPart, OcxParsedRequest } from "../types"; -import { antigravityUserAgent } from "./client-fingerprint"; +import { antigravityCliUserAgent, antigravityUserAgent } from "./client-fingerprint"; /** * Antigravity request User-Agent. Mirrors the real Antigravity IDE UA @@ -11,6 +11,27 @@ import { antigravityUserAgent } from "./client-fingerprint"; */ export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent(); +/** + * Antigravity CLI User-Agent. CCA serves Gemini 4 Argon only to the CLI client family, and it + * 404s other agent models (`gemini-3.7-flash-tiered`, `gemini-3.1-pro-low`) under it, so the + * choice is per wire model: see `antigravityUserAgentForWireModel`. + */ +export const ANTIGRAVITY_CLI_REQUEST_UA = antigravityCliUserAgent(); + +/** + * Model discovery (`:fetchAvailableModels`) uses the CLI UA so Argon's wire ids are listed; + * under the IDE UA they are absent. + */ +export const ANTIGRAVITY_DISCOVERY_UA = ANTIGRAVITY_CLI_REQUEST_UA; + +/** Wire ids CCA serves only to the CLI client family (verified 2026-10-14). */ +const ANTIGRAVITY_CLI_ONLY_WIRE_MODEL = /^gemini-4-argon(?:-(?:low|medium|high))?$/; + +/** The User-Agent an Antigravity inference request for `wireModelId` must carry. */ +export function antigravityUserAgentForWireModel(wireModelId: string): string { + return ANTIGRAVITY_CLI_ONLY_WIRE_MODEL.test(wireModelId) ? ANTIGRAVITY_CLI_REQUEST_UA : ANTIGRAVITY_REQUEST_UA; +} + /** * Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a * foreign id that must not be forwarded to Gemini/Antigravity. diff --git a/src/adapters/google.ts b/src/adapters/google.ts index df34d14a493..a60bf565d67 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -21,7 +21,7 @@ import { getVertexAccessToken } from "../lib/gcp-adc"; import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http"; import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors"; import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation"; -import { ANTIGRAVITY_REQUEST_UA, antigravitySessionAnchor, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire"; +import { antigravitySessionAnchor, antigravityUserAgentForWireModel, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire"; import { summarizeGoogleWireShape } from "./google-wire-shape"; import { compileGoogleWireBody } from "./google-wire-compiler"; import type { GoogleToolSchemaLossReport, GoogleToolSchemaProfile } from "./google-tool-schema"; @@ -1155,7 +1155,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte requestId: `agent-${crypto.randomUUID()}`, request, }; - headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA; + headers["User-Agent"] = antigravityUserAgentForWireModel(wireModelId); headers["Authorization"] = `Bearer ${token}`; return { url, method: "POST", headers, body: JSON.stringify(envelope) }; } diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 29acaf210dd..f3d0af7d047 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -49,7 +49,7 @@ import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUr import { loginCommandCode, refreshCommandCodeToken } from "./command-code"; import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse"; import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter"; -import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire"; +import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire"; import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive"; import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys"; import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry"; @@ -1161,7 +1161,9 @@ export function buildModelsRequest( if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") { headers.Accept = "application/json"; headers["Content-Type"] = "application/json"; - headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA; + // CLI UA: CCA lists Gemini 4 Argon's wire ids only to the CLI client family. Inference keeps + // choosing per wire model (antigravityUserAgentForWireModel). + headers["User-Agent"] = ANTIGRAVITY_DISCOVERY_UA; if (apiKey) headers.Authorization = `Bearer ${apiKey}`; return guarded({ method: "POST" as const, diff --git a/src/providers/antigravity-models.ts b/src/providers/antigravity-models.ts index 5ad7403aa15..6066a436b6a 100644 --- a/src/providers/antigravity-models.ts +++ b/src/providers/antigravity-models.ts @@ -17,6 +17,13 @@ import { readAntigravityWireSnapshot, writeAntigravityWireSnapshot, type Antigra /** Current Antigravity Flash generation. */ const GEMINI_FLASH_CURRENT = "gemini-3.8-flash"; +/** + * Gemini 4 Argon. Like 3.8 it publishes one wire id per tier (`-low/-medium/-high`) and no + * `-tiered` row, verified 2026-10-14 against live CCA. CCA serves these ids only to the CLI + * User-Agent (see antigravityUserAgentForWireModel), and only to AI Ultra / Enterprise accounts. + */ +const GEMINI_ARGON = "gemini-4-argon"; + /** * Previous Flash generation — still served, still picker-visible. * @@ -75,6 +82,9 @@ const ANTIGRAVITY_WIRE_MODELS = [ ]; const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record = { + "gemini-4-argon-low": GEMINI_ARGON, + "gemini-4-argon-medium": GEMINI_ARGON, + "gemini-4-argon-high": GEMINI_ARGON, "gemini-3.8-flash-low": "gemini-3.8-flash", "gemini-3.8-flash-medium": "gemini-3.8-flash", "gemini-3.8-flash-high": "gemini-3.8-flash", @@ -164,7 +174,8 @@ function collapsesIntoKnownPickerModel(candidateId: string): boolean { // Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern). export const ANTIGRAVITY_MODEL_EFFORTS: Record = { // No `minimal`: Google documents it as an error for this generation, and CCA exposes only - // the three tiers. + // the three tiers. Argon documents the same ladder (`minimal` is an HTTP 400). + [GEMINI_ARGON]: ["low", "medium", "high"], "gemini-3.8-flash": ["low", "medium", "high"], "gemini-3.7-flash": ["low", "medium", "high"], "gemini-3.1-pro": ["low", "high"], @@ -175,7 +186,12 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record = { // ── Effort → wire model map for Gemini base models ── const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { // 3.8 publishes one wire id per tier and no `-tiered` row, so its efforts ride the suffix. - // This is the 3.6 shape, not the 3.7 one. + // This is the 3.6 shape, not the 3.7 one. Argon has the same shape. + [GEMINI_ARGON]: { + low: "gemini-4-argon-low", + medium: "gemini-4-argon-medium", + high: "gemini-4-argon-high", + }, "gemini-3.8-flash": { low: "gemini-3.8-flash-low", medium: "gemini-3.8-flash-medium", @@ -198,7 +214,7 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { * `gemini-3.1-pro` is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries * no tier suffix, so there the level is the only thing naming the effort. */ -const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set(["gemini-3.8-flash"]); +const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set([GEMINI_ARGON, "gemini-3.8-flash"]); function completeDiscoveredEffortWireModelIds( pickerId: string, @@ -223,6 +239,7 @@ function completeDiscoveredEffortWireModelIds( // ── Default effort per Gemini base model ── const ANTIGRAVITY_DEFAULT_EFFORT: Record = { // Google's documented thinking_level default, and the tier CCA marks `recommended`. + [GEMINI_ARGON]: "medium", "gemini-3.8-flash": "medium", "gemini-3.1-pro": "high", }; @@ -287,6 +304,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record = { // Picker-visible: collapsed base models only. export const ANTIGRAVITY_MODELS = [ + GEMINI_ARGON, GEMINI_FLASH_CURRENT, GEMINI_FLASH_PREVIOUS, "gemini-3.1-pro", @@ -302,6 +320,9 @@ function isKnownAntigravityPickerModelId(value: string): boolean { // Context windows from the upstream `:fetchAvailableModels` maxTokens per model. const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record = { + "gemini-4-argon-low": 1_048_576, + "gemini-4-argon-medium": 1_048_576, + "gemini-4-argon-high": 1_048_576, "gemini-3.8-flash-low": 1_048_576, "gemini-3.8-flash-medium": 1_048_576, "gemini-3.8-flash-high": 1_048_576, @@ -316,6 +337,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record = { export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record = { // Collapsed base IDs — explicit entries for the picker. + [GEMINI_ARGON]: 1_048_576, "gemini-3.8-flash": 1_048_576, "gemini-3.7-flash": 1_048_576, "gemini-3.1-pro": 1_048_576, @@ -330,6 +352,7 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record = { }; export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record = { + [GEMINI_ARGON]: ["text", "image"], // Google documents 3.7 Flash as also accepting video, audio and PDF, but this proxy // carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex // catalog normalizes `input_modalities` against a closed enum. Advertising a modality diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index 39c42aa7a09..da72ecc1c9c 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -351,9 +351,8 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is // not published, so this is `verified-derived` rather than `verified`: the number is // proven, the claim that Antigravity charges it is inferred. - // Gemini 4 Argon on Antigravity (AI Ultra / Enterprise only). Google documents one id with - // low/medium/high thinking levels; the suffix rows cover a discovered -low/-medium/-high ladder, - // which discovery collapses onto the base id. Billing equivalence to the API price is inferred. + // Gemini 4 Argon on Antigravity (AI Ultra / Enterprise only): the picker id plus its three + // verified CCA wire ids. Billing equivalence to the API price is inferred. { provider: "google-antigravity", modelId: "gemini-4-argon", cost4: GEMINI_4_ARGON, source: `derived: Gemini 4 Argon introductory rate through 2026-11-30 ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-4-argon-low", cost4: GEMINI_4_ARGON, source: `derived: gemini-4-argon ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-4-argon-medium", cost4: GEMINI_4_ARGON, source: `derived: gemini-4-argon ${GEMINI_4_ARGON_PRICING}`, verifiedAt: "2026-10-14", status: "verified-derived" }, diff --git a/src/web-search/gemini-executor.ts b/src/web-search/gemini-executor.ts index 9e4d4b1af9c..f72baca1d78 100644 --- a/src/web-search/gemini-executor.ts +++ b/src/web-search/gemini-executor.ts @@ -15,7 +15,7 @@ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort"; import { readBoundedResponseBytes } from "../lib/bounded-body"; import { sidecarEnter } from "../lib/sidecar-tracker"; import { redactSecretString } from "../lib/redact"; -import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire"; +import { antigravityUserAgentForWireModel } from "../adapters/google-antigravity-wire"; import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models"; import { getProviderRegistryEntry } from "../providers/registry"; import { MAX_SIDECAR_RESPONSE_BYTES, type WebSearchSource } from "./parse"; @@ -74,7 +74,7 @@ export async function runGeminiWebSearch( headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, - "User-Agent": ANTIGRAVITY_REQUEST_UA, + "User-Agent": antigravityUserAgentForWireModel(wireModelId), }, body: JSON.stringify(envelope), signal: linkedSignal.signal, diff --git a/tests/adapters/google/gemini-4-argon-antigravity.test.ts b/tests/adapters/google/gemini-4-argon-antigravity.test.ts new file mode 100644 index 00000000000..077dd0414f8 --- /dev/null +++ b/tests/adapters/google/gemini-4-argon-antigravity.test.ts @@ -0,0 +1,180 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { createGoogleAdapter as createGoogleAdapterProduction } from "../../../src/adapters/google"; +import { antigravityCliUserAgent } from "../../../src/adapters/client-fingerprint"; +import { + ANTIGRAVITY_CLI_REQUEST_UA, + ANTIGRAVITY_DISCOVERY_UA, + ANTIGRAVITY_REQUEST_UA, + antigravityUserAgentForWireModel, +} from "../../../src/adapters/google-antigravity-wire"; +import { buildModelsRequest } from "../../../src/oauth"; +import { + ANTIGRAVITY_MODELS, + ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, + ANTIGRAVITY_MODEL_EFFORTS, + ANTIGRAVITY_MODEL_INPUT_MODALITIES, + canonicalAntigravityUsageModel, + parseAntigravityAvailableModels, + resolveAntigravityEffortWireModel, +} from "../../../src/providers/antigravity-models"; +import { PROVIDER_REGISTRY } from "../../../src/providers/registry"; +import type { OcxParsedRequest, OcxProviderConfig } from "../../../src/types"; +import { withTestTranslatorBudget } from "../../helpers/translator-budget"; + +// Verified 2026-10-14 against live CCA: Gemini 4 Argon answers only to the CLI family, and the +// IDE UA gets 404 "Requested entity was not found". The reverse also holds: under the CLI UA, +// gemini-3.7-flash-tiered and gemini-3.1-pro-low 404. Both strings are pinned exactly because +// the backend gates by family AND version. +const CLI_UA = "antigravity/cli/1.3.4 (os_type=darwin; arch=arm64; aidev_client; auth_method=oauth)"; +const IDE_UA = "antigravity/ide/2.5.5 (os_type=windows; arch=amd64; aidev_client; auth_method=oauth)"; + +const createGoogleAdapter = (...args: Parameters) => + withTestTranslatorBudget(createGoogleAdapterProduction(...args)); + +const provider = { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + googleMode: "cloud-code-assist", + project: "proj-123", + apiKey: "ya29.token", + modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, +} as OcxProviderConfig; + +function parsedWithEffort(modelId: string, effort?: string): OcxParsedRequest { + return { + modelId, + stream: false, + context: { messages: [{ role: "user", content: "test" }], systemPrompt: [], tools: [] }, + options: effort ? { reasoning: effort } : {}, + } as unknown as OcxParsedRequest; +} + +async function built(modelId: string, effort?: string) { + const req = await createGoogleAdapter(provider).buildRequest(parsedWithEffort(modelId, effort)); + return { req, env: JSON.parse(req.body) }; +} + +const originalUaOverride = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT; +afterEach(() => { + if (originalUaOverride === undefined) delete process.env.GOOGLE_ANTIGRAVITY_USER_AGENT; + else process.env.GOOGLE_ANTIGRAVITY_USER_AGENT = originalUaOverride; +}); + +describe("Gemini 4 Argon on Antigravity", () => { + test("is a collapsed picker row with the 3.8 ladder shape", () => { + expect(ANTIGRAVITY_MODELS).toContain("gemini-4-argon"); + for (const wire of ["gemini-4-argon-low", "gemini-4-argon-medium", "gemini-4-argon-high"]) { + expect(ANTIGRAVITY_MODELS).not.toContain(wire); + expect(ANTIGRAVITY_MODEL_CONTEXT_WINDOWS[wire]).toBe(1_048_576); + } + const entry = PROVIDER_REGISTRY.find(candidate => candidate.id === "google-antigravity"); + expect(entry?.models).toContain("gemini-4-argon"); + // Argon is gated to AI Ultra / Enterprise, so it must not become the default. + expect(entry?.defaultModel).toBe("gemini-3.8-flash"); + expect(ANTIGRAVITY_MODEL_EFFORTS["gemini-4-argon"]).toEqual(["low", "medium", "high"]); + expect(ANTIGRAVITY_MODEL_CONTEXT_WINDOWS["gemini-4-argon"]).toBe(1_048_576); + expect(ANTIGRAVITY_MODEL_INPUT_MODALITIES["gemini-4-argon"]).toEqual(["text", "image"]); + }); + + test("each effort resolves to its verified suffix wire id with no thinkingLevel", () => { + for (const [effort, wireModelId] of [ + ["low", "gemini-4-argon-low"], + ["medium", "gemini-4-argon-medium"], + ["high", "gemini-4-argon-high"], + ["xhigh", "gemini-4-argon-high"], + ["max", "gemini-4-argon-high"], + ] as const) { + expect(resolveAntigravityEffortWireModel("gemini-4-argon", effort)).toEqual({ wireModelId }); + } + // Google's documented default thinking level. + expect(resolveAntigravityEffortWireModel("gemini-4-argon")).toEqual({ wireModelId: "gemini-4-argon-medium" }); + }); + + test("usage collapses every Argon wire id onto the picker id", () => { + for (const wire of ["gemini-4-argon-low", "gemini-4-argon-medium", "gemini-4-argon-high"]) { + expect(canonicalAntigravityUsageModel(wire)).toBe("gemini-4-argon"); + } + }); + + test("discovery collapses the complete Argon ladder into one row", () => { + const modelIds = ["gemini-4-argon-low", "gemini-4-argon-medium", "gemini-4-argon-high"]; + const rows = parseAntigravityAvailableModels({ + models: Object.fromEntries(modelIds.map(id => [id, { maxTokens: 1_048_576 }])), + agentModelSorts: [{ groups: [{ modelIds }] }], + })!; + expect(rows.map(model => model.id)).toEqual(["gemini-4-argon"]); + expect(rows[0]?.effortWireModelIds).toEqual({ + low: "gemini-4-argon-low", + medium: "gemini-4-argon-medium", + high: "gemini-4-argon-high", + }); + }); +}); + +describe("Antigravity User-Agent selection", () => { + test("pins the CLI and IDE strings", () => { + expect(ANTIGRAVITY_CLI_REQUEST_UA).toBe(CLI_UA); + expect(ANTIGRAVITY_REQUEST_UA).toBe(IDE_UA); + }); + + test("only Argon wire ids take the CLI UA", () => { + for (const wire of ["gemini-4-argon", "gemini-4-argon-low", "gemini-4-argon-medium", "gemini-4-argon-high"]) { + expect(antigravityUserAgentForWireModel(wire)).toBe(CLI_UA); + } + for (const wire of [ + "gemini-3.7-flash-tiered", + "gemini-3.1-pro-low", + "gemini-pro-agent", + "gemini-3.8-flash-medium", + "gemini-3.1-flash-image", + "claude-sonnet-4-6", + "claude-opus-4-6-thinking", + "gpt-oss-120b-medium", + // Near misses must not widen the CLI gate. + "gemini-4-argon-max", + "gemini-4-argon-tiered", + "gemini-4-argonaut", + "x-gemini-4-argon-high", + ]) { + expect(antigravityUserAgentForWireModel(wire)).toBe(IDE_UA); + } + }); + + test("an Argon inference request carries the CLI UA and the suffix wire id", async () => { + for (const [effort, wire] of [["low", "gemini-4-argon-low"], ["high", "gemini-4-argon-high"], [undefined, "gemini-4-argon-medium"]] as const) { + const { req, env } = await built("gemini-4-argon", effort); + expect(env.model).toBe(wire); + expect(req.headers["User-Agent"]).toBe(CLI_UA); + // The suffix already names the tier; a thinkingLevel beside it would state it twice. + expect(env.request.generationConfig?.thinkingConfig?.thinkingLevel).toBeUndefined(); + } + }); + + test("every other model keeps the IDE UA, including the ids the CLI UA 404s", async () => { + const cases: Array<[string, string | undefined, string]> = [ + ["gemini-3.7-flash", "low", "gemini-3.7-flash-tiered"], + ["gemini-3.1-pro", "low", "gemini-3.1-pro-low"], + ["gemini-3.1-pro", "high", "gemini-pro-agent"], + ["gemini-3.8-flash", "medium", "gemini-3.8-flash-medium"], + ["claude-sonnet-4-6", "high", "claude-sonnet-4-6"], + ]; + for (const [modelId, effort, wire] of cases) { + const { req, env } = await built(modelId, effort); + expect(env.model).toBe(wire); + expect(req.headers["User-Agent"]).toBe(IDE_UA); + } + }); + + test("model discovery uses the CLI UA", () => { + const prov = { adapter: "google", authMode: "oauth", baseUrl: "https://daily-cloudcode-pa.googleapis.com", liveModels: true } as OcxProviderConfig; + const { url, headers } = buildModelsRequest(prov, "oauth-token", "google-antigravity"); + expect(url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"); + expect(ANTIGRAVITY_DISCOVERY_UA).toBe(CLI_UA); + expect(headers["User-Agent"]).toBe(CLI_UA); + }); + + test("the IDE UA override does not replace the CLI UA", () => { + process.env.GOOGLE_ANTIGRAVITY_USER_AGENT = "custom-ua/1.2.3"; + expect(antigravityCliUserAgent()).toBe(CLI_UA); + }); +}); diff --git a/tests/adapters/google/google-antigravity-wire.test.ts b/tests/adapters/google/google-antigravity-wire.test.ts index fdb34de1182..43f53e6c25b 100644 --- a/tests/adapters/google/google-antigravity-wire.test.ts +++ b/tests/adapters/google/google-antigravity-wire.test.ts @@ -78,8 +78,9 @@ describe("antigravity CCA envelope", () => { test("exposes Gemini 3.8 and 3.7 Flash while retired Flash ids resolve to 3.7", async () => { // Collapsed picker: base models only. 3.8 leads because CCA ranks it first in the - // Recommended sort; 3.7 stays because Google still serves it. + // Recommended sort; 3.7 stays because Google still serves it. Argon sits above both. expect(ANTIGRAVITY_MODELS).toEqual([ + "gemini-4-argon", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.1-pro", diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 2a988c6090f..729e9f15cbf 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -622,7 +622,7 @@ "flash-route-image-modalities.test.ts": "providers", "format-result.test.ts": "web-search", "forward-admission-separation.test.ts": "providers", "fresh-connection-optout.test.ts": "responses", "fulfill-video.test.ts": "videos", "gather-routed-models-single-flight.test.ts": "codex-integration", - "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", + "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", "gemini-4-argon-antigravity.test.ts": "adapters/google", "gemini-inline.test.ts": "images", "gemini-web-search.test.ts": "adapters/google", "generic-oauth-failover.test.ts": "oauth", "generic-oauth-refusal-composition.test.ts": "oauth", "github-copilot-auto.test.ts": "providers/github-copilot", diff --git a/tests/oauth/oauth-provider-reconcile.test.ts b/tests/oauth/oauth-provider-reconcile.test.ts index 8e8a1b67afd..7b05e378e34 100644 --- a/tests/oauth/oauth-provider-reconcile.test.ts +++ b/tests/oauth/oauth-provider-reconcile.test.ts @@ -263,6 +263,7 @@ describe("OAuth provider reconciliation", () => { const provider = config.providers["google-antigravity"]; expect(provider.defaultModel).toBe("gemini-3.5-flash-low"); expect(provider.models).toEqual([ + "gemini-4-argon", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.1-pro", From cc0e74b9b8f3f3c8acd62d4450ec6e3fdcf96149 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 14 Oct 2026 03:41:16 +0900 Subject: [PATCH 3/5] feat(antigravity): trim the static lineup to 3.8 Flash, 4 Argon, Opus 5.5 and Sonnet 5.5 The static google-antigravity picker now lists gemini-3.8-flash, gemini-4-argon, claude-opus-5-5 and claude-sonnet-5-5. Claude 5.5 gets the same low/medium/high suffix-tier mapping as Argon and 3.8 Flash. Dropped bases (3.7 Flash, 3.1 Pro, 3.1 Flash Image, Claude 4.6, gpt-oss) stay known to routing, discovery collapse and usage identity, so saved selections route unchanged and discovered rows still appear. --- .../src/content/docs/guides/providers.md | 6 ++ src/providers/antigravity-models.ts | 67 +++++++++++++++++-- .../google/gemini-37-flash-migration.test.ts | 14 ++-- .../google/gemini-4-argon-antigravity.test.ts | 41 ++++++++++++ .../google/google-antigravity-wire.test.ts | 16 ++--- tests/oauth/oauth-provider-reconcile.test.ts | 15 ++--- .../provider-registry-parity.test.ts | 11 +-- 7 files changed, 132 insertions(+), 38 deletions(-) diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 6f5ba60a4e7..5c0b3d268cd 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -154,6 +154,12 @@ eligible Enterprise accounts. Cloud Code Assist serves Argon only to the Antigra OpenCodex sends Argon requests and model discovery with the CLI User-Agent (`antigravity/cli/1.3.4`) and keeps the IDE User-Agent for every other model. +The built-in `google-antigravity` model list is Gemini 3.8 Flash (`gemini-3.8-flash`), Gemini 4 Argon +(`gemini-4-argon`), Claude Opus 5.5 (`claude-opus-5-5`), and Claude Sonnet 5.5 +(`claude-sonnet-5-5`), each with low, medium, and high efforts. Other models appear only when live +model discovery returns them for your account. Saved selections of models that left the list keep +routing as before. + To check which mode a provider is actually using, open it on the Providers page: the **Connection** block's **Authentication** row reads `OAuth`, `API key`, `ChatGPT passthrough`, `Local`, or `No key needed`. It is a provider-level setting, so the account rows below it do not repeat it. diff --git a/src/providers/antigravity-models.ts b/src/providers/antigravity-models.ts index 6066a436b6a..901f07560ec 100644 --- a/src/providers/antigravity-models.ts +++ b/src/providers/antigravity-models.ts @@ -24,6 +24,14 @@ const GEMINI_FLASH_CURRENT = "gemini-3.8-flash"; */ const GEMINI_ARGON = "gemini-4-argon"; +/** + * Claude 5.5 on Antigravity: one wire id per tier (`-low/-medium/-high`), each 1,000,000 tokens + * with image input, observed in live CCA discovery on 2026-10-03 + * (devlog/_fin/261003_release_lane_c). They ride the IDE User-Agent like every non-Argon model. + */ +const CLAUDE_OPUS_55 = "claude-opus-5-5"; +const CLAUDE_SONNET_55 = "claude-sonnet-5-5"; + /** * Previous Flash generation — still served, still picker-visible. * @@ -82,6 +90,12 @@ const ANTIGRAVITY_WIRE_MODELS = [ ]; const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record = { + "claude-opus-5-5-low": CLAUDE_OPUS_55, + "claude-opus-5-5-medium": CLAUDE_OPUS_55, + "claude-opus-5-5-high": CLAUDE_OPUS_55, + "claude-sonnet-5-5-low": CLAUDE_SONNET_55, + "claude-sonnet-5-5-medium": CLAUDE_SONNET_55, + "claude-sonnet-5-5-high": CLAUDE_SONNET_55, "gemini-4-argon-low": GEMINI_ARGON, "gemini-4-argon-medium": GEMINI_ARGON, "gemini-4-argon-high": GEMINI_ARGON, @@ -177,6 +191,8 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record = { // the three tiers. Argon documents the same ladder (`minimal` is an HTTP 400). [GEMINI_ARGON]: ["low", "medium", "high"], "gemini-3.8-flash": ["low", "medium", "high"], + [CLAUDE_OPUS_55]: ["low", "medium", "high"], + [CLAUDE_SONNET_55]: ["low", "medium", "high"], "gemini-3.7-flash": ["low", "medium", "high"], "gemini-3.1-pro": ["low", "high"], "claude-sonnet-4-6": ["low", "medium", "high", "max"], @@ -192,6 +208,17 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { medium: "gemini-4-argon-medium", high: "gemini-4-argon-high", }, + // Claude 5.5 publishes the same per-tier suffix family. + [CLAUDE_OPUS_55]: { + low: "claude-opus-5-5-low", + medium: "claude-opus-5-5-medium", + high: "claude-opus-5-5-high", + }, + [CLAUDE_SONNET_55]: { + low: "claude-sonnet-5-5-low", + medium: "claude-sonnet-5-5-medium", + high: "claude-sonnet-5-5-high", + }, "gemini-3.8-flash": { low: "gemini-3.8-flash-low", medium: "gemini-3.8-flash-medium", @@ -214,7 +241,7 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { * `gemini-3.1-pro` is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries * no tier suffix, so there the level is the only thing naming the effort. */ -const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set([GEMINI_ARGON, "gemini-3.8-flash"]); +const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set([GEMINI_ARGON, "gemini-3.8-flash", CLAUDE_OPUS_55, CLAUDE_SONNET_55]); function completeDiscoveredEffortWireModelIds( pickerId: string, @@ -241,6 +268,9 @@ const ANTIGRAVITY_DEFAULT_EFFORT: Record = { // Google's documented thinking_level default, and the tier CCA marks `recommended`. [GEMINI_ARGON]: "medium", "gemini-3.8-flash": "medium", + // Matches the discovered-family default (medium), so static and live resolution agree. + [CLAUDE_OPUS_55]: "medium", + [CLAUDE_SONNET_55]: "medium", "gemini-3.1-pro": "high", }; @@ -302,10 +332,25 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record = { ...ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES, }; -// Picker-visible: collapsed base models only. +// Static picker lineup: collapsed base models only, exactly "3.8 / 4 / 5.5 / 5.5" (2026-10-14). +// Anything else reaches the picker only when live discovery returns it. export const ANTIGRAVITY_MODELS = [ - GEMINI_ARGON, GEMINI_FLASH_CURRENT, + GEMINI_ARGON, + CLAUDE_OPUS_55, + CLAUDE_SONNET_55, +]; + +/** + * Every base id the routing and discovery rules recognise: the static lineup plus models that + * left it. Removing a model from the picker must not change how a saved selection routes or how + * a discovered row collapses, so these legacy bases stay known here. Live discovery on + * 2026-10-14 no longer returns gemini-3.7-flash(-tiered) or the gemini-3.1-pro wire ids (both + * 404), but still returns the Claude 4.6 rows and gpt-oss-120b-medium, which therefore keep + * appearing through discovery. + */ +export const ANTIGRAVITY_KNOWN_PICKER_MODELS: readonly string[] = [ + ...ANTIGRAVITY_MODELS, GEMINI_FLASH_PREVIOUS, "gemini-3.1-pro", "gemini-3.1-flash-image", @@ -315,11 +360,17 @@ export const ANTIGRAVITY_MODELS = [ ]; function isKnownAntigravityPickerModelId(value: string): boolean { - return isValidModelDiscoveryModelId(value) && ANTIGRAVITY_MODELS.includes(value); + return isValidModelDiscoveryModelId(value) && ANTIGRAVITY_KNOWN_PICKER_MODELS.includes(value); } // Context windows from the upstream `:fetchAvailableModels` maxTokens per model. const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record = { + "claude-opus-5-5-low": 1_000_000, + "claude-opus-5-5-medium": 1_000_000, + "claude-opus-5-5-high": 1_000_000, + "claude-sonnet-5-5-low": 1_000_000, + "claude-sonnet-5-5-medium": 1_000_000, + "claude-sonnet-5-5-high": 1_000_000, "gemini-4-argon-low": 1_048_576, "gemini-4-argon-medium": 1_048_576, "gemini-4-argon-high": 1_048_576, @@ -339,6 +390,8 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record = { // Collapsed base IDs — explicit entries for the picker. [GEMINI_ARGON]: 1_048_576, "gemini-3.8-flash": 1_048_576, + [CLAUDE_OPUS_55]: 1_000_000, + [CLAUDE_SONNET_55]: 1_000_000, "gemini-3.7-flash": 1_048_576, "gemini-3.1-pro": 1_048_576, // Wire IDs and aliases via derivation. @@ -353,6 +406,8 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record = { export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record = { [GEMINI_ARGON]: ["text", "image"], + [CLAUDE_OPUS_55]: ["text", "image"], + [CLAUDE_SONNET_55]: ["text", "image"], // Google documents 3.7 Flash as also accepting video, audio and PDF, but this proxy // carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex // catalog normalizes `input_modalities` against a closed enum. Advertising a modality @@ -664,7 +719,7 @@ export function resolveAntigravityWireModelId(modelId: string, baseUrl?: string) * IS the effort, and sending both creates a contradictory request. */ export function isAntigravitySuffixModelId(modelId: string): boolean { - return !(ANTIGRAVITY_MODELS as string[]).includes(modelId); + return !ANTIGRAVITY_KNOWN_PICKER_MODELS.includes(modelId); } /** The reasoning tier a retired Flash id used to encode, if it is one. */ @@ -776,7 +831,7 @@ export function resolveAntigravityEffortWireModel( // summary aggregation consults this reverse map so one call model = one usage row. const ANTIGRAVITY_USAGE_BASE_BY_ID: Record = (() => { const rev: Record = {}; - for (const base of ANTIGRAVITY_MODELS) rev[base] = base; + for (const base of ANTIGRAVITY_KNOWN_PICKER_MODELS) rev[base] = base; for (const [base, effortMap] of Object.entries(ANTIGRAVITY_EFFORT_WIRE_MAP)) { rev[base] = base; for (const wire of Object.values(effortMap)) rev[wire] = base; diff --git a/tests/adapters/google/gemini-37-flash-migration.test.ts b/tests/adapters/google/gemini-37-flash-migration.test.ts index 0ea67ac68a3..85d676dd80b 100644 --- a/tests/adapters/google/gemini-37-flash-migration.test.ts +++ b/tests/adapters/google/gemini-37-flash-migration.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + ANTIGRAVITY_KNOWN_PICKER_MODELS, ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, @@ -36,7 +37,7 @@ const RETIRED_TIERS: Record = { describe("Gemini 3.7 Flash replaces 3.6 on Antigravity", () => { test("no retired Flash id is picker-visible", () => { - expect(ANTIGRAVITY_MODELS).toContain("gemini-3.7-flash"); + expect(ANTIGRAVITY_KNOWN_PICKER_MODELS).toContain("gemini-3.7-flash"); for (const retired of Object.keys(RETIRED_TIERS)) { expect(ANTIGRAVITY_MODELS).not.toContain(retired); } @@ -47,7 +48,10 @@ describe("Gemini 3.7 Flash replaces 3.6 on Antigravity", () => { // unlike the 3.6 generation it must NOT join the retired map when its successor ships. // A regression here would strand every saved 3.7 selection on a redirect it never needed. expect(RETIRED_TIERS).not.toHaveProperty("gemini-3.7-flash"); - expect(ANTIGRAVITY_MODELS).toContain("gemini-3.7-flash"); + // Off the static lineup since 2026-10-14, but still a known base, so saved selections route + // exactly as before. + expect(ANTIGRAVITY_MODELS).not.toContain("gemini-3.7-flash"); + expect(ANTIGRAVITY_KNOWN_PICKER_MODELS).toContain("gemini-3.7-flash"); expect(ANTIGRAVITY_MODEL_EFFORTS["gemini-3.7-flash"]).toEqual(["low", "medium", "high"]); }); @@ -55,7 +59,7 @@ describe("Gemini 3.7 Flash replaces 3.6 on Antigravity", () => { const entry = PROVIDER_REGISTRY.find(row => row.id === "google-antigravity"); expect(entry?.defaultModel).toBe("gemini-3.8-flash"); expect(entry?.models).toContain("gemini-3.8-flash"); - expect(entry?.models).toContain("gemini-3.7-flash"); + expect(entry?.models).not.toContain("gemini-3.7-flash"); }); test("capability maps describe 3.7 rather than inheriting a fallback", () => { @@ -318,8 +322,8 @@ describe("the -tiered wire rename reaches the request path", () => { test("the rename is confined to the wire, leaving the user-facing id intact", () => { // The picker, the usage rows and the price overlays are all keyed on the base id. // If the rename leaked into them, saved selections and historical spend would move. - expect(ANTIGRAVITY_MODELS).toContain("gemini-3.7-flash"); - expect(ANTIGRAVITY_MODELS).not.toContain("gemini-3.7-flash-tiered"); + expect(ANTIGRAVITY_KNOWN_PICKER_MODELS).toContain("gemini-3.7-flash"); + expect(ANTIGRAVITY_KNOWN_PICKER_MODELS).not.toContain("gemini-3.7-flash-tiered"); expect(canonicalAntigravityUsageModel("gemini-3.7-flash")).toBe("gemini-3.7-flash"); expect(resolveMatchedPrice("google-antigravity", "gemini-3.7-flash")?.cost4).toBeDefined(); }); diff --git a/tests/adapters/google/gemini-4-argon-antigravity.test.ts b/tests/adapters/google/gemini-4-argon-antigravity.test.ts index 077dd0414f8..5e784b412c4 100644 --- a/tests/adapters/google/gemini-4-argon-antigravity.test.ts +++ b/tests/adapters/google/gemini-4-argon-antigravity.test.ts @@ -9,11 +9,13 @@ import { } from "../../../src/adapters/google-antigravity-wire"; import { buildModelsRequest } from "../../../src/oauth"; import { + ANTIGRAVITY_KNOWN_PICKER_MODELS, ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES, canonicalAntigravityUsageModel, + isAntigravitySuffixModelId, parseAntigravityAvailableModels, resolveAntigravityEffortWireModel, } from "../../../src/providers/antigravity-models"; @@ -178,3 +180,42 @@ describe("Antigravity User-Agent selection", () => { expect(antigravityCliUserAgent()).toBe(CLI_UA); }); }); + +describe("Antigravity static lineup (3.8 / 4 / 5.5 / 5.5)", () => { + test("is exactly the four confirmed base models", () => { + expect(ANTIGRAVITY_MODELS).toEqual(["gemini-3.8-flash", "gemini-4-argon", "claude-opus-5-5", "claude-sonnet-5-5"]); + const entry = PROVIDER_REGISTRY.find(candidate => candidate.id === "google-antigravity"); + expect(entry?.models).toEqual(ANTIGRAVITY_MODELS); + }); + + test("Claude 5.5 efforts ride their suffix wire ids on the IDE UA", async () => { + for (const base of ["claude-opus-5-5", "claude-sonnet-5-5"]) { + expect(ANTIGRAVITY_MODEL_EFFORTS[base]).toEqual(["low", "medium", "high"]); + expect(ANTIGRAVITY_MODEL_CONTEXT_WINDOWS[base]).toBe(1_000_000); + expect(ANTIGRAVITY_MODEL_INPUT_MODALITIES[base]).toEqual(["text", "image"]); + for (const effort of ["low", "medium", "high"] as const) { + expect(resolveAntigravityEffortWireModel(base, effort)).toEqual({ wireModelId: `${base}-${effort}` }); + expect(canonicalAntigravityUsageModel(`${base}-${effort}`)).toBe(base); + } + expect(resolveAntigravityEffortWireModel(base)).toEqual({ wireModelId: `${base}-medium` }); + const { req, env } = await built(base, "high"); + expect(env.model).toBe(`${base}-high`); + expect(req.headers["User-Agent"]).toBe(IDE_UA); + } + }); + + test("models dropped from the lineup still route exactly as before", () => { + for (const legacy of ["gemini-3.7-flash", "gemini-3.1-pro", "claude-sonnet-4-6", "claude-opus-4-6-thinking", "gpt-oss-120b-medium"]) { + expect(ANTIGRAVITY_MODELS).not.toContain(legacy); + expect(ANTIGRAVITY_KNOWN_PICKER_MODELS).toContain(legacy); + // A known base is not mistaken for a tier-encoding suffix id. + expect(isAntigravitySuffixModelId(legacy)).toBe(false); + expect(canonicalAntigravityUsageModel(legacy)).toBe(legacy); + } + expect(resolveAntigravityEffortWireModel("gemini-3.7-flash", "low")) + .toEqual({ wireModelId: "gemini-3.7-flash-tiered", thinkingLevel: "low" }); + expect(resolveAntigravityEffortWireModel("gemini-3.1-pro", "high")).toEqual({ wireModelId: "gemini-pro-agent", thinkingLevel: "high" }); + expect(resolveAntigravityEffortWireModel("claude-sonnet-4-6", "high")) + .toEqual({ wireModelId: "claude-sonnet-4-6", thinkingLevel: "high" }); + }); +}); diff --git a/tests/adapters/google/google-antigravity-wire.test.ts b/tests/adapters/google/google-antigravity-wire.test.ts index 43f53e6c25b..c54cf2eef33 100644 --- a/tests/adapters/google/google-antigravity-wire.test.ts +++ b/tests/adapters/google/google-antigravity-wire.test.ts @@ -76,18 +76,14 @@ describe("antigravity CCA envelope", () => { expect(req.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"); }); - test("exposes Gemini 3.8 and 3.7 Flash while retired Flash ids resolve to 3.7", async () => { - // Collapsed picker: base models only. 3.8 leads because CCA ranks it first in the - // Recommended sort; 3.7 stays because Google still serves it. Argon sits above both. + test("exposes the 3.8 / 4 / 5.5 / 5.5 lineup while retired Flash ids still resolve to 3.7", async () => { + // Collapsed picker: base models only, the static lineup confirmed 2026-10-14. Older models + // reach the picker only through live discovery. expect(ANTIGRAVITY_MODELS).toEqual([ - "gemini-4-argon", "gemini-3.8-flash", - "gemini-3.7-flash", - "gemini-3.1-pro", - "gemini-3.1-flash-image", - "claude-sonnet-4-6", - "claude-opus-4-6-thinking", - "gpt-oss-120b-medium", + "gemini-4-argon", + "claude-opus-5-5", + "claude-sonnet-5-5", ]); for (const hidden of [ "gemini-3.6-flash", diff --git a/tests/oauth/oauth-provider-reconcile.test.ts b/tests/oauth/oauth-provider-reconcile.test.ts index 7b05e378e34..5e03d16c6c5 100644 --- a/tests/oauth/oauth-provider-reconcile.test.ts +++ b/tests/oauth/oauth-provider-reconcile.test.ts @@ -263,14 +263,10 @@ describe("OAuth provider reconciliation", () => { const provider = config.providers["google-antigravity"]; expect(provider.defaultModel).toBe("gemini-3.5-flash-low"); expect(provider.models).toEqual([ - "gemini-4-argon", "gemini-3.8-flash", - "gemini-3.7-flash", - "gemini-3.1-pro", - "gemini-3.1-flash-image", - "claude-sonnet-4-6", - "claude-opus-4-6-thinking", - "gpt-oss-120b-medium", + "gemini-4-argon", + "claude-opus-5-5", + "claude-sonnet-5-5", ]); expect(provider.models).not.toContain("gemini-3.5-flash-low"); expect(provider.models).not.toContain("gemini-3.6-flash"); @@ -328,7 +324,7 @@ describe("OAuth provider reconciliation", () => { upsertOAuthProvider(config, "google-antigravity"); expect(config.providers["google-antigravity"].liveModels).toBe(true); - expect(config.providers["google-antigravity"].models).toHaveLength(7); + expect(config.providers["google-antigravity"].models).toHaveLength(4); }); test("an explicit 3.7 default survives the 3.8 launch while its capabilities refresh", () => { @@ -360,7 +356,8 @@ describe("OAuth provider reconciliation", () => { const provider = config.providers["google-antigravity"]; expect(provider.defaultModel).toBe("gemini-3.7-flash"); - expect(provider.models).toContain("gemini-3.7-flash"); + // The static lineup no longer lists 3.7, but a live-discovery default stays operator-owned. + expect(provider.models).not.toContain("gemini-3.7-flash"); expect(provider.models).toContain("gemini-3.8-flash"); // Capability records still refresh from the registry — preservation is about the // user's CHOICE, not about freezing the row. diff --git a/tests/providers/provider-registry-parity.test.ts b/tests/providers/provider-registry-parity.test.ts index 09707ea063a..844f782116b 100644 --- a/tests/providers/provider-registry-parity.test.ts +++ b/tests/providers/provider-registry-parity.test.ts @@ -1337,14 +1337,9 @@ describe("provider registry parity", () => { expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.liveModels).toBe(true); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.defaultModel).toBe("gemini-3.8-flash"); // Collapsed picker: base models only, no effort-suffix variants. - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.7-flash"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.1-pro"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("claude-sonnet-4-6"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("claude-opus-4-6-thinking"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gpt-oss-120b-medium"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.1-flash-image"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.8-flash"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toHaveLength(7); + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toEqual([ + "gemini-3.8-flash", "gemini-4-argon", "claude-opus-5-5", "claude-sonnet-5-5", + ]); // Effort ladders on collapsed base models. expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["gemini-3.7-flash"]).toEqual(["low", "medium", "high"]); From 24cfaa8102669b85922cad7615dcb4a687b676c4 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 14 Oct 2026 04:00:47 +0900 Subject: [PATCH 4/5] fix(antigravity): keep migrations and discovery tests consistent with the trimmed lineup Retired 3.5/3.6 Flash selections now migrate to the live gemini-3.8-flash base (registrySeedsTarget refused 3.7 once it left the lineup). Discovery-only family tests use a hypothetical claude-opus-6-1 because Claude 5.5 now has a static ladder. The static catalog count is 4. --- src/providers/model-rename-migration.ts | 8 +++++--- .../antigravity-discovered-families.test.ts | 4 +++- .../google/gemini-37-flash-migration.test.ts | 18 ++++++++--------- tests/codex-integration/codex-catalog.test.ts | 2 +- .../providers/model-rename-migration.test.ts | 4 ++-- ...provider-antigravity-wire-snapshot.test.ts | 20 +++++++++---------- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/providers/model-rename-migration.ts b/src/providers/model-rename-migration.ts index 78672e6cb4a..e4bc3f6e949 100644 --- a/src/providers/model-rename-migration.ts +++ b/src/providers/model-rename-migration.ts @@ -124,16 +124,18 @@ export const MODEL_RENAMES: readonly ModelRename[] = [ // Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a // dead selection rather than a merely outdated one. Routing already redirects these ids // at request time; this migration repairs the saved config so the picker, the allowlist - // and the capability maps stop naming a model the backend no longer serves. + // and the capability maps stop naming a model the backend no longer serves. The target is + // the live 3.8 base: 3.7 left the static lineup on 2026-10-14 (CCA 404s its wire id), and + // registrySeedsTarget refuses to write an id the registry no longer seeds. ...(["gemini-3.6-flash", "gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-flash-high", "gemini-3.5-flash-extra-low", "gemini-3.5-flash-low", "gemini-3.5-flash-mid", "gemini-3.5-flash-high", "gemini-3-flash-agent"] as const).map(from => ({ provider: "google-antigravity", from, - to: "gemini-3.7-flash", + to: "gemini-3.8-flash", reason: "Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships, so the saved id no longer resolves to a live model", // The retired Flash tiers were wire ids, so any saved per-model record keyed by one - // may also hold one as a value. 3.7 expresses tiers as thinkingLevel names instead. + // may also hold one as a value. 3.8 resolves tiers from its own suffix ladder instead. dropReasoningEffortMap: true, })), ]; diff --git a/tests/adapters/google/antigravity-discovered-families.test.ts b/tests/adapters/google/antigravity-discovered-families.test.ts index 0d10efe26ef..e4b02c719b9 100644 --- a/tests/adapters/google/antigravity-discovered-families.test.ts +++ b/tests/adapters/google/antigravity-discovered-families.test.ts @@ -62,7 +62,9 @@ test("family metadata uses the lower common capability instead of first-tier met }); test("discovery mapping remains URL scoped and retires with cache generation", () => { - const base = "claude-opus-5-5"; + // A family with no static ladder: Claude 5.5 now has one, so it no longer exercises the + // discovery-only fallback to the bare id. + const base = "claude-opus-6-1"; registerAntigravityDiscoveredWireModels(baseUrl, parseAntigravityAvailableModels(payload(base))!, { provider: "family-test", cacheGeneration: captureModelCacheGeneration("family-test"), }); diff --git a/tests/adapters/google/gemini-37-flash-migration.test.ts b/tests/adapters/google/gemini-37-flash-migration.test.ts index 85d676dd80b..3fe082e56d2 100644 --- a/tests/adapters/google/gemini-37-flash-migration.test.ts +++ b/tests/adapters/google/gemini-37-flash-migration.test.ts @@ -165,13 +165,13 @@ describe("a saved config survives the retirement", () => { const { config, changed } = projectModelRenames(configWith(["gemini-3.6-flash"])); expect(changed).toBe(true); const prov = config.providers["google-antigravity"]!; - expect(prov.selectedModels).toEqual(["gemini-3.7-flash"]); - expect(prov.defaultModel).toBe("gemini-3.7-flash"); + expect(prov.selectedModels).toEqual(["gemini-3.8-flash"]); + expect(prov.defaultModel).toBe("gemini-3.8-flash"); }); - test("migrating a tier id does not duplicate an already-present 3.7 entry", () => { - const { config } = projectModelRenames(configWith(["gemini-3.6-flash-high", "gemini-3.7-flash"])); - expect(config.providers["google-antigravity"]!.selectedModels).toEqual(["gemini-3.7-flash"]); + test("migrating a tier id does not duplicate an already-present 3.8 entry", () => { + const { config } = projectModelRenames(configWith(["gemini-3.6-flash-high", "gemini-3.8-flash"])); + expect(config.providers["google-antigravity"]!.selectedModels).toEqual(["gemini-3.8-flash"]); }); test("an unrelated selection is left alone", () => { @@ -208,12 +208,12 @@ describe("a saved config survives the retirement", () => { const prov = migrated.providers["google-antigravity"]!; const map = prov.modelReasoningEffortMap ?? {}; expect(map["gemini-3.6-flash"]).toBeUndefined(); - expect(map["gemini-3.7-flash"]).toBeUndefined(); + expect(map["gemini-3.8-flash"]).toBeUndefined(); // The descriptive records move to the new id rather than vanishing. - expect(prov.modelReasoningEfforts?.["gemini-3.7-flash"]).toEqual(["low", "medium", "high"]); + expect(prov.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); expect(prov.modelReasoningEfforts?.["gemini-3.6-flash"]).toBeUndefined(); - expect(prov.modelContextWindows?.["gemini-3.7-flash"]).toBe(1_048_576); - expect(prov.modelInputModalities?.["gemini-3.7-flash"]).toEqual(["text", "image"]); + expect(prov.modelContextWindows?.["gemini-3.8-flash"]).toBe(1_048_576); + expect(prov.modelInputModalities?.["gemini-3.8-flash"]).toEqual(["text", "image"]); }); test("a request under the retired effort map still reaches 3.7 at the requested tier", () => { diff --git a/tests/codex-integration/codex-catalog.test.ts b/tests/codex-integration/codex-catalog.test.ts index 4207585f0aa..cda61de6e73 100644 --- a/tests/codex-integration/codex-catalog.test.ts +++ b/tests/codex-integration/codex-catalog.test.ts @@ -4667,7 +4667,7 @@ describe("Codex catalog routed normalization", () => { expect(fetchCalls).toBe(0); expect(ids).toEqual([...(provider.models ?? [])].sort()); - expect(ids).toHaveLength(7); + expect(ids).toHaveLength(4); expect(getProviderDiscoveryStatus(providerName)).toBeUndefined(); markProviderDiscoveryFailed(providerName, { reason: "http", httpStatus: 404 }); diff --git a/tests/providers/model-rename-migration.test.ts b/tests/providers/model-rename-migration.test.ts index f235921d28a..9e8206d36bf 100644 --- a/tests/providers/model-rename-migration.test.ts +++ b/tests/providers/model-rename-migration.test.ts @@ -593,7 +593,7 @@ describe("registry-seeded metadata does not re-arm the rename (#5066)", () => { // the user is still owed. expect(Object.keys(afterFirstBoot.modelContextWindows ?? {})).toContain("gemini-3.6-flash"); // What the migration did own stayed fixed. - expect(afterFirstBoot.selectedModels).toEqual(["gemini-3.7-flash"]); + expect(afterFirstBoot.selectedModels).toEqual(["gemini-3.8-flash"]); const second = renameLines(() => { runModelRenameStartupMigration(loadConfig()); }); expect(second).toEqual([]); @@ -611,7 +611,7 @@ describe("registry-seeded metadata does not re-arm the rename (#5066)", () => { expect(changed).toBe(true); expect(warnings).toHaveLength(1); - expect(prov.selectedModels).toEqual(["gemini-3.7-flash"]); + expect(prov.selectedModels).toEqual(["gemini-3.8-flash"]); // Left alone on purpose: a request that still names the retired id routes to 3.7 and needs // a window under the id it asked for, and reconciliation would rewrite this record anyway. expect(prov.modelContextWindows?.["gemini-3.6-flash"]).toBe(1_048_576); diff --git a/tests/providers/provider-antigravity-wire-snapshot.test.ts b/tests/providers/provider-antigravity-wire-snapshot.test.ts index 2821ae4ffbf..a3c90d0ecf1 100644 --- a/tests/providers/provider-antigravity-wire-snapshot.test.ts +++ b/tests/providers/provider-antigravity-wire-snapshot.test.ts @@ -14,7 +14,7 @@ import { captureModelCacheGeneration, clearModelCache, getStaleCached } from './ import { gatherRoutedModels } from './src/codex/catalog'; const provider = 'google-antigravity'; const baseUrl = 'https://snapshot.example.test/TenantA'; -const base = 'claude-opus-5-5'; +const base = 'claude-opus-6-1'; const ids = ['low','medium','high'].map(tier => base+'-'+tier); const payload = {models: Object.fromEntries(ids.map(id => [id, {maxTokens:350000}])), agentModelSorts:[{groups:[{modelIds:ids}]}]}; const config = {port:0, defaultProvider:provider, providers:{[provider]:{adapter:'google',authMode:'key',apiKey:'fixture-token',baseUrl,googleMode:'cloud-code-assist',project:'fixture-project',liveModels:true,models:['safe-previous'],fetch:(...args)=>globalThis.fetch(...args)}}}; @@ -28,7 +28,7 @@ function run(code: string) { } function discover() { expect(run(`globalThis.fetch=async()=>Response.json(payload); console.log(JSON.stringify((await gatherRoutedModels(config)).map(m=>m.id)));`)) - .toEqual(["claude-opus-5-5"]); + .toEqual(["claude-opus-6-1"]); } function snapshotFile() { return join(home, readdirSync(home).find(file => file.startsWith("antigravity-wire-"))!); } @@ -57,8 +57,8 @@ test("discovery survives restart and failed discovery with actual adapter wire e console.log(JSON.stringify(wires)); `); expect(result).toEqual([ - {model:"claude-opus-5-5-low"},{model:"claude-opus-5-5-medium"},{model:"claude-opus-5-5-high"},{model:"claude-opus-5-5-medium"}, - {wireModelId:"claude-opus-5-5"},{wireModelId:"claude-opus-5-5"}, + {model:"claude-opus-6-1-low"},{model:"claude-opus-6-1-medium"},{model:"claude-opus-6-1-high"},{model:"claude-opus-6-1-medium"}, + {wireModelId:"claude-opus-6-1"},{wireModelId:"claude-opus-6-1"}, ]); }); @@ -67,26 +67,26 @@ test("accepted partial and empty discoveries replace old durable families", () = discover(); run(`registerAntigravityDiscoveredWireModels(baseUrl,${models},{provider,cacheGeneration:captureModelCacheGeneration(provider)});console.log('true');`); expect(run(`console.log(JSON.stringify(resolveAntigravityEffortWireModel(base,'high',baseUrl)));`)) - .toEqual({wireModelId:"claude-opus-5-5"}); + .toEqual({wireModelId:"claude-opus-6-1"}); } }); test("stale discovery never overwrites durable evidence and path case remains isolated", () => { discover(); expect(run(`const generation=captureModelCacheGeneration(provider);clearModelCache(provider);registerAntigravityDiscoveredWireModels(baseUrl,[],{provider,cacheGeneration:generation});console.log(JSON.stringify(resolveAntigravityEffortWireModel(base,'high',baseUrl)));`)) - .toEqual({wireModelId:"claude-opus-5-5-high"}); + .toEqual({wireModelId:"claude-opus-6-1-high"}); expect(run(`console.log(JSON.stringify(resolveAntigravityEffortWireModel(base,'high',baseUrl.toLowerCase())));`)) - .toEqual({wireModelId:"claude-opus-5-5"}); + .toEqual({wireModelId:"claude-opus-6-1"}); }); test("malformed and oversized snapshots fail closed", () => { discover(); const path = snapshotFile(); for (const content of ["{", JSON.stringify({version:2,provider:"google-antigravity",families:{}}), - JSON.stringify({version:1,provider:"google-antigravity",families:{"claude-opus-5-5":{low:"different",medium:"claude-opus-5-5-medium",high:"claude-opus-5-5-high"}}}), " ".repeat(4*1024*1024+1)]) { + JSON.stringify({version:1,provider:"google-antigravity",families:{"claude-opus-6-1":{low:"different",medium:"claude-opus-6-1-medium",high:"claude-opus-6-1-high"}}}), " ".repeat(4*1024*1024+1)]) { writeFileSync(path,content); expect(run(`console.log(JSON.stringify(resolveAntigravityEffortWireModel(base,'high',baseUrl)));`)) - .toEqual({wireModelId:"claude-opus-5-5"}); + .toEqual({wireModelId:"claude-opus-6-1"}); } }); @@ -107,6 +107,6 @@ test("generationless registrations stay memory-only and homes do not share mappi const other=resolveAntigravityEffortWireModel(base,'high',baseUrl); process.env.OPENCODEX_HOME=originalHome; console.log(JSON.stringify([original,other,resolveAntigravityEffortWireModel(base,'low',baseUrl)])); - `)).toEqual([{wireModelId:"claude-opus-5-5-high"},{wireModelId:"claude-opus-5-5"},{wireModelId:"claude-opus-5-5-low"}]); + `)).toEqual([{wireModelId:"claude-opus-6-1-high"},{wireModelId:"claude-opus-6-1"},{wireModelId:"claude-opus-6-1-low"}]); expect(readdirSync(home).filter(file=>file.startsWith("antigravity-wire-"))).toEqual([]); }); From 93ecf639a5e2802c5332461a6db58c6ca58208b7 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 14 Oct 2026 04:15:12 +0900 Subject: [PATCH 5/5] feat(antigravity): filter and route models by each account roster Antigravity gates models per account (Claude 4.6 on some accounts, Claude 5.5 suffix families on others, Gemini 4 Argon on Ultra/Enterprise). Discovery now records each account roster; account selection prefers an account whose roster lists the requested model, skips a known non-listing account, and treats a missing roster as unknown. The catalog keeps exposing only the active account discovery, and the static-lineup drop is no longer warned as drift. --- .../src/content/docs/guides/providers.md | 11 +- scripts/test-layout/layout.json | 1 + src/codex/catalog/model-hints.ts | 4 +- src/codex/catalog/provider-models.ts | 4 + src/oauth/generic-account-failover.ts | 34 +++- src/providers/antigravity-account-roster.ts | 102 ++++++++++ structure/providers-and-adapters.md | 2 +- .../google/antigravity-account-roster.test.ts | 178 ++++++++++++++++++ tests/fixtures/test-layout-expected.json | 2 +- 9 files changed, 327 insertions(+), 11 deletions(-) create mode 100644 src/providers/antigravity-account-roster.ts create mode 100644 tests/adapters/google/antigravity-account-roster.test.ts diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 5c0b3d268cd..49d06e71a2c 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -156,9 +156,14 @@ OpenCodex sends Argon requests and model discovery with the CLI User-Agent The built-in `google-antigravity` model list is Gemini 3.8 Flash (`gemini-3.8-flash`), Gemini 4 Argon (`gemini-4-argon`), Claude Opus 5.5 (`claude-opus-5-5`), and Claude Sonnet 5.5 -(`claude-sonnet-5-5`), each with low, medium, and high efforts. Other models appear only when live -model discovery returns them for your account. Saved selections of models that left the list keep -routing as before. +(`claude-sonnet-5-5`), each with low, medium, and high efforts. What you actually see is your +account's own model discovery, because Google enables models per account: some accounts list +Claude Sonnet 4.6 and Claude Opus 4.6 (Thinking), others only Claude Opus and Sonnet 5.5 (directly +billed, non-trial Pro or Ultra plans), and Gemini 4 Argon only on Ultra and Enterprise. Both Claude +generations map: 4.6 takes its effort as a thinking level, 5.5 as its `-low`, `-medium`, or +`-high` model. With several Antigravity accounts and `oauthAccountFailover.enabled`, a request +prefers an account whose discovery lists the requested model. Saved selections of models that left +the list keep routing as before. To check which mode a provider is actually using, open it on the Providers page: the **Connection** block's **Authentication** row reads `OAuth`, `API key`, `ChatGPT passthrough`, `Local`, or diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 69269a79aa0..1f1cd043a0f 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -958,6 +958,7 @@ "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", "gemini-4-argon-antigravity.test.ts": "adapters/google", + "antigravity-account-roster.test.ts": "adapters/google", "gemini-inline.test.ts": "images", "gemini-web-search.test.ts": "adapters/google", "generic-oauth-failover.test.ts": "oauth", diff --git a/src/codex/catalog/model-hints.ts b/src/codex/catalog/model-hints.ts index aa2b9d7fe3b..0eaa7c6c088 100644 --- a/src/codex/catalog/model-hints.ts +++ b/src/codex/catalog/model-hints.ts @@ -431,7 +431,9 @@ export function suppressedSyntheticMaxCatalogSlugs( } return slugs; } -export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]); +// google-antigravity: the static lineup is an entitlement superset (Claude 5.5 and Gemini 4 Argon +// are per-account), so a live roster that omits some of it is the expected case, not drift. +export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai", "google-antigravity"]); export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly>> = { // CodeBuddy's vendor defaults are real callable selectors — both the bundled manifests diff --git a/src/codex/catalog/provider-models.ts b/src/codex/catalog/provider-models.ts index 7fe2c27c694..c2340cc6b62 100644 --- a/src/codex/catalog/provider-models.ts +++ b/src/codex/catalog/provider-models.ts @@ -53,6 +53,7 @@ import { import type { FastPolicyAuthority } from "../../providers/fastwire"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry"; import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models"; +import { recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster"; import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"; import { effectiveModelAliases } from "../../providers/default-aliases"; @@ -807,6 +808,9 @@ export async function fetchProviderModelsWithAuth( provider: name, cacheGeneration, }); + // Entitlement is per account (Claude 4.6 vs 5.5, Argon): remember what THIS account + // lists so account selection can prefer one that serves the requested model. + if (auth.oauthAccountId) recordAntigravityAccountRoster(name, auth.oauthAccountId, antigravity); if (!setCached(name, forCache, Date.now(), cacheGeneration)) { return observed(withConfiguredRetention(configured), "degraded"); } diff --git a/src/oauth/generic-account-failover.ts b/src/oauth/generic-account-failover.ts index 41410bb9edc..e1a1884037b 100644 --- a/src/oauth/generic-account-failover.ts +++ b/src/oauth/generic-account-failover.ts @@ -41,6 +41,7 @@ import type { KiroRefusalKind } from "../adapters/kiro-refusal"; import { kiroAccountEvidence } from "../providers/kiro-usage"; import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk"; import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog"; +import { antigravityAccountSupportsModel } from "../providers/antigravity-account-roster"; import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire"; import { sweepExpiredOnWrite } from "../lib/state-store-sweeper"; import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events"; @@ -248,11 +249,31 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now(), return eligibleIdsIn(getAccountSet(providerName), providerName, now, family); } -/** Model evidence only ranks accounts already eligible and below any configured cap. */ +/** Whether an account's own model listing includes `model`; undefined when unknown. */ +function accountListsModel(providerName: string, accountId: string, model: string): boolean | undefined { + if (providerName === "kiro") return kiroAccountSupportsModel(accountId, model); + if (providerName === "google-antigravity") return antigravityAccountSupportsModel(providerName, accountId, model); + return undefined; +} + +/** + * Model evidence only ranks accounts already eligible and below any configured cap. + * + * Kiro and Antigravity both gate models per account. For Antigravity (Claude 4.6 vs 5.5, Argon) + * an account whose discovered roster lists the model wins; an account whose roster omits it is + * skipped while any other remains; an account with no roster yet is unknown and stays usable. + */ export function preferKiroModelSupport( providerName: string, ids: string[], model?: string | null, config?: OcxConfig, ): string[] { - if (providerName !== "kiro" || !model) return ids; + if (!model) return ids; + if (providerName === "google-antigravity") { + const positive = ids.filter(id => accountListsModel(providerName, id, model) === true); + if (positive.length) return positive; + const notListedAgainst = ids.filter(id => accountListsModel(providerName, id, model) !== false); + return notListedAgainst.length ? notListedAgainst : ids; + } + if (providerName !== "kiro") return ids; const cap = config?.providers?.kiro?.oauthAccountFailover?.maxConcurrentPerAccount; const withRoom = typeof cap === "number" && Number.isInteger(cap) && cap >= 1 && cap <= 100 ? ids.filter(id => accountInFlight("kiro", id) < cap) : ids; @@ -631,10 +652,13 @@ export function preferredInitialAccount( const modelEligible = preferKiroModelSupport(providerName, eligibleFailoverAccounts(providerName, now, classifyModelFamilyForQuota(providerName, requestedModelId)), requestedModelId, config); - const activeLacks = providerName === "kiro" && !!active && !!requestedModelId - && kiroAccountSupportsModel(active, requestedModelId) === false; + const activeLacks = (providerName === "kiro" || providerName === "google-antigravity") + && !!active && !!requestedModelId + && accountListsModel(providerName, active, requestedModelId) === false; const listingSibling = activeLacks - ? modelEligible.find(id => id !== active && kiroModelListingHasRoom(config, id, requestedModelId!)) + ? modelEligible.find(id => id !== active && (providerName === "kiro" + ? kiroModelListingHasRoom(config, id, requestedModelId!) + : accountListsModel(providerName, id, requestedModelId!) === true)) : undefined; if (listingSibling) return listingSibling; diff --git a/src/providers/antigravity-account-roster.ts b/src/providers/antigravity-account-roster.ts new file mode 100644 index 00000000000..051798f3aa9 --- /dev/null +++ b/src/providers/antigravity-account-roster.ts @@ -0,0 +1,102 @@ +import { getConfigDir } from "../config/paths"; +import { ANTIGRAVITY_MODEL_ALIASES, canonicalAntigravityUsageModel, type AntigravityAvailableModel } from "./antigravity-models"; + +/** + * Per-account Antigravity model rosters, taken from each account's own live + * `:fetchAvailableModels` discovery. + * + * Google gates models per account, not per client. Measured 2026-10-14: one account lists + * `claude-sonnet-4-6` / `claude-opus-4-6-thinking` and answers 404 "Requested entity was not + * found" for every `claude-{opus,sonnet}-5-5-*` id, while other accounts list only the 5.5 + * families. Gemini 4 Argon is likewise Ultra / Enterprise only. Google documents Claude 5.5 as + * needing a directly billed, non-trial Pro or Ultra plan and rolls it out gradually, so the static + * lineup is a superset and only discovery says what one account can actually call. + * + * The catalog already exposes only the ACTIVE account's discovered rows (an authoritative live + * roster drops static rows it did not return). This store keeps every account's last roster so + * that account selection can prefer an account that lists the requested model, instead of + * landing a 5.5 request on a 4.6-only account. Absence of a roster is "unknown", never "no": + * an account that was never active during discovery stays fully usable. + */ + +/** A roster older than this is treated as unknown; entitlements change with plan and rollout. */ +export const ANTIGRAVITY_ACCOUNT_ROSTER_TTL_MS = 6 * 60 * 60 * 1000; + +interface AccountRoster { + readonly ids: ReadonlySet; + readonly modelIds: readonly string[]; + readonly observedAt: number; +} + +const rosters = new Map(); + +function rosterKey(providerName: string, accountId: string): string { + return `${getConfigDir()}\0${providerName}\0${accountId}`; +} + +/** Record one account's discovered roster: picker ids, representative wire ids, and tier wire ids. */ +export function recordAntigravityAccountRoster( + providerName: string, + accountId: string, + models: readonly AntigravityAvailableModel[], + now = Date.now(), +): void { + if (!providerName || !accountId) return; + const ids = new Set(); + for (const model of models) { + ids.add(model.id); + ids.add(model.wireModelId); + for (const wire of Object.values(model.effortWireModelIds ?? {})) if (wire) ids.add(wire); + } + rosters.set(rosterKey(providerName, accountId), { + ids, + modelIds: models.map(model => model.id), + observedAt: now, + }); +} + +function freshRoster(providerName: string, accountId: string, now: number): AccountRoster | undefined { + const roster = rosters.get(rosterKey(providerName, accountId)); + if (!roster || now - roster.observedAt > ANTIGRAVITY_ACCOUNT_ROSTER_TTL_MS) return undefined; + return roster; +} + +/** The picker ids one account's last discovery returned, or `undefined` when unknown. */ +export function antigravityAccountRosterModelIds( + providerName: string, + accountId: string, + now = Date.now(), +): readonly string[] | undefined { + return freshRoster(providerName, accountId, now)?.modelIds; +} + +/** + * Whether one account's roster lists `modelId`: `true` / `false` from its last discovery, + * `undefined` when that account has no fresh roster. A picker id, one of its tier wire ids, or a + * compatibility alias of either all count as listed. + */ +export function antigravityAccountSupportsModel( + providerName: string, + accountId: string, + modelId: string, + now = Date.now(), +): boolean | undefined { + const roster = freshRoster(providerName, accountId, now); + if (!roster) return undefined; + const id = modelId.trim(); + if (!id) return undefined; + if (roster.ids.has(id)) return true; + const canonical = canonicalAntigravityUsageModel(id); + if (roster.ids.has(canonical)) return true; + const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, id) ? ANTIGRAVITY_MODEL_ALIASES[id] : undefined; + return alias !== undefined && roster.ids.has(alias); +} + +/** Forget rosters (one account, or all). For account removal and tests. */ +export function clearAntigravityAccountRosters(providerName?: string, accountId?: string): void { + if (providerName && accountId) { + rosters.delete(rosterKey(providerName, accountId)); + return; + } + rosters.clear(); +} diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index c7c9665547a..c522806f375 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -585,7 +585,7 @@ Antigravity discovery in `src/providers/antigravity-models.ts` collapses complet Antigravity catalog rows carry the observed complete effort map from `src/providers/antigravity-models.ts` through the in-memory cache. `src/providers/antigravity-effort-families.ts` uses that evidence to group public rows and project suffix allowlists into the base selection, including both final catalog merge callers. Internal retained suffix rows remain available to combo resolution. Authoritative new-model reconciliation normalizes both the prior baseline and policy input before detecting arrivals: an existing suffix family is not a new model. All-disabled tiers transfer a base disable once; a known base keeps the operator's later choice. Genuinely new families still follow new-model policy. Saved defaults, combo targets, suffix disables and provider selections remain unchanged. A discovered ladder advertises only its wire-backed efforts unless explicitly overridden; unknown single-wire models gain no invented ladder. -Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged. +Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged. Antigravity model entitlement is per account: one account may list Claude 4.6, another only the Claude 5.5 suffix families, and Gemini 4 Argon only on Ultra/Enterprise. `ANTIGRAVITY_MODELS` is therefore a static superset (Gemini 3.8 Flash, Gemini 4 Argon, Claude Opus 5.5, Claude Sonnet 5.5), and `ANTIGRAVITY_KNOWN_PICKER_MODELS` keeps dropped bases known to routing, discovery collapse and usage identity. The catalog exposes only what the active account's authoritative discovery returns. `src/providers/antigravity-account-roster.ts` records each account's discovered roster (in memory, six-hour freshness, scoped to config home); `src/oauth/generic-account-failover.ts` ranks Antigravity accounts by it the way it ranks Kiro listings: a listing account wins, a known non-listing account is skipped while another remains, and an account without a fresh roster is unknown and stays usable. Gemini 4 Argon requests and Antigravity model discovery send the Antigravity CLI User-Agent; every other model keeps the IDE User-Agent. ## Combo model refusal evidence diff --git a/tests/adapters/google/antigravity-account-roster.test.ts b/tests/adapters/google/antigravity-account-roster.test.ts new file mode 100644 index 00000000000..65a3574ad23 --- /dev/null +++ b/tests/adapters/google/antigravity-account-roster.test.ts @@ -0,0 +1,178 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { gatherRoutedModels as gatherRoutedModelsDirect } from "../../../src/codex/catalog"; +import { clearModelCache } from "../../../src/codex/model-cache"; +import { preferKiroModelSupport, preferredInitialAccount } from "../../../src/oauth/generic-account-failover"; +import { + ANTIGRAVITY_ACCOUNT_ROSTER_TTL_MS, + antigravityAccountRosterModelIds, + antigravityAccountSupportsModel, + clearAntigravityAccountRosters, + recordAntigravityAccountRoster, +} from "../../../src/providers/antigravity-account-roster"; +import { + ANTIGRAVITY_MODELS, + ANTIGRAVITY_MODEL_EFFORTS, + parseAntigravityAvailableModels, + resolveAntigravityEffortWireModel, +} from "../../../src/providers/antigravity-models"; +import type { OcxConfig, OcxProviderConfig } from "../../../src/types"; +import { withStubbedProviderFetch } from "../../helpers/catalog-provider-fetch"; +import { removeTreeWithRetry } from "../../helpers/remove-tree"; + +// Measured 2026-10-14 on live CCA: Antigravity gates Claude per account. One account lists only +// claude-sonnet-4-6 / claude-opus-4-6-thinking (every claude-*-5-5-* id answers 404), others list +// only the 5.5 suffix families. These payloads mirror those two roster shapes. +const PROVIDER = "google-antigravity"; +const row = (maxTokens: number) => ({ maxTokens, supportsImages: true, supportsThinking: true }); +const ROSTER_46 = { + models: { + "gemini-3.8-flash-low": row(1_048_576), "gemini-3.8-flash-medium": row(1_048_576), "gemini-3.8-flash-high": row(1_048_576), + "claude-sonnet-4-6": row(250_000), "claude-opus-4-6-thinking": row(250_000), + }, + agentModelSorts: [{ groups: [{ modelIds: [ + "gemini-3.8-flash-high", "gemini-3.8-flash-medium", "gemini-3.8-flash-low", "claude-sonnet-4-6", "claude-opus-4-6-thinking", + ] }] }], +}; +const tiers = (base: string) => ["low", "medium", "high"].map(tier => `${base}-${tier}`); +const ROSTER_55 = { + models: Object.fromEntries([ + ...tiers("gemini-3.8-flash").map(id => [id, row(1_048_576)]), + ...tiers("gemini-4-argon").map(id => [id, row(1_048_576)]), + ...tiers("claude-opus-5-5").map(id => [id, row(1_000_000)]), + ...tiers("claude-sonnet-5-5").map(id => [id, row(1_000_000)]), + ]), + agentModelSorts: [{ groups: [{ modelIds: [ + ...tiers("gemini-3.8-flash"), ...tiers("gemini-4-argon"), ...tiers("claude-opus-5-5"), ...tiers("claude-sonnet-5-5"), + ] }] }], +}; + +const gatherRoutedModels: typeof gatherRoutedModelsDirect = (config, options) => + gatherRoutedModelsDirect(withStubbedProviderFetch(config), options); + +const originalFetch = globalThis.fetch; +const originalHome = process.env.OPENCODEX_HOME; +const homes: string[] = []; +afterEach(() => { + globalThis.fetch = originalFetch; + clearModelCache(); + clearAntigravityAccountRosters(); + if (originalHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = originalHome; + for (const home of homes.splice(0)) removeTreeWithRetry(home); +}); + +function homeWithAccounts(accountIds: string[], activeAccountId: string): string { + const home = mkdtempSync(join(tmpdir(), "ocx-agy-roster-")); + homes.push(home); + process.env.OPENCODEX_HOME = home; + writeFileSync(join(home, "auth.json"), JSON.stringify({ + [PROVIDER]: { + activeAccountId, + accounts: accountIds.map(id => ({ + id, + credential: { access: `access-${id}`, refresh: `refresh-${id}`, expires: Date.now() + 3_600_000, projectId: "project-id" }, + })), + }, + })); + return home; +} + +function provider(extra: Partial = {}): OcxProviderConfig { + return { + adapter: "google", + authMode: "oauth", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + googleMode: "cloud-code-assist", + project: "configured-project", + liveModels: true, + // The static lineup, as the registry seeds it. + models: [...ANTIGRAVITY_MODELS], + modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, + ...extra, + } as OcxProviderConfig; +} + +async function discoveredIds(payload: unknown, accountId: string): Promise { + homeWithAccounts([accountId], accountId); + globalThis.fetch = (async () => Response.json(payload)) as typeof fetch; + const models = await gatherRoutedModels({ providers: { [PROVIDER]: provider() } } as unknown as OcxConfig); + return models.filter(model => model.provider === PROVIDER).map(model => model.id).sort(); +} + +describe("Antigravity exposure follows each account's own discovery", () => { + test("a Claude 4.6 account exposes 4.6 and hides the static 5.5 and Argon rows", async () => { + const ids = await discoveredIds(ROSTER_46, "acct-46"); + expect(ids).toEqual(["claude-opus-4-6-thinking", "claude-sonnet-4-6", "gemini-3.8-flash"]); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-46", "claude-sonnet-4-6")).toBe(true); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-46", "claude-opus-5-5")).toBe(false); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-46", "gemini-4-argon")).toBe(false); + }); + + test("a Claude 5.5 account exposes the full 3.8 / 4 / 5.5 / 5.5 lineup and no 4.6 row", async () => { + const ids = await discoveredIds(ROSTER_55, "acct-55"); + expect(ids).toEqual(["claude-opus-5-5", "claude-sonnet-5-5", "gemini-3.8-flash", "gemini-4-argon"]); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-55", "claude-opus-5-5")).toBe(true); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-55", "claude-opus-5-5-high")).toBe(true); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-55", "claude-sonnet-4-6")).toBe(false); + expect(antigravityAccountRosterModelIds(PROVIDER, "acct-55")).toEqual(expect.arrayContaining(["claude-opus-5-5", "gemini-4-argon"])); + }); +}); + +describe("Claude 4.6 and 5.5 both map", () => { + test("4.6 rides thinkingLevel on its single wire id", () => { + expect(ANTIGRAVITY_MODEL_EFFORTS["claude-sonnet-4-6"]).toEqual(["low", "medium", "high", "max"]); + expect(resolveAntigravityEffortWireModel("claude-sonnet-4-6", "high")) + .toEqual({ wireModelId: "claude-sonnet-4-6", thinkingLevel: "high" }); + // CCA's ThinkingLevel enum tops out at high. + expect(resolveAntigravityEffortWireModel("claude-opus-4-6-thinking", "max")) + .toEqual({ wireModelId: "claude-opus-4-6-thinking", thinkingLevel: "high" }); + }); + + test("5.5 rides its per-tier suffix wire ids, statically and from discovery", () => { + for (const base of ["claude-opus-5-5", "claude-sonnet-5-5"]) { + for (const effort of ["low", "medium", "high"] as const) { + expect(resolveAntigravityEffortWireModel(base, effort)).toEqual({ wireModelId: `${base}-${effort}` }); + } + } + const rows = parseAntigravityAvailableModels(ROSTER_55)!; + expect(rows.find(model => model.id === "claude-opus-5-5")?.effortWireModelIds).toEqual({ + low: "claude-opus-5-5-low", medium: "claude-opus-5-5-medium", high: "claude-opus-5-5-high", + }); + }); +}); + +describe("account selection prefers an account whose roster lists the model", () => { + function recordBoth(): void { + recordAntigravityAccountRoster(PROVIDER, "acct-46", parseAntigravityAvailableModels(ROSTER_46)!); + recordAntigravityAccountRoster(PROVIDER, "acct-55", parseAntigravityAvailableModels(ROSTER_55)!); + } + + test("ranking keeps listing accounts, skips known non-listing ones, and keeps unknown ones", () => { + homeWithAccounts(["acct-46", "acct-55", "acct-new"], "acct-46"); + recordBoth(); + expect(preferKiroModelSupport(PROVIDER, ["acct-46", "acct-55"], "claude-opus-5-5")).toEqual(["acct-55"]); + expect(preferKiroModelSupport(PROVIDER, ["acct-46", "acct-55"], "claude-sonnet-4-6")).toEqual(["acct-46"]); + // No roster yet means unknown, never "unsupported". + expect(preferKiroModelSupport(PROVIDER, ["acct-46", "acct-new"], "claude-opus-5-5")).toEqual(["acct-new"]); + // Nobody lists it: fall back to every eligible account rather than refusing. + expect(preferKiroModelSupport(PROVIDER, ["acct-46"], "claude-opus-5-5")).toEqual(["acct-46"]); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-new", "claude-opus-5-5")).toBeUndefined(); + }); + + test("a stale roster is unknown", () => { + homeWithAccounts(["acct-46"], "acct-46"); + recordAntigravityAccountRoster(PROVIDER, "acct-46", parseAntigravityAvailableModels(ROSTER_46)!, + Date.now() - ANTIGRAVITY_ACCOUNT_ROSTER_TTL_MS - 1); + expect(antigravityAccountSupportsModel(PROVIDER, "acct-46", "claude-opus-5-5")).toBeUndefined(); + }); + + test("with proactive failover on, a 5.5 request leaves a 4.6-only active account", () => { + homeWithAccounts(["acct-46", "acct-55"], "acct-46"); + recordBoth(); + const config = { providers: { [PROVIDER]: provider({ oauthAccountFailover: { enabled: true } }) } } as unknown as OcxConfig; + expect(preferredInitialAccount(config, PROVIDER, Date.now(), "claude-opus-5-5")).toBe("acct-55"); + }); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 729e9f15cbf..2d9362435a5 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -622,7 +622,7 @@ "flash-route-image-modalities.test.ts": "providers", "format-result.test.ts": "web-search", "forward-admission-separation.test.ts": "providers", "fresh-connection-optout.test.ts": "responses", "fulfill-video.test.ts": "videos", "gather-routed-models-single-flight.test.ts": "codex-integration", - "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", "gemini-4-argon-antigravity.test.ts": "adapters/google", + "gcp-adc.test.ts": "adapters/google", "gemini-37-flash-migration.test.ts": "adapters/google", "gemini-4-argon-antigravity.test.ts": "adapters/google", "antigravity-account-roster.test.ts": "adapters/google", "gemini-inline.test.ts": "images", "gemini-web-search.test.ts": "adapters/google", "generic-oauth-failover.test.ts": "oauth", "generic-oauth-refusal-composition.test.ts": "oauth", "github-copilot-auto.test.ts": "providers/github-copilot",