Skip to content

Commit 40c91ff

Browse files
committed
fix(providers): keep Antigravity catalog static
1 parent cd46a34 commit 40c91ff

12 files changed

Lines changed: 109 additions & 20 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 (on Unix, install with `curl -fsSL https://cli.kiro.dev/install | bash`; on Windows PowerShell, use `irm 'https://cli.kiro.dev/install.ps1' | iex`; 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` セッションを取り込みます(Unix では `curl -fsSL https://cli.kiro.dev/install | bash`、Windows PowerShell では `irm 'https://cli.kiro.dev/install.ps1' | iex` でインストールしてから `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` 세션을 가져옵니다(Unix에서는 `curl -fsSL https://cli.kiro.dev/install | bash`, Windows PowerShell에서는 `irm 'https://cli.kiro.dev/install.ps1' | iex`로 설치한 뒤 `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 (в Unix: `curl -fsSL https://cli.kiro.dev/install | bash`; в Windows PowerShell: `irm 'https://cli.kiro.dev/install.ps1' | iex`; затем выполните `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 会话(Unix 使用 `curl -fsSL https://cli.kiro.dev/install | bash`;Windows PowerShell 使用 `irm 'https://cli.kiro.dev/install.ps1' | iex`;然后运行 `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
@@ -381,7 +381,6 @@ function boundedOwnedBy(value: unknown): string | undefined {
381381

382382
export async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
383383
if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
384-
const apiKey = await resolveModelsAuthToken(name, prov);
385384
const seedVertexDefault = prov.adapter === "google"
386385
&& prov.googleMode === "vertex"
387386
&& (prov.models?.length ?? 0) === 0
@@ -392,6 +391,13 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
392391
provider: name,
393392
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
394393
}));
394+
// Static catalogs never need an OAuth refresh or an upstream model request. Clear any
395+
// discovery failure left by an older live configuration even when the account is logged out.
396+
if (prov.liveModels === false) {
397+
clearProviderDiscoveryStatus(name);
398+
return configured;
399+
}
400+
const apiKey = await resolveModelsAuthToken(name, prov);
395401
// A configured default is a real callable selector and must remain discoverable when a
396402
// compatible provider's live /models request fails (issue #308). Keep this separate from the
397403
// explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero
@@ -410,10 +416,6 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
410416
: models
411417
);
412418
if (prov.adapter === "cursor") {
413-
if (prov.liveModels === false) {
414-
clearProviderDiscoveryStatus(name);
415-
return configured;
416-
}
417419
if (!apiKey) return configured;
418420
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
419421
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
@@ -448,10 +450,6 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
448450
// matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
449451
return configured;
450452
}
451-
if (prov.liveModels === false) {
452-
clearProviderDiscoveryStatus(name);
453-
return configured;
454-
}
455453
const fresh = getFreshCached(name, ttlMs);
456454
if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
457455
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);

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");

tests/oauth-provider-reconcile.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import { loadConfig } from "../src/config";
6-
import { reconcileOAuthProviders } from "../src/oauth";
6+
import { OAUTH_PROVIDERS, reconcileOAuthProviders, upsertOAuthProvider } from "../src/oauth";
77
import { getCredential, saveCredential } from "../src/oauth/store";
88
import type { OcxConfig } from "../src/types";
99

@@ -61,6 +61,7 @@ describe("OAuth provider reconciliation", () => {
6161
expect(provider.models).not.toContain("gemini-3.6-flash-medium");
6262
expect(provider.models).not.toContain("gemini-3.6-flash-high");
6363
expect(provider.modelContextWindows?.["gemini-3.6-flash"]).toBe(1_048_576);
64+
expect(provider.liveModels).toBe(false);
6465
expect(provider.project).toBe("config-project-sentinel");
6566
expect(provider.note).toBe("user-owned-note");
6667
expect(getCredential("google-antigravity")).toMatchObject({
@@ -71,6 +72,27 @@ describe("OAuth provider reconciliation", () => {
7172

7273
const persisted = loadConfig();
7374
expect(persisted.providers["google-antigravity"]?.defaultModel).toBe("gemini-3.6-flash");
75+
expect(persisted.providers["google-antigravity"]?.liveModels).toBe(false);
7476
expect(reconcileOAuthProviders(config)).toBe(false);
7577
});
78+
79+
test("preserves an explicit Antigravity liveModels override during reconcile and re-login", () => {
80+
const config = {
81+
port: 10100,
82+
defaultProvider: "google-antigravity",
83+
providers: {
84+
"google-antigravity": {
85+
...structuredClone(OAUTH_PROVIDERS["google-antigravity"].providerConfig),
86+
liveModels: true,
87+
},
88+
},
89+
} satisfies OcxConfig;
90+
91+
expect(reconcileOAuthProviders(config)).toBe(false);
92+
expect(config.providers["google-antigravity"].liveModels).toBe(true);
93+
94+
upsertOAuthProvider(config, "google-antigravity");
95+
expect(config.providers["google-antigravity"].liveModels).toBe(true);
96+
expect(config.providers["google-antigravity"].models).toHaveLength(6);
97+
});
7698
});

0 commit comments

Comments
 (0)