mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(codex): normalize native cliproxy tuning aliases (#1254)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -45,6 +45,15 @@ const IFLOW_LEGACY_MODEL_ALIASES: Readonly<Record<string, string>> = 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.
|
||||
*
|
||||
|
||||
@@ -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<string, un
|
||||
};
|
||||
}
|
||||
|
||||
function buildProviderBlock(baseUrl: string): string {
|
||||
return stringifyTomlObject({
|
||||
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
||||
const prefix = rawText.trimEnd();
|
||||
const providerBlock = stringifyTomlObject({
|
||||
model_providers: {
|
||||
[CODEX_CLIPROXY_PROVIDER_ID]: buildProviderConfig(baseUrl, CODEX_CLIPROXY_PROVIDER_ENV_KEY),
|
||||
},
|
||||
});
|
||||
}).trimEnd();
|
||||
return prefix ? `${prefix}\n\n${providerBlock}\n` : `${providerBlock}\n`;
|
||||
}
|
||||
|
||||
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
||||
const prefix = rawText.trimEnd();
|
||||
const providerBlock = buildProviderBlock(baseUrl).trimEnd();
|
||||
return prefix ? `${prefix}\n\n${providerBlock}\n` : `${providerBlock}\n`;
|
||||
function normalizeTopLevelCodexModelAlias(config: Record<string, unknown>): 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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user