mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 00:22:34 +00:00
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:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user