mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
fix(codex): normalize native cliproxy tuning aliases (#1254)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
normalizeModelIdForProvider,
|
normalizeModelIdForProvider,
|
||||||
normalizeModelIdForRouting,
|
normalizeModelIdForRouting,
|
||||||
normalizeModelEnvVarsForProvider,
|
normalizeModelEnvVarsForProvider,
|
||||||
|
parseCodexModelTuningAlias,
|
||||||
} from '../model-id-normalizer';
|
} from '../model-id-normalizer';
|
||||||
|
|
||||||
describe('model-id-normalizer', () => {
|
describe('model-id-normalizer', () => {
|
||||||
@@ -135,6 +136,20 @@ describe('model-id-normalizer', () => {
|
|||||||
'gpt-5.4-high-fast'
|
'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', () => {
|
describe('env normalization', () => {
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ const IFLOW_LEGACY_MODEL_ALIASES: Readonly<Record<string, string>> = Object.free
|
|||||||
'minimax-m2.5': 'qwen3-coder-plus',
|
'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 {
|
function trimModelId(model: string): string {
|
||||||
return model.trim();
|
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.
|
* Extract provider segment from `/api/provider/{provider}` request paths.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import {
|
|||||||
stringifyTomlObject,
|
stringifyTomlObject,
|
||||||
writeTomlFileAtomic,
|
writeTomlFileAtomic,
|
||||||
} from '../web-server/services/compatible-cli-toml-file-service';
|
} 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 CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
|
||||||
export const CODEX_CLIPROXY_PROVIDER_ID = 'cliproxy';
|
export const CODEX_CLIPROXY_PROVIDER_ID = 'cliproxy';
|
||||||
export const CODEX_CLIPROXY_PROVIDER_ENV_KEY = 'CLIPROXY_API_KEY';
|
export const CODEX_CLIPROXY_PROVIDER_ENV_KEY = 'CLIPROXY_API_KEY';
|
||||||
export const CODEX_CLIPROXY_PROVIDER_NAME = 'CLIProxy Codex';
|
export const CODEX_CLIPROXY_PROVIDER_NAME = 'CLIProxy Codex';
|
||||||
|
const CODEX_FAST_SERVICE_TIER = 'priority';
|
||||||
|
|
||||||
export interface CodexCliproxyProviderRepairResult {
|
export interface CodexCliproxyProviderRepairResult {
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
@@ -99,18 +102,28 @@ function buildProviderConfig(baseUrl: string, envKey: string): Record<string, un
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProviderBlock(baseUrl: string): string {
|
function appendProviderBlock(rawText: string, baseUrl: string): string {
|
||||||
return stringifyTomlObject({
|
const prefix = rawText.trimEnd();
|
||||||
|
const providerBlock = stringifyTomlObject({
|
||||||
model_providers: {
|
model_providers: {
|
||||||
[CODEX_CLIPROXY_PROVIDER_ID]: buildProviderConfig(baseUrl, CODEX_CLIPROXY_PROVIDER_ENV_KEY),
|
[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 {
|
function normalizeTopLevelCodexModelAlias(config: Record<string, unknown>): boolean {
|
||||||
const prefix = rawText.trimEnd();
|
const model = config.model;
|
||||||
const providerBlock = buildProviderBlock(baseUrl).trimEnd();
|
if (typeof model !== 'string') return false;
|
||||||
return prefix ? `${prefix}\n\n${providerBlock}\n` : `${providerBlock}\n`;
|
|
||||||
|
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(
|
export async function ensureCodexCliproxyProviderConfig(
|
||||||
@@ -131,6 +144,7 @@ export async function ensureCodexCliproxyProviderConfig(
|
|||||||
const modelProvidersValue = config.model_providers;
|
const modelProvidersValue = config.model_providers;
|
||||||
const providers = asObject(modelProvidersValue);
|
const providers = asObject(modelProvidersValue);
|
||||||
const expectedBaseUrl = buildCodexCliproxyProviderBaseUrl(port);
|
const expectedBaseUrl = buildCodexCliproxyProviderBaseUrl(port);
|
||||||
|
const normalizedModelAlias = normalizeTopLevelCodexModelAlias(config);
|
||||||
|
|
||||||
if (modelProvidersValue !== undefined && !providers) {
|
if (modelProvidersValue !== undefined && !providers) {
|
||||||
throw new Error(`Cannot repair ${displayPath}: [model_providers] must be a table.`);
|
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)) {
|
if (!providers || !Object.prototype.hasOwnProperty.call(providers, CODEX_CLIPROXY_PROVIDER_ID)) {
|
||||||
await writeTomlFileAtomic({
|
await writeTomlFileAtomic({
|
||||||
filePath: configPath,
|
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,
|
expectedMtime: fileProbe.diagnostics.mtimeMs ?? undefined,
|
||||||
fileLabel: 'config.toml',
|
fileLabel: 'config.toml',
|
||||||
});
|
});
|
||||||
@@ -154,15 +179,18 @@ export async function ensureCodexCliproxyProviderConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envKey = resolveProviderEnvKey(currentProvider);
|
const envKey = resolveProviderEnvKey(currentProvider);
|
||||||
|
const providerReady = isProviderReady(currentProvider, expectedBaseUrl, envKey);
|
||||||
|
|
||||||
if (isProviderReady(currentProvider, expectedBaseUrl, envKey)) {
|
if (!providerReady) {
|
||||||
return { changed: false, configPath, displayPath, envKey };
|
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
|
||||||
|
...currentProvider,
|
||||||
|
...buildProviderConfig(expectedBaseUrl, envKey),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
providers[CODEX_CLIPROXY_PROVIDER_ID] = {
|
if (providerReady && !normalizedModelAlias) {
|
||||||
...currentProvider,
|
return { changed: false, configPath, displayPath, envKey };
|
||||||
...buildProviderConfig(expectedBaseUrl, envKey),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
await writeTomlFileAtomic({
|
await writeTomlFileAtomic({
|
||||||
filePath: configPath,
|
filePath: configPath,
|
||||||
|
|||||||
@@ -136,4 +136,65 @@ supports_websockets = false
|
|||||||
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
expect(result.envKey).toBe('CLIPROXY_API_KEY');
|
||||||
expect(fs.readFileSync(configPath, 'utf8')).toBe(rawText);
|
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', () => {
|
it('loads the configured cliproxy provider env_key for ccsxp launches', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user