diff --git a/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts b/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts index 76ee7dc8..5e963b3d 100644 --- a/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts +++ b/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts @@ -12,6 +12,7 @@ import { normalizeModelIdForProvider, normalizeModelIdForRouting, normalizeModelEnvVarsForProvider, + parseCodexModelTuningAlias, } from '../model-id-normalizer'; describe('model-id-normalizer', () => { @@ -135,6 +136,20 @@ describe('model-id-normalizer', () => { 'gpt-5.4-high-fast' ); }); + + it('parses codex model tuning suffixes', () => { + expect(parseCodexModelTuningAlias('gpt-5.5-high')).toEqual({ + baseModel: 'gpt-5.5', + effort: 'high', + serviceTier: null, + }); + expect(parseCodexModelTuningAlias('gpt-5.5-fast-high')).toEqual({ + baseModel: 'gpt-5.5', + effort: 'high', + serviceTier: 'fast', + }); + expect(parseCodexModelTuningAlias('gpt-5.5[1m]')).toBeNull(); + }); }); describe('env normalization', () => { diff --git a/src/cliproxy/ai-providers/model-id-normalizer.ts b/src/cliproxy/ai-providers/model-id-normalizer.ts index 9686311a..7cee2d38 100644 --- a/src/cliproxy/ai-providers/model-id-normalizer.ts +++ b/src/cliproxy/ai-providers/model-id-normalizer.ts @@ -45,6 +45,15 @@ const IFLOW_LEGACY_MODEL_ALIASES: Readonly> = Object.free 'minimax-m2.5': 'qwen3-coder-plus', }); +export type CodexModelTuningEffort = 'medium' | 'high' | 'xhigh'; +export type CodexModelServiceTier = 'fast'; + +export interface CodexModelTuningAlias { + baseModel: string; + effort: CodexModelTuningEffort | null; + serviceTier: CodexModelServiceTier | null; +} + function trimModelId(model: string): string { return model.trim(); } @@ -93,6 +102,23 @@ function splitCodexTuningSuffix(model: string): { baseModel: string; suffix: str }; } +export function parseCodexModelTuningAlias(model: string): CodexModelTuningAlias | null { + const trimmed = trimModelId(model); + const { baseModel, suffix } = splitBaseModelAndSuffix(trimmed); + if (suffix) return null; + + const parsed = splitCodexTuningSuffix(baseModel); + if (!parsed.suffix) return null; + + const tokens = parsed.suffix.slice(1).split('-'); + const effort = tokens.find((token) => token !== 'fast') as CodexModelTuningEffort | undefined; + return { + baseModel: parsed.baseModel.trim(), + effort: effort ?? null, + serviceTier: tokens.includes('fast') ? 'fast' : null, + }; +} + /** * Extract provider segment from `/api/provider/{provider}` request paths. * diff --git a/src/targets/codex-cliproxy-provider-config.ts b/src/targets/codex-cliproxy-provider-config.ts index 6ef94a23..2944e9ba 100644 --- a/src/targets/codex-cliproxy-provider-config.ts +++ b/src/targets/codex-cliproxy-provider-config.ts @@ -6,11 +6,14 @@ import { stringifyTomlObject, writeTomlFileAtomic, } from '../web-server/services/compatible-cli-toml-file-service'; +import { getModelMaxLevel } from '../cliproxy/model-catalog'; +import { parseCodexModelTuningAlias } from '../cliproxy/ai-providers/model-id-normalizer'; export const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT'; export const CODEX_CLIPROXY_PROVIDER_ID = 'cliproxy'; export const CODEX_CLIPROXY_PROVIDER_ENV_KEY = 'CLIPROXY_API_KEY'; export const CODEX_CLIPROXY_PROVIDER_NAME = 'CLIProxy Codex'; +const CODEX_FAST_SERVICE_TIER = 'priority'; export interface CodexCliproxyProviderRepairResult { changed: boolean; @@ -99,18 +102,28 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record): boolean { + const model = config.model; + if (typeof model !== 'string') return false; + + const parsed = parseCodexModelTuningAlias(model); + if (!parsed || (!parsed.effort && !parsed.serviceTier)) return false; + if (!parsed.baseModel || getModelMaxLevel('codex', parsed.baseModel) === undefined) return false; + + config.model = parsed.baseModel; + if (parsed.effort) config.model_reasoning_effort = parsed.effort; + if (parsed.serviceTier) config.service_tier = CODEX_FAST_SERVICE_TIER; + return true; } export async function ensureCodexCliproxyProviderConfig( @@ -131,6 +144,7 @@ export async function ensureCodexCliproxyProviderConfig( const modelProvidersValue = config.model_providers; const providers = asObject(modelProvidersValue); const expectedBaseUrl = buildCodexCliproxyProviderBaseUrl(port); + const normalizedModelAlias = normalizeTopLevelCodexModelAlias(config); if (modelProvidersValue !== undefined && !providers) { throw new Error(`Cannot repair ${displayPath}: [model_providers] must be a table.`); @@ -139,7 +153,18 @@ export async function ensureCodexCliproxyProviderConfig( if (!providers || !Object.prototype.hasOwnProperty.call(providers, CODEX_CLIPROXY_PROVIDER_ID)) { await writeTomlFileAtomic({ filePath: configPath, - rawText: appendProviderBlock(fileProbe.rawText, expectedBaseUrl), + rawText: normalizedModelAlias + ? stringifyTomlObject({ + ...config, + model_providers: { + ...(providers ?? {}), + [CODEX_CLIPROXY_PROVIDER_ID]: buildProviderConfig( + expectedBaseUrl, + CODEX_CLIPROXY_PROVIDER_ENV_KEY + ), + }, + }) + : appendProviderBlock(fileProbe.rawText, expectedBaseUrl), expectedMtime: fileProbe.diagnostics.mtimeMs ?? undefined, fileLabel: 'config.toml', }); @@ -154,15 +179,18 @@ export async function ensureCodexCliproxyProviderConfig( } const envKey = resolveProviderEnvKey(currentProvider); + const providerReady = isProviderReady(currentProvider, expectedBaseUrl, envKey); - if (isProviderReady(currentProvider, expectedBaseUrl, envKey)) { - return { changed: false, configPath, displayPath, envKey }; + if (!providerReady) { + providers[CODEX_CLIPROXY_PROVIDER_ID] = { + ...currentProvider, + ...buildProviderConfig(expectedBaseUrl, envKey), + }; } - providers[CODEX_CLIPROXY_PROVIDER_ID] = { - ...currentProvider, - ...buildProviderConfig(expectedBaseUrl, envKey), - }; + if (providerReady && !normalizedModelAlias) { + return { changed: false, configPath, displayPath, envKey }; + } await writeTomlFileAtomic({ filePath: configPath, diff --git a/tests/unit/targets/codex-cliproxy-provider-config.test.ts b/tests/unit/targets/codex-cliproxy-provider-config.test.ts index a470399e..f182ce2c 100644 --- a/tests/unit/targets/codex-cliproxy-provider-config.test.ts +++ b/tests/unit/targets/codex-cliproxy-provider-config.test.ts @@ -136,4 +136,65 @@ supports_websockets = false expect(result.envKey).toBe('CLIPROXY_API_KEY'); expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText); }); + + it('normalizes a ready native Codex tuning alias before requests reach cliproxy', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync( + configPath, + `model = "gpt-5.5-high-fast" + +[model_providers.cliproxy] +name = "CLIProxy Codex" +base_url = "http://localhost:8317/api/provider/codex" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses" +requires_openai_auth = false +supports_websockets = false +`, + 'utf8' + ); + + const result = await ensureCodexCliproxyProviderConfig(8317, env); + + expect(result.changed).toBe(true); + const rawText = fs.readFileSync(configPath, 'utf8'); + expect(rawText).toContain('model = "gpt-5.5"'); + expect(rawText).toContain('model_reasoning_effort = "high"'); + expect(rawText).toContain('service_tier = "priority"'); + expect(rawText).toContain('[model_providers.cliproxy]'); + expect(rawText).not.toContain('gpt-5.5-high-fast'); + }); + + it('normalizes a native Codex effort alias when adding the missing provider', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(configPath, 'model = "gpt-5.5-xhigh"\n', 'utf8'); + + const result = await ensureCodexCliproxyProviderConfig(8317, env); + + expect(result.changed).toBe(true); + const rawText = fs.readFileSync(configPath, 'utf8'); + expect(rawText).toContain('model = "gpt-5.5"'); + expect(rawText).toContain('model_reasoning_effort = "xhigh"'); + expect(rawText).toContain('[model_providers.cliproxy]'); + }); + + it('does not strip unknown top-level models that happen to end with effort tokens', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + const rawText = `model = "literal-high" + +[model_providers.cliproxy] +name = "CLIProxy Codex" +base_url = "http://localhost:8317/api/provider/codex" +env_key = "CLIPROXY_API_KEY" +wire_api = "responses" +requires_openai_auth = false +supports_websockets = false +`; + fs.writeFileSync(configPath, rawText, 'utf8'); + + const result = await ensureCodexCliproxyProviderConfig(8317, env); + + expect(result.changed).toBe(false); + expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText); + }); }); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 8e916942..3c0fc2a5 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -747,6 +747,36 @@ process.exit(0); ]); }); + it('normalizes ccsxp native Codex tuning aliases in config.toml', () => { + if (process.platform === 'win32') return; + + const codexHome = path.join(tmpHome, '.codex'); + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.5-high-fast"\n'); + + const result = runCcsxpAlias(['fix failing tests'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + HOME: tmpHome, + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath, + }); + + expect(result.status).toBe(0); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([ + ['--config', 'model_provider="cliproxy"', 'fix failing tests'], + ]); + const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8'); + expect(codexConfig).toContain('model = "gpt-5.5"'); + expect(codexConfig).toContain('model_reasoning_effort = "high"'); + expect(codexConfig).toContain('service_tier = "priority"'); + expect(codexConfig).toContain('[model_providers.cliproxy]'); + expect(codexConfig).not.toContain('gpt-5.5-high-fast'); + }); + it('loads the configured cliproxy provider env_key for ccsxp launches', () => { if (process.platform === 'win32') return;