fix(cliproxy): normalize codex effort aliases without reasoning proxy

This commit is contained in:
Tam Nhu Tran
2026-02-22 23:12:23 +07:00
parent 5003ae55db
commit 742b5ed580
2 changed files with 136 additions and 1 deletions
+41 -1
View File
@@ -24,7 +24,7 @@ import { stripClaudeCodeEnv } from '../../utils/shell-executor';
import { CodexReasoningProxy } from '../codex-reasoning-proxy';
import { ToolSanitizationProxy } from '../tool-sanitization-proxy';
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
import { normalizeModelIdForProvider } from '../model-id-normalizer';
import { MODEL_ENV_VAR_KEYS, normalizeModelIdForProvider } from '../model-id-normalizer';
export interface RemoteProxyConfig {
host: string;
@@ -61,6 +61,38 @@ export interface ProxyChainConfig {
compositeDefaultTier?: 'opus' | 'sonnet' | 'haiku';
}
const CODEX_EFFORT_SUFFIX_REGEX = /^(.*)-(xhigh|high|medium)$/i;
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
function normalizeCodexModelForDirectUpstream(model: string): string {
const withoutExtendedContext = model.trim().replace(EXTENDED_CONTEXT_SUFFIX_REGEX, '').trim();
if (!withoutExtendedContext) return withoutExtendedContext;
const effortMatch = withoutExtendedContext.match(CODEX_EFFORT_SUFFIX_REGEX);
if (!effortMatch?.[1] || !effortMatch[2]) {
return withoutExtendedContext;
}
return `${effortMatch[1].trim()}(${effortMatch[2].toLowerCase()})`;
}
function normalizeCodexEnvForDirectUpstream(envVars: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
let nextEnv: NodeJS.ProcessEnv | null = null;
for (const key of MODEL_ENV_VAR_KEYS) {
const value = envVars[key];
if (typeof value !== 'string' || value.trim().length === 0) continue;
const normalizedValue = normalizeCodexModelForDirectUpstream(value);
if (normalizedValue === value) continue;
if (!nextEnv) nextEnv = { ...envVars };
nextEnv[key] = normalizedValue;
}
return nextEnv ?? envVars;
}
/**
* Build final environment variables for Claude CLI execution
* Handles proxy chain ordering and integration with hooks
@@ -190,6 +222,14 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
// Auto-enabled for Gemini, opt-in for Claude (--1m flag)
applyExtendedContextConfig(envVars, provider, extendedContextOverride);
// Fallback compatibility for Codex:
// CLIProxyAPI provider lookup recognizes model(level) suffix form, while CCS stores codex
// effort aliases as model-level (e.g., gpt-5.3-codex-high). If codex reasoning proxy is
// unavailable, normalize to model(level) to avoid "unknown provider for model ..." failures.
if (provider === 'codex' && !isComposite && !codexReasoningPort) {
envVars = normalizeCodexEnvForDirectUpstream(envVars);
}
// Determine the final ANTHROPIC_BASE_URL based on active proxies
// Chain order: Claude CLI → [CodexReasoningProxy] → [ToolSanitizationProxy] → CLIProxy
let finalBaseUrl = envVars.ANTHROPIC_BASE_URL;
@@ -0,0 +1,95 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it } from 'bun:test';
import { buildClaudeEnvironment } from '../../../src/cliproxy/executor/env-resolver';
const tempDirs: string[] = [];
function createCodexSettingsFile(models: {
defaultModel: string;
opusModel: string;
sonnetModel: string;
haikuModel: string;
}): string {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-fallback-'));
tempDirs.push(tempDir);
const settingsPath = path.join(tempDir, 'codex-test.settings.json');
fs.writeFileSync(
settingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
ANTHROPIC_AUTH_TOKEN: 'test-token',
ANTHROPIC_MODEL: models.defaultModel,
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opusModel,
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnetModel,
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haikuModel,
},
},
null,
2
) + '\n'
);
return settingsPath;
}
describe('buildClaudeEnvironment codex fallback normalization', () => {
afterEach(() => {
while (tempDirs.length > 0) {
const tempDir = tempDirs.pop();
if (tempDir) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
});
it('normalizes codex effort aliases when reasoning proxy is unavailable', () => {
const settingsPath = createCodexSettingsFile({
defaultModel: 'gpt-5.3-codex-high',
opusModel: 'gpt-5.3-codex-xhigh',
sonnetModel: 'gpt-5.3-codex-high',
haikuModel: 'gpt-5-mini-medium',
});
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
customSettingsPath: settingsPath,
verbose: false,
});
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex(high)');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex(xhigh)');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex(high)');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini(medium)');
});
it('keeps codex effort aliases when reasoning proxy is active', () => {
const settingsPath = createCodexSettingsFile({
defaultModel: 'gpt-5.3-codex-high',
opusModel: 'gpt-5.3-codex-xhigh',
sonnetModel: 'gpt-5.3-codex-high',
haikuModel: 'gpt-5-mini-medium',
});
const env = buildClaudeEnvironment({
provider: 'codex',
useRemoteProxy: false,
localPort: 8317,
customSettingsPath: settingsPath,
codexReasoningPort: 9444,
verbose: false,
});
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-high');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-medium');
expect(env.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:9444/api/provider/codex');
});
});