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({
diff --git a/ui/tests/unit/ui/lib/codex-effort.test.ts b/ui/tests/unit/ui/lib/codex-effort.test.ts
new file mode 100644
index 00000000..965d1963
--- /dev/null
+++ b/ui/tests/unit/ui/lib/codex-effort.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest';
+import { getCodexEffortDisplay, parseCodexEffort } from '@/lib/codex-effort';
+
+describe('parseCodexEffort', () => {
+ it('parses lowercase suffixes', () => {
+ expect(parseCodexEffort('gpt-5.3-codex-high')).toBe('high');
+ expect(parseCodexEffort('gpt-5.3-codex-xhigh')).toBe('xhigh');
+ expect(parseCodexEffort('gpt-5-mini-medium')).toBe('medium');
+ });
+
+ it('parses mixed-case suffixes', () => {
+ expect(parseCodexEffort('gpt-5.3-codex-XHIGH')).toBe('xhigh');
+ });
+
+ it('returns undefined for unsuffixed or unsupported values', () => {
+ expect(parseCodexEffort('gpt-5.3-codex')).toBeUndefined();
+ expect(parseCodexEffort('gpt-5.3-codex-low')).toBeUndefined();
+ expect(parseCodexEffort(undefined)).toBeUndefined();
+ });
+});
+
+describe('getCodexEffortDisplay', () => {
+ it('returns pinned label for suffixed models', () => {
+ expect(getCodexEffortDisplay('gpt-5.3-codex-high')).toEqual({
+ label: 'Pinned high',
+ explicit: true,
+ });
+ });
+
+ it('returns auto label for unsuffixed models', () => {
+ expect(getCodexEffortDisplay('gpt-5.3-codex')).toEqual({
+ label: 'Auto effort',
+ explicit: false,
+ });
+ });
+
+ it('returns null for empty input', () => {
+ expect(getCodexEffortDisplay(undefined)).toBeNull();
+ });
+});