diff --git a/src/cliproxy/executor/env-resolver.ts b/src/cliproxy/executor/env-resolver.ts index ace7a18d..dbe49ad6 100644 --- a/src/cliproxy/executor/env-resolver.ts +++ b/src/cliproxy/executor/env-resolver.ts @@ -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 { + 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'); + }); +});