From 945db1d6909e546fef988f7823d60e03adcffd44 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 14 Feb 2026 11:31:18 +0700 Subject: [PATCH] feat(cliproxy): clarify codex effort mapping and preset UX --- config/base-codex.settings.json | 6 +- src/cliproxy/model-catalog.ts | 4 +- src/cliproxy/model-config.ts | 47 ++++++++--- src/cliproxy/services/variant-settings.ts | 58 +++++++++---- src/commands/help-command.ts | 5 +- .../cliproxy/variant-update-service.test.ts | 13 ++- .../provider-editor/custom-preset-dialog.tsx | 6 ++ .../provider-editor/model-config-section.tsx | 6 ++ .../cliproxy/provider-model-selector.tsx | 81 ++++++++++++++----- ui/src/lib/codex-effort.ts | 21 +++++ ui/src/lib/model-catalogs.ts | 24 +++--- .../settings/sections/thinking/index.tsx | 7 +- ui/tests/unit/ui/lib/codex-effort.test.ts | 40 +++++++++ 13 files changed, 245 insertions(+), 73 deletions(-) create mode 100644 ui/src/lib/codex-effort.ts create mode 100644 ui/tests/unit/ui/lib/codex-effort.test.ts diff --git a/config/base-codex.settings.json b/config/base-codex.settings.json index fbf1314a..dcc10763 100644 --- a/config/base-codex.settings.json +++ b/config/base-codex.settings.json @@ -2,9 +2,9 @@ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8317/api/provider/codex", "ANTHROPIC_AUTH_TOKEN": "ccs-internal-managed", - "ANTHROPIC_MODEL": "gpt-5.3-codex", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-5.3-codex", + "ANTHROPIC_MODEL": "gpt-5.3-codex-xhigh", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-5.3-codex-xhigh", "ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5.3-codex-high", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-5-mini" + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-5-mini-medium" } } diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index a3f75b24..e50d0ffc 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -165,7 +165,7 @@ export const MODEL_CATALOG: Partial> = { id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', - description: 'Full reasoning support (xhigh)', + description: 'Supports up to xhigh effort', thinking: { type: 'levels', levels: ['medium', 'high', 'xhigh'], @@ -187,7 +187,7 @@ export const MODEL_CATALOG: Partial> = { id: 'gpt-5-mini', name: 'GPT-5 Mini', - description: 'Capped at high reasoning (no xhigh)', + description: 'Capped at high effort (no xhigh)', thinking: { type: 'levels', levels: ['medium', 'high'], diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index 8e1644a2..1b03a8f4 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -14,6 +14,22 @@ import { CLIProxyProvider } from './types'; import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; import { getCcsDir } from '../utils/config-manager'; +const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; + +function stripCodexEffortSuffix(model: string, provider: CLIProxyProvider): string { + if (provider !== 'codex') return model; + return model.replace(CODEX_EFFORT_SUFFIX_REGEX, ''); +} + +function normalizeCodexTierModel( + provider: CLIProxyProvider, + model: string, + fallbackEffort: 'medium' | 'high' | 'xhigh' +): string { + if (provider !== 'codex') return model; + return CODEX_EFFORT_SUFFIX_REGEX.test(model) ? model : `${model}-${fallbackEffort}`; +} + /** * Check if provider has user settings configured */ @@ -120,7 +136,9 @@ export async function configureProviderModel( // Find default index - use current model if configured, otherwise catalog default const currentModel = getCurrentModel(provider, customSettingsPath); - const targetModel = currentModel || catalog.defaultModel; + const targetModel = currentModel + ? stripCodexEffortSuffix(currentModel, provider) + : catalog.defaultModel; const defaultIdx = catalog.models.findIndex((m) => m.id === targetModel); const safeDefaultIdx = defaultIdx >= 0 ? defaultIdx : 0; @@ -142,10 +160,14 @@ export async function configureProviderModel( // Get base env vars for defaults const baseEnv = getClaudeEnvVars(provider); - const codexSonnetModel = - provider === 'codex' && !selectedModel.match(/-(xhigh|high|medium)$/) - ? `${selectedModel}-high` - : selectedModel; + const selectedDefaultModel = normalizeCodexTierModel(provider, selectedModel, 'xhigh'); + const selectedOpusModel = normalizeCodexTierModel(provider, selectedModel, 'xhigh'); + const selectedSonnetModel = normalizeCodexTierModel(provider, selectedModel, 'high'); + const selectedHaikuModel = normalizeCodexTierModel( + provider, + baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || selectedModel, + 'medium' + ); // Read existing settings to preserve user customizations let existingSettings: Record = {}; @@ -167,10 +189,10 @@ export async function configureProviderModel( const ccsControlledEnv: Record = { ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '', ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '', - ANTHROPIC_MODEL: selectedModel, - ANTHROPIC_DEFAULT_OPUS_MODEL: selectedModel, - ANTHROPIC_DEFAULT_SONNET_MODEL: codexSonnetModel, - ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || '', + ANTHROPIC_MODEL: selectedDefaultModel, + ANTHROPIC_DEFAULT_OPUS_MODEL: selectedOpusModel, + ANTHROPIC_DEFAULT_SONNET_MODEL: selectedSonnetModel, + ANTHROPIC_DEFAULT_HAIKU_MODEL: selectedHaikuModel, }; // Merge: user env vars (preserved) + CCS controlled (override) @@ -232,13 +254,16 @@ export async function showCurrentConfig(provider: CLIProxyProvider): Promise m.id === currentModel); + const entry = catalog.models.find((m) => m.id === normalizedCurrentModel); const displayName = entry?.name || 'Unknown'; console.error( ` ${bold('Current:')} ${color(displayName, 'success')} ${dim(`(${currentModel})`)}` @@ -255,7 +280,7 @@ export async function showCurrentConfig(provider: CLIProxyProvider): Promise { - const isCurrent = m.id === currentModel; + const isCurrent = m.id === normalizedCurrentModel; console.error(formatModelDetailed(m, isCurrent)); }); diff --git a/src/cliproxy/services/variant-settings.ts b/src/cliproxy/services/variant-settings.ts index 8dc6425d..551a2f89 100644 --- a/src/cliproxy/services/variant-settings.ts +++ b/src/cliproxy/services/variant-settings.ts @@ -35,6 +35,21 @@ interface SettingsFile { [key: string]: unknown; } +const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; + +function hasCodexEffortSuffix(model: string): boolean { + return CODEX_EFFORT_SUFFIX_REGEX.test(model); +} + +function normalizeCodexTierModel( + provider: CLIProxyProfileName | undefined, + model: string, + fallbackEffort: 'medium' | 'high' | 'xhigh' +): string { + if (provider !== 'codex') return model; + return hasCodexEffortSuffix(model) ? model : `${model}-${fallbackEffort}`; +} + /** * Build settings env object for a variant */ @@ -44,26 +59,25 @@ function buildSettingsEnv( port: number = CLIPROXY_DEFAULT_PORT ): SettingsEnv { const baseEnv = getClaudeEnvVars(provider as CLIProxyProvider, port); - const codexSonnetModel = normalizeCodexSonnetModel(provider, model); + const defaultModel = normalizeCodexTierModel(provider, model, 'xhigh'); + const opusModel = normalizeCodexTierModel(provider, model, 'xhigh'); + const sonnetModel = normalizeCodexTierModel(provider, model, 'high'); + const haikuModel = normalizeCodexTierModel( + provider, + baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || model, + 'medium' + ); return { ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '', ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '', - ANTHROPIC_MODEL: model, - ANTHROPIC_DEFAULT_OPUS_MODEL: model, - ANTHROPIC_DEFAULT_SONNET_MODEL: codexSonnetModel, - ANTHROPIC_DEFAULT_HAIKU_MODEL: baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL || model, + ANTHROPIC_MODEL: defaultModel, + ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel, + ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel, + ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel, }; } -function normalizeCodexSonnetModel( - provider: CLIProxyProfileName | undefined, - model: string -): string { - if (provider !== 'codex') return model; - return /-(xhigh|high|medium)$/.test(model) ? model : `${model}-high`; -} - /** * Ensure directory exists */ @@ -299,10 +313,20 @@ export function updateSettingsModel( if (model) { settings.env = settings.env || ({} as SettingsEnv); - const codexSonnetModel = normalizeCodexSonnetModel(provider, model); - settings.env.ANTHROPIC_MODEL = model; - settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = model; - settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = codexSonnetModel; + settings.env.ANTHROPIC_MODEL = normalizeCodexTierModel(provider, model, 'xhigh'); + settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = normalizeCodexTierModel(provider, model, 'xhigh'); + settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = normalizeCodexTierModel( + provider, + model, + 'high' + ); + if (provider === 'codex' && settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { + settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = normalizeCodexTierModel( + provider, + settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, + 'medium' + ); + } } else { // Clear model settings to use defaults delete (settings.env as unknown as Record).ANTHROPIC_MODEL; diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 0e8f0065..830aa902 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -168,7 +168,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ], [ ['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'], - ['ccs codex', 'OpenAI Codex (gpt-5.3-codex)'], + ['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'], ['ccs agy', 'Antigravity (Claude/Gemini models)'], ['ccs qwen', 'Qwen Code (qwen3-coder)'], ['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'], @@ -346,11 +346,12 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['--thinking ', 'Custom token budget (512-100000)'], ['', ''], ['--effort ', 'Codex alias for reasoning effort (medium/high/xhigh)'], - ['--effort xhigh', 'Codex 5.3 full-depth reasoning'], + ['--effort xhigh', 'Pin Codex effort to xhigh for this run'], ['', ''], ['Note:', 'Extended thinking allocates compute for step-by-step reasoning'], ['', 'before responding.'], ['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'], + ['', 'Codex model suffixes also pin effort: -medium / -high / -xhigh.'], ]); // Extended Context (1M) diff --git a/tests/unit/cliproxy/variant-update-service.test.ts b/tests/unit/cliproxy/variant-update-service.test.ts index e75c80f7..38220280 100644 --- a/tests/unit/cliproxy/variant-update-service.test.ts +++ b/tests/unit/cliproxy/variant-update-service.test.ts @@ -100,7 +100,10 @@ cliproxy: }; expect(settings.env.ANTHROPIC_BASE_URL).toContain('/api/provider/codex'); - expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.1-codex-mini'); + expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.1-codex-mini-xhigh'); + expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.1-codex-mini-xhigh'); + expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.1-codex-mini-high'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-medium'); expect(settings.env.CUSTOM_FLAG).toBe('keep-me'); expect(settings.hooks.PreToolUse.length).toBe(1); @@ -119,7 +122,10 @@ cliproxy: let settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { env: Record; }; + expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-xhigh'); + expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh'); expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-medium'); const modelOnly = updateVariant('demo', { model: 'gpt-5.3-codex' }); expect(modelOnly.success).toBe(true); @@ -127,8 +133,9 @@ cliproxy: settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { env: Record; }; - expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex'); - expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex'); + expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex-xhigh'); + expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex-xhigh'); expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex-high'); + expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini-medium'); }); }); diff --git a/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx b/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx index a078dd6b..07dbfd46 100644 --- a/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx +++ b/ui/src/components/cliproxy/provider-editor/custom-preset-dialog.tsx @@ -63,6 +63,12 @@ export function CustomPresetDialog({ /> + {catalog?.provider === 'codex' && ( +

+ Codex tip: suffixes -medium, -high, and -xhigh{' '} + pin effort. Unsuffixed models use Thinking settings. +

+ )} Configure which models to use for each tier

+ {provider === 'codex' && ( +

+ Codex tip: suffixes -medium, -high, and -xhigh{' '} + pin reasoning effort. Unsuffixed models use Thinking settings. +

+ )}
m.id) || []); + const isCodexProvider = catalog?.provider === 'codex'; + const selectedCodexEffort = isCodexProvider ? getCodexEffortDisplay(value) : null; return (
@@ -273,7 +276,19 @@ export function FlexibleModelSelector({