From 50b0ffba759b7de6fac525e31e5d02bde20b54f3 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Fri, 1 May 2026 02:19:31 -0400 Subject: [PATCH] fix(cliproxy): refresh upstream model and quota surfaces (#1158) * fix(cliproxy): refresh upstream model and quota surfaces * test(cliproxy): cover codex legacy model fallback --- src/cliproxy/binary/platform-detector.ts | 4 +- .../quota-fetcher-antigravity-failure.test.ts | 107 ++++++++++++++++++ src/cliproxy/quota/quota-fetcher.ts | 59 ++++++++-- ui/src/lib/model-catalogs.ts | 107 +++++------------- .../model-config-section.test.tsx | 2 +- .../cliproxy/provider-model-selector.test.tsx | 21 ++++ .../unit/ui/lib/model-catalogs-codex.test.ts | 17 ++- 7 files changed, 220 insertions(+), 97 deletions(-) diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 4b5e38f5..9c1d2f9f 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -19,13 +19,13 @@ export const BACKEND_CONFIG = { repo: 'router-for-me/CLIProxyAPI', binaryPrefix: 'CLIProxyAPI', executable: 'cli-proxy-api', - fallbackVersion: '6.8.2', + fallbackVersion: '6.9.45', }, plus: { repo: 'kaitranntt/CLIProxyAPIPlus', binaryPrefix: 'CLIProxyAPIPlus', executable: 'cli-proxy-api-plus', - fallbackVersion: '6.9.36-0', + fallbackVersion: '6.9.45-0', }, } as const; diff --git a/src/cliproxy/quota/__tests__/quota-fetcher-antigravity-failure.test.ts b/src/cliproxy/quota/__tests__/quota-fetcher-antigravity-failure.test.ts index ef60b0e7..b02ee89a 100644 --- a/src/cliproxy/quota/__tests__/quota-fetcher-antigravity-failure.test.ts +++ b/src/cliproxy/quota/__tests__/quota-fetcher-antigravity-failure.test.ts @@ -208,4 +208,111 @@ describe('Antigravity quota failure metadata', () => { fs.rmSync(tempHome, { recursive: true, force: true }); } }); + + it('tries the daily loadCodeAssist host before falling back to prod', async () => { + const moduleId = Date.now() + Math.random(); + const { fetchAccountQuota } = await import(`../quota-fetcher?agy-daily-host=${moduleId}`); + const { getProviderAuthDir } = await import( + `../../config/config-generator?agy-config=${moduleId}` + ); + const fs = await import('node:fs'); + const os = await import('node:os'); + const path = await import('node:path'); + + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-agy-daily-host-')); + const originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + + try { + const authDir = getProviderAuthDir('agy'); + fs.mkdirSync(authDir, { recursive: true }); + fs.writeFileSync( + path.join(authDir, 'antigravity-user@example.com.json'), + JSON.stringify({ + type: 'antigravity', + email: 'user@example.com', + access_token: 'token', + }) + ); + + const originalFetch = globalThis.fetch; + const urls: string[] = []; + globalThis.fetch = (async (input, init) => { + const url = String(input); + urls.push(url); + + if (url === 'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist') { + const bodyText = typeof init?.body === 'string' ? init.body : ''; + expect(init?.headers).toMatchObject({ + 'X-Goog-Api-Client': 'gl-node/22.21.1', + }); + expect(bodyText).toBe( + JSON.stringify({ + metadata: { + ide_name: 'antigravity', + ide_type: 'ANTIGRAVITY', + ide_version: '1.21.9', + }, + }) + ); + return new Response('daily unavailable', { status: 503 }); + } + + if (url === 'https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist') { + return new Response( + JSON.stringify({ + cloudaicompanionProject: { id: 'project-x' }, + paidTier: { id: 'g1-pro-tier' }, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + if (url === 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels') { + return new Response( + JSON.stringify({ + models: { + 'gemini-3-pro-high': { + quotaInfo: { + remainingFraction: 0.75, + resetTime: '2026-05-01T10:00:00Z', + }, + }, + }, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + return new Response('unexpected url', { status: 500 }); + }) as typeof fetch; + + try { + const result = await fetchAccountQuota('agy', 'user@example.com'); + expect(result.success).toBe(true); + expect(result.tier).toBe('pro'); + expect(result.projectId).toBe('project-x'); + expect(urls).toEqual([ + 'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', + 'https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', + 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', + ]); + } finally { + globalThis.fetch = originalFetch; + } + } finally { + if (originalCcsHome === undefined) { + delete process.env.CCS_HOME; + } else { + process.env.CCS_HOME = originalCcsHome; + } + fs.rmSync(tempHome, { recursive: true, force: true }); + } + }); }); diff --git a/src/cliproxy/quota/quota-fetcher.ts b/src/cliproxy/quota/quota-fetcher.ts index 8bf92e9d..4a3699b7 100644 --- a/src/cliproxy/quota/quota-fetcher.ts +++ b/src/cliproxy/quota/quota-fetcher.ts @@ -82,17 +82,20 @@ export interface QuotaResult { } /** Google Cloud Code API endpoints */ +const ANTIGRAVITY_DAILY_API_BASE = 'https://daily-cloudcode-pa.googleapis.com'; const ANTIGRAVITY_API_BASE = 'https://cloudcode-pa.googleapis.com'; const ANTIGRAVITY_API_VERSION = 'v1internal'; +const ANTIGRAVITY_LOADCODEASSIST_BASE_URLS = [ + ANTIGRAVITY_DAILY_API_BASE, + ANTIGRAVITY_API_BASE, +] as const; const MANAGEMENT_API_TIMEOUT_MS = 5000; -/** Headers for loadCodeAssist (matches CLIProxyAPI antigravity.go) */ +/** Headers for loadCodeAssist (matches current CLIProxyAPIPlus control-plane requests) */ const LOADCODEASSIST_HEADERS = { 'Content-Type': 'application/json', - 'User-Agent': 'google-api-nodejs-client/9.15.1', - 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1', - 'Client-Metadata': - '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}', + 'User-Agent': 'antigravity/1.21.9 darwin/arm64 google-api-nodejs-client/10.3.0', + 'X-Goog-Api-Client': 'gl-node/22.21.1', }; /** Headers for fetchAvailableModels (matches CLIProxyAPI antigravity_executor.go) */ @@ -543,6 +546,40 @@ async function performAntigravityRequest( } } +async function performAntigravityRequestWithBaseUrlFallback( + accountId: string, + accessToken: string, + baseUrls: readonly string[], + apiPath: string, + headers: Record, + body: string +): Promise { + let lastResponse: ManagedResponse | null = null; + + for (const baseUrl of baseUrls) { + const response = await performAntigravityRequest( + accountId, + accessToken, + `${baseUrl}/${apiPath}`, + headers, + body + ); + if (response.status >= 200 && response.status < 300) { + return response; + } + lastResponse = response; + } + + return ( + lastResponse ?? { + status: 503, + bodyText: 'No Antigravity API endpoint available', + json: null, + viaManagement: false, + } + ); +} + /** * Read auth data from auth file (access token, project_id, expiry status) */ @@ -616,18 +653,18 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData | * Uses paidTier.id for accurate tier detection (g1-ultra-tier, g1-pro-tier) */ async function getProjectId(accountId: string, accessToken: string): Promise { - const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:loadCodeAssist`; const body = JSON.stringify({ metadata: { - ideType: 'IDE_UNSPECIFIED', - platform: 'PLATFORM_UNSPECIFIED', - pluginType: 'GEMINI', + ide_name: 'antigravity', + ide_type: 'ANTIGRAVITY', + ide_version: '1.21.9', }, }); - const response = await performAntigravityRequest( + const response = await performAntigravityRequestWithBaseUrlFallback( accountId, accessToken, - url, + ANTIGRAVITY_LOADCODEASSIST_BASE_URLS, + `${ANTIGRAVITY_API_VERSION}:loadCodeAssist`, LOADCODEASSIST_HEADERS, body ); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index ab2ed45b..18a40732 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -257,130 +257,83 @@ export const MODEL_CATALOGS: Record = { codex: { provider: 'codex', displayName: 'Codex', - defaultModel: 'gpt-5-codex', + defaultModel: 'gpt-5.4', models: [ { id: 'gpt-5.5', name: 'GPT-5.5', tier: 'paid', - description: 'Newest Codex-only GPT-5 family model', + description: + 'Newest Codex-released GPT-5 family model; falls back to GPT-5.4 on free plans', codexMaxEffort: 'xhigh', presetMapping: { default: 'gpt-5.5', opus: 'gpt-5.5', sonnet: 'gpt-5.5', - haiku: 'gpt-5-codex-mini', + haiku: 'gpt-5.4-mini', }, }, { - id: 'gpt-5-codex', - name: 'GPT-5 Codex', - description: 'Cross-plan safe Codex default', + id: 'gpt-5.4', + name: 'GPT-5.4', + description: 'Recommended Codex default for most coding and agentic tasks', codexMaxEffort: 'xhigh', presetMapping: { - default: 'gpt-5-codex', - opus: 'gpt-5-codex', - sonnet: 'gpt-5-codex', - haiku: 'gpt-5-codex-mini', + default: 'gpt-5.4', + opus: 'gpt-5.4', + sonnet: 'gpt-5.4', + haiku: 'gpt-5.4-mini', }, }, { - id: 'gpt-5-codex-mini', - name: 'GPT-5 Codex Mini', - description: 'Faster and cheaper Codex option', + id: 'gpt-5.4-mini', + name: 'GPT-5.4 Mini', + description: 'Fast, lower-cost Codex option for lighter tasks and haiku-tier routing', codexMaxEffort: 'high', presetMapping: { - default: 'gpt-5-codex-mini', - opus: 'gpt-5-codex', - sonnet: 'gpt-5-codex', - haiku: 'gpt-5-codex-mini', - }, - }, - { - id: 'gpt-5-mini', - name: 'GPT-5 Mini', - description: 'Legacy mini model ID kept for backwards compatibility', - codexMaxEffort: 'high', - presetMapping: { - default: 'gpt-5-mini', - opus: 'gpt-5-codex', - sonnet: 'gpt-5-mini', - haiku: 'gpt-5-mini', - }, - }, - { - id: 'gpt-5.1-codex-mini', - name: 'GPT-5.1 Codex Mini', - description: 'Legacy fast Codex mini model', - codexMaxEffort: 'high', - presetMapping: { - default: 'gpt-5.1-codex-mini', - opus: 'gpt-5.1-codex-max', - sonnet: 'gpt-5.1-codex-max', - haiku: 'gpt-5.1-codex-mini', - }, - }, - { - id: 'gpt-5.1-codex-max', - name: 'GPT-5.1 Codex Max', - description: 'Higher-effort Codex model with xhigh support', - codexMaxEffort: 'xhigh', - presetMapping: { - default: 'gpt-5.1-codex-max', - opus: 'gpt-5.1-codex-max', - sonnet: 'gpt-5.1-codex-max', - haiku: 'gpt-5.1-codex-mini', - }, - }, - { - id: 'gpt-5.2-codex', - name: 'GPT-5.2 Codex', - description: 'Cross-plan Codex model with xhigh support', - codexMaxEffort: 'xhigh', - presetMapping: { - default: 'gpt-5.2-codex', - opus: 'gpt-5.2-codex', - sonnet: 'gpt-5.2-codex', - haiku: 'gpt-5-codex-mini', + default: 'gpt-5.4-mini', + opus: 'gpt-5.4', + sonnet: 'gpt-5.4', + haiku: 'gpt-5.4-mini', }, }, { id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', tier: 'paid', - description: 'Paid Codex plans only', + description: 'Previous flagship coding model whose capabilities now power GPT-5.4', codexMaxEffort: 'xhigh', presetMapping: { default: 'gpt-5.3-codex', opus: 'gpt-5.3-codex', sonnet: 'gpt-5.3-codex', - haiku: 'gpt-5-codex-mini', + haiku: 'gpt-5.4-mini', }, }, { id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark', tier: 'paid', - description: 'Paid Codex plans only, ultra-fast coding model', + description: + 'Research preview model for ChatGPT Pro subscribers, optimized for near-instant coding iteration', codexMaxEffort: 'xhigh', presetMapping: { default: 'gpt-5.3-codex-spark', opus: 'gpt-5.3-codex', sonnet: 'gpt-5.3-codex', - haiku: 'gpt-5-codex-mini', + haiku: 'gpt-5.4-mini', }, }, { - id: 'gpt-5.4', - name: 'GPT-5.4', - tier: 'paid', - description: 'Paid Codex plans only, latest GPT-5 family model', + id: 'gpt-5.2', + name: 'GPT-5.2', + description: 'Previous general-purpose Codex model', codexMaxEffort: 'xhigh', presetMapping: { - default: 'gpt-5.4', - opus: 'gpt-5.4', - sonnet: 'gpt-5.4', - haiku: 'gpt-5-codex-mini', + default: 'gpt-5.2', + opus: 'gpt-5.2', + sonnet: 'gpt-5.2', + haiku: 'gpt-5.4-mini', }, }, ], diff --git a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx index f3eef2bc..f26e9909 100644 --- a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx @@ -45,7 +45,7 @@ describe('ModelConfigSection presets', () => { ANTHROPIC_MODEL: 'gpt-5.4', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.4', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.4', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-codex-mini', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5.4-mini', }); }); diff --git a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx index d7c15e75..abfde015 100644 --- a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx @@ -130,6 +130,27 @@ describe('FlexibleModelSelector', () => { expect(screen.getAllByText('gpt-5.3-codex-high').length).toBeGreaterThan(0); }); + it('preserves saved legacy codex model IDs under the current-value fallback', async () => { + render( + + ); + + expect(screen.getByRole('button', { name: /gpt-5-codex/i })).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', { name: /gpt-5-codex/i })); + + expect(screen.getByText('Current value')).toBeInTheDocument(); + expect(screen.getAllByText('gpt-5-codex').length).toBeGreaterThan(0); + expect(screen.getByText('gpt-5.4')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.4-mini')).toBeInTheDocument(); + }); + it('preserves explicit suffixes on supplemental codex models outside the static catalog', async () => { const onChange = vi.fn(); diff --git a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts index 4e3f5f94..c6f7f1fa 100644 --- a/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts +++ b/ui/tests/unit/ui/lib/model-catalogs-codex.test.ts @@ -2,17 +2,22 @@ import { describe, expect, it } from 'vitest'; import { MODEL_CATALOGS } from '@/lib/model-catalogs'; describe('codex model catalog defaults', () => { - it('uses gpt-5-codex-mini as the haiku mapping for cross-plan codex presets', () => { + it('mirrors the current Codex runtime catalog and free-safe defaults', () => { const codexCatalog = MODEL_CATALOGS.codex; const codex55 = codexCatalog.models.find((model) => model.id === 'gpt-5.5'); const codex53 = codexCatalog.models.find((model) => model.id === 'gpt-5.3-codex'); - const codex52 = codexCatalog.models.find((model) => model.id === 'gpt-5.2-codex'); - const codexMini = codexCatalog.models.find((model) => model.id === 'gpt-5-codex-mini'); + const codex52 = codexCatalog.models.find((model) => model.id === 'gpt-5.2'); + const codex54 = codexCatalog.models.find((model) => model.id === 'gpt-5.4'); + const codexMini = codexCatalog.models.find((model) => model.id === 'gpt-5.4-mini'); - expect(codex55?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); + expect(codexCatalog.defaultModel).toBe('gpt-5.4'); + expect(codex55?.tier).toBe('paid'); + expect(codex55?.presetMapping?.haiku).toBe('gpt-5.4-mini'); expect(codex55?.codexMaxEffort).toBe('xhigh'); - expect(codex53?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); - expect(codex52?.presetMapping?.haiku).toBe('gpt-5-codex-mini'); + expect(codex54?.tier).toBeUndefined(); + expect(codex54?.presetMapping?.haiku).toBe('gpt-5.4-mini'); + expect(codex53?.presetMapping?.haiku).toBe('gpt-5.4-mini'); + expect(codex52?.presetMapping?.haiku).toBe('gpt-5.4-mini'); expect(codex53?.codexMaxEffort).toBe('xhigh'); expect(codexMini?.codexMaxEffort).toBe('high'); });