Skip to content

Commit ddf59d0

Browse files
committed
fix(providers): keep Antigravity catalog static
1 parent 45e9dc4 commit ddf59d0

13 files changed

Lines changed: 109 additions & 21 deletions

File tree

docs-site/src/content/docs/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ ocx logout <provider>
8686
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude models; live model list fetched from `/v1/models`. |
8787
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 coding models. |
8888
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Initial login imports the installed, signed-in `kiro-cli` session (install with `curl -fsSL https://cli.kiro.dev/install | bash`, then run `kiro-cli login`). **Add account** logs `kiro-cli` out, starts a fresh browser login that switches the account used by `kiro-cli`, and stores account-scoped profile metadata. Existing OpenCodex accounts are preserved, and cancellation or failure restores the previous `kiro-cli` session. |
89-
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. |
89+
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. Uses the maintained six-model static catalog because CCA does not expose the generic `/models` endpoint. |
9090
| `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport, and account-filtered model discovery. |
9191
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. |
9292

docs-site/src/content/docs/ja/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ ocx logout <provider>
8181
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude モデル; ライブモデル一覧は `/v1/models` から取得。 |
8282
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 コーディングモデル。 |
8383
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 初回ログインは Kiro CLI をインストール(`curl -fsSL https://cli.kiro.dev/install | bash`)し、`kiro-cli login` でサインインした既存セッションを取り込みます。**アカウントを追加**は `kiro-cli` をログアウトして新しいブラウザログインを開始し、`kiro-cli` 自体のアカウントを切り替えてアカウント別プロファイルメタデータを保存します。既存の OpenCodex アカウントは保持され、キャンセルまたは失敗時には以前の `kiro-cli` セッションが復元されます。 |
84-
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。 |
84+
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。CCA は汎用 `/models` エンドポイントを公開しないため、管理された 6 モデルの静的カタログを使用します。 |
8585
| `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 |
8686

8787
正規の Kimi Coding Plan プリセット(`kimi` アカウントログインと `kimi-code` API key)では、

docs-site/src/content/docs/ko/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ ocx logout <provider>
8181
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 모델; 실시간 모델 목록은 `/v1/models`에서 가져옵니다. |
8282
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 코딩 모델. |
8383
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 최초 로그인은 Kiro CLI를 설치(`curl -fsSL https://cli.kiro.dev/install | bash`)하고 `kiro-cli login`으로 로그인한 기존 세션을 가져옵니다. **계정 추가**는 `kiro-cli`에서 로그아웃한 뒤 새 브라우저 로그인을 시작하여 `kiro-cli` 자체의 계정을 전환하고, 계정별 프로필 메타데이터를 저장합니다. 기존 OpenCodex 계정은 유지되며, 취소되거나 실패하면 이전 `kiro-cli` 세션을 복원합니다. |
84-
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. |
84+
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. CCA가 범용 `/models` 엔드포인트를 제공하지 않으므로 유지 관리되는 6개 모델 정적 카탈로그를 사용합니다. |
8585
| `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. |
8686

8787
정식 Kimi Coding Plan 프리셋(`kimi` 계정 로그인과 `kimi-code` API key)의 경우, opencodex는

docs-site/src/content/docs/ru/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ ocx logout <provider>
8989
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Модели Claude; актуальный список моделей загружается из `/v1/models`. |
9090
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Модели Kimi K2.7/K2.6/K2.5 для кодинга. |
9191
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Первый вход импортирует существующую сессию после установки Kiro CLI (`curl -fsSL https://cli.kiro.dev/install | bash`) и входа через `kiro-cli login`. **Добавить аккаунт** выполняет выход из `kiro-cli`, запускает новый вход через браузер, переключает аккаунт самого `kiro-cli` и сохраняет метаданные профиля отдельно для каждого аккаунта. Существующие аккаунты OpenCodex сохраняются; при отмене или сбое восстанавливается предыдущая сессия `kiro-cli`. |
92-
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. |
92+
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. Используется поддерживаемый статический каталог из шести моделей, поскольку CCA не предоставляет общий эндпоинт `/models`. |
9393
| `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. |
9494
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. |
9595

docs-site/src/content/docs/zh-cn/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ ocx logout <provider>
7575
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude 模型;实时模型列表从 `/v1/models` 获取。 |
7676
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 编程模型。 |
7777
| `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 首次登录会导入已安装并已登录的 Kiro CLI 会话(使用 `curl -fsSL https://cli.kiro.dev/install | bash` 安装,然后运行 `kiro-cli login`)。**添加账户**会先退出 `kiro-cli`,再启动新的浏览器登录,从而切换 `kiro-cli` 自身使用的账户,并保存账户范围的配置文件元数据。现有 OpenCodex 账户会保留;如果取消或失败,则恢复之前的 `kiro-cli` 会话。 |
78-
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。 |
78+
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。由于 CCA 不提供通用 `/models` 端点,因此使用维护中的六模型静态目录。 |
7979
| `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、HTTP/2 传输和按账号筛选的模型发现。 |
8080

8181
对于规范的 Kimi Coding Plan 预设(`kimi` 账号登录和 `kimi-code` API key),opencodex

src/codex/catalog/provider-fetch.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,6 @@ function boundedOwnedBy(value: unknown): string | undefined {
308308

309309
export async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
310310
if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
311-
const apiKey = await resolveModelsAuthToken(name, prov);
312311
const seedVertexDefault = prov.adapter === "google"
313312
&& prov.googleMode === "vertex"
314313
&& (prov.models?.length ?? 0) === 0
@@ -319,6 +318,13 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
319318
provider: name,
320319
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
321320
}));
321+
// Static catalogs never need an OAuth refresh or an upstream model request. Clear any
322+
// discovery failure left by an older live configuration even when the account is logged out.
323+
if (prov.liveModels === false) {
324+
clearProviderDiscoveryStatus(name);
325+
return configured;
326+
}
327+
const apiKey = await resolveModelsAuthToken(name, prov);
322328
// A configured default is a real callable selector and must remain discoverable when a
323329
// compatible provider's live /models request fails (issue #308). Keep this separate from the
324330
// explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero
@@ -337,10 +343,6 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
337343
: models
338344
);
339345
if (prov.adapter === "cursor") {
340-
if (prov.liveModels === false) {
341-
clearProviderDiscoveryStatus(name);
342-
return configured;
343-
}
344346
if (!apiKey) return configured;
345347
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
346348
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
@@ -375,10 +377,6 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
375377
// matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
376378
return configured;
377379
}
378-
if (prov.liveModels === false) {
379-
clearProviderDiscoveryStatus(name);
380-
return configured;
381-
}
382380
const fresh = getFreshCached(name, ttlMs);
383381
if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
384382
if (isModelsFetchCoolingDown(name)) {

src/oauth/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,9 @@ export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | und
541541
* configs on the next `ocx start`, instead of only fresh installs. The live `/models` fetch stays
542542
* the primary source; this keeps the static fallback (and models-not-in-/models) current.
543543
*
544-
* Only touches providers that are registry-managed AND still `authMode: "oauth"`, and only the
545-
* preset fields (never apiKey/baseUrl/user toggles). Persists + returns true when anything changed.
544+
* Only touches providers that are registry-managed AND still `authMode: "oauth"`. Preset fields
545+
* are refreshed, while the registry's `liveModels` default is filled only when the user has no
546+
* explicit toggle. Persists + returns true when anything changed.
546547
*/
547548
function cloneProviderField(value: unknown): unknown {
548549
if (Array.isArray(value)) return [...value];
@@ -585,6 +586,10 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
585586
}
586587
changed = true;
587588
}
589+
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
590+
prov.liveModels = preset.liveModels;
591+
changed = true;
592+
}
588593
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
589594
if (prov.defaultModel && preset.defaultModel && !(prov.models ?? []).includes(prov.defaultModel)) {
590595
prov.defaultModel = preset.defaultModel;
@@ -651,6 +656,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
651656
if (namespaceCollision) throw new Error(namespaceCollision);
652657
const existing = config.providers[provider];
653658
const next: OcxProviderConfig = { ...def.providerConfig };
659+
// `liveModels` is a user-facing provider toggle. A registry default seeds new rows, but an
660+
// explicit choice must survive re-login and the post-credential latest-config upsert.
661+
if (typeof existing?.liveModels === "boolean") next.liveModels = existing.liveModels;
654662
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
655663
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
656664
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);

src/providers/registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
796796
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
797797
// evidence from ai.google.dev does not establish Vertex publisher availability.
798798
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
799-
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
799+
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: false, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
800800
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
801801
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
802802
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },

tests/codex-catalog.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { OcxConfig } from "../src/types";
2525
import type { NormalizedComboConfig } from "../src/combos/types";
2626
import { enrichProviderFromRegistry } from "../src/providers/derive";
2727
import { handleManagementAPI } from "../src/server/management-api";
28+
import { OAUTH_PROVIDERS } from "../src/oauth";
2829

2930
const originalFetch = globalThis.fetch;
3031

@@ -1269,6 +1270,42 @@ describe("Codex catalog routed normalization", () => {
12691270
}
12701271
});
12711272

1273+
test("Google Antigravity uses its static registry catalog and suppresses stale discovery (#723)", async () => {
1274+
const providerName = "google-antigravity";
1275+
const provider = structuredClone(OAUTH_PROVIDERS[providerName].providerConfig);
1276+
const config = {
1277+
port: 10100,
1278+
defaultProvider: providerName,
1279+
providers: { [providerName]: provider },
1280+
} as OcxConfig;
1281+
let fetchCalls = 0;
1282+
globalThis.fetch = (() => {
1283+
fetchCalls += 1;
1284+
throw new Error("static Antigravity catalog must not call /models");
1285+
}) as typeof fetch;
1286+
markProviderDiscoveryFailed(providerName, { reason: "http", httpStatus: 404 });
1287+
1288+
const models = await gatherRoutedModels(config);
1289+
const ids = models.filter(model => model.provider === providerName).map(model => model.id).sort();
1290+
1291+
expect(fetchCalls).toBe(0);
1292+
expect(ids).toEqual([...(provider.models ?? [])].sort());
1293+
expect(ids).toHaveLength(6);
1294+
expect(getProviderDiscoveryStatus(providerName)).toBeUndefined();
1295+
1296+
markProviderDiscoveryFailed(providerName, { reason: "http", httpStatus: 404 });
1297+
const url = new URL("http://127.0.0.1/api/providers");
1298+
const response = await handleManagementAPI(new Request(url), url, config);
1299+
const rows = await response!.json() as Array<Record<string, unknown>>;
1300+
const row = rows.find(item => item.name === providerName);
1301+
expect(row).toMatchObject({
1302+
name: providerName,
1303+
liveModels: false,
1304+
models: provider.models,
1305+
});
1306+
expect(row).not.toHaveProperty("discovery");
1307+
});
1308+
12721309
test("failed discovery falls back to defaultModel when no static models are configured (#308)", async () => {
12731310
clearModelCache("anthropic-compatible-default");
12741311
const originalFetch = globalThis.fetch;

tests/google-models-listing.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ describe("buildModelsRequest google routing", () => {
3838
expect(headers["x-goog-api-key"]).toBe("gk-123");
3939
});
4040

41-
test("google-antigravity (oauth) keeps Authorization: Bearer via registry backfill", () => {
42-
// A saved config may omit googleMode — the registry entry (cloud-code-assist) must win.
43-
const prov = { adapter: "google", authMode: "oauth", baseUrl: "https://daily-cloudcode-pa.googleapis.com" } as OcxProviderConfig;
41+
test("an explicit Antigravity live-discovery override keeps Authorization: Bearer", () => {
42+
// Static discovery is the preset default. If a user explicitly opts into the generic probe,
43+
// a saved config may still omit googleMode — the registry's cloud-code-assist mode must win.
44+
const prov = { adapter: "google", authMode: "oauth", baseUrl: "https://daily-cloudcode-pa.googleapis.com", liveModels: true } as OcxProviderConfig;
4445
const { url, headers } = buildModelsRequest(prov, "oauth-token", "google-antigravity");
4546
expect(url).toBe("https://daily-cloudcode-pa.googleapis.com/models");
4647
expect(headers["Authorization"]).toBe("Bearer oauth-token");

0 commit comments

Comments
 (0)