fix(cliproxy): refresh upstream model and quota surfaces (#1158)

* fix(cliproxy): refresh upstream model and quota surfaces

* test(cliproxy): cover codex legacy model fallback
This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-01 02:19:31 -04:00
committed by GitHub
parent 66865127d7
commit 50b0ffba75
7 changed files with 220 additions and 97 deletions
+2 -2
View File
@@ -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;
@@ -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 });
}
});
});
+48 -11
View File
@@ -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<string, string>,
body: string
): Promise<ManagedResponse> {
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<ProjectLookupResult> {
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
);
+30 -77
View File
@@ -257,130 +257,83 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
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',
},
},
],
@@ -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',
});
});
@@ -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(
<FlexibleModelSelector
label="Primary model"
value="gpt-5-codex"
onChange={vi.fn()}
catalog={MODEL_CATALOGS.codex}
allModels={[]}
/>
);
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();
@@ -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');
});