From 6569eed15b99f85cf32db15d7a6fbd57b001b326 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 20 May 2026 11:11:46 -0400 Subject: [PATCH] feat: support minimal codex effort aliases --- docs/project-roadmap.md | 1 + .../codex-plan-compatibility.test.ts | 4 ++ ...x-reasoning-proxy-extended-context.test.ts | 53 +++++++++++++++++++ .../__tests__/model-id-normalizer.test.ts | 13 +++++ .../ai-providers/codex-plan-compatibility.ts | 4 +- .../ai-providers/codex-reasoning-proxy.ts | 26 +++++---- .../ai-providers/model-id-normalizer.ts | 4 +- src/cliproxy/config/thinking-config.ts | 12 ++--- src/cliproxy/executor/env-resolver.ts | 2 +- src/commands/cliproxy/variant-subcommand.ts | 2 +- .../codex-cliproxy-provider-config.test.ts | 12 +++++ .../targets/codex-runtime-integration.test.ts | 26 +++++++++ .../provider-editor/model-config-section.tsx | 7 +-- .../cliproxy/provider-model-selector.tsx | 30 ++++++++++- ui/src/lib/codex-effort.ts | 12 +++-- ui/src/lib/i18n.ts | 10 ++++ .../cliproxy/provider-model-selector.test.tsx | 4 ++ ui/tests/unit/ui/lib/codex-effort.test.ts | 11 +++- 18 files changed, 204 insertions(+), 29 deletions(-) diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index c2885952..f7753478 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic ### Recent Fixes +- **2026-05-20**: **#1285** Codex model picker aliases now include `minimal` and `low` reasoning effort suffixes alongside `medium`, `high`, and `xhigh`, and the dashboard separates base recommendations, reasoning variants, and fast variants so low-effort selections such as `gpt-5.5-low` are visible. - **2026-05-19**: **#1252** Claude extension persistence now rejects Codex CLIProxy profiles instead of writing `ANTHROPIC_BASE_URL=/api/provider/codex` into Claude Code settings. Native Codex subscription use stays on `ccsxp` or `ccs codex --target codex`, and `ccs doctor` warns when stale Claude settings still point at the Codex translator. - **2026-05-18**: **#1287** `ccsx resume` now passes through to the native Codex `resume` subcommand before CCS profile detection, preserving Codex session continuation while keeping `ccsx auth` on the managed Codex profile namespace. - **2026-05-09**: **#1199** Existing Claude auth accounts now have dashboard-visible Shared Resources controls separate from History Sync. Accounts exposes a dedicated Resources action for `shared` vs `profile-local`, `/shared` now inventories commands, skills, agents, plugins, and `settings.json`, plugin directories without docs show factual directory contents, and shared settings content is inspectable read-only through the localhost-gated shared-content API. diff --git a/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts b/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts index 793a6623..28201039 100644 --- a/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts +++ b/src/cliproxy/ai-providers/__tests__/codex-plan-compatibility.test.ts @@ -15,8 +15,12 @@ describe('codex plan compatibility', () => { it('maps paid-only free-plan models to safe fallbacks', () => { expect(getFreePlanFallbackCodexModel('gpt-5.5')).toBe('gpt-5.4'); + expect(getFreePlanFallbackCodexModel('gpt-5.5-minimal')).toBe('gpt-5.4'); + expect(getFreePlanFallbackCodexModel('gpt-5.5-low')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.5-xhigh')).toBe('gpt-5.4'); + expect(getFreePlanFallbackCodexModel('gpt-5.5-low-fast')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.5-high-fast')).toBe('gpt-5.4'); + expect(getFreePlanFallbackCodexModel('gpt-5.5-fast-minimal')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.5-fast-high')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.3-codex')).toBe('gpt-5.4'); expect(getFreePlanFallbackCodexModel('gpt-5.3-codex-xhigh')).toBe('gpt-5.4'); diff --git a/src/cliproxy/ai-providers/__tests__/codex-reasoning-proxy-extended-context.test.ts b/src/cliproxy/ai-providers/__tests__/codex-reasoning-proxy-extended-context.test.ts index 6fd5be8f..9b156985 100644 --- a/src/cliproxy/ai-providers/__tests__/codex-reasoning-proxy-extended-context.test.ts +++ b/src/cliproxy/ai-providers/__tests__/codex-reasoning-proxy-extended-context.test.ts @@ -201,6 +201,59 @@ describe('CodexReasoningProxy extended-context compatibility', () => { expect(capturedBodies[1]?.service_tier).toBe('priority'); }); + it('translates minimal and low codex effort suffixes before forwarding upstream', async () => { + const capturedBodies: JsonRecord[] = []; + + const upstream = http.createServer((req, res) => { + let rawBody = ''; + req.setEncoding('utf8'); + req.on('data', (chunk) => { + rawBody += chunk; + }); + req.on('end', () => { + capturedBodies.push(rawBody ? (JSON.parse(rawBody) as JsonRecord) : {}); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + }); + }); + cleanupServers.push(upstream); + + const upstreamPort = await listenOnRandomPort(upstream); + const proxy = new CodexReasoningProxy({ + upstreamBaseUrl: `http://127.0.0.1:${upstreamPort}`, + modelMap: { + defaultModel: 'gpt-5.4', + }, + defaultEffort: 'medium', + }); + + const proxyPort = await proxy.start(); + const minimalResponse = await postJson( + `http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`, + { + model: 'gpt-5.5-minimal', + messages: [], + } + ); + const lowFastResponse = await postJson( + `http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`, + { + model: 'gpt-5.5-fast-low', + messages: [], + } + ); + + proxy.stop(); + + expect(minimalResponse.statusCode).toBe(200); + expect(lowFastResponse.statusCode).toBe(200); + expect(capturedBodies[0]?.model).toBe('gpt-5.5'); + expect((capturedBodies[0]?.reasoning as JsonRecord | undefined)?.effort).toBe('minimal'); + expect(capturedBodies[1]?.model).toBe('gpt-5.5'); + expect((capturedBodies[1]?.reasoning as JsonRecord | undefined)?.effort).toBe('low'); + expect(capturedBodies[1]?.service_tier).toBe('priority'); + }); + it('skips reasoning injection when disableEffort is enabled', async () => { let capturedBody: JsonRecord | null = null; diff --git a/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts b/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts index 5e963b3d..d9fafeae 100644 --- a/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts +++ b/src/cliproxy/ai-providers/__tests__/model-id-normalizer.test.ts @@ -123,7 +123,10 @@ describe('model-id-normalizer', () => { it('normalizes legacy codex aliases to the current supported model IDs', () => { expect(normalizeCodexLegacyModelAliases('gpt-5-codex')).toBe('gpt-5.4'); expect(normalizeCodexLegacyModelAliases('gpt-5-codex-mini[1m]')).toBe('gpt-5.4-mini[1m]'); + expect(normalizeCodexLegacyModelAliases('gpt-5-codex-minimal')).toBe('gpt-5.4-minimal'); + expect(normalizeCodexLegacyModelAliases('gpt-5-codex-low')).toBe('gpt-5.4-low'); expect(normalizeCodexLegacyModelAliases('gpt-5-codex-high')).toBe('gpt-5.4-high'); + expect(normalizeCodexLegacyModelAliases('gpt-5-codex-fast-low')).toBe('gpt-5.4-low-fast'); expect(normalizeCodexLegacyModelAliases('gpt-5-codex-fast-high')).toBe('gpt-5.4-high-fast'); expect(normalizeCodexLegacyModelAliases('gpt-5-codex-high[1m]')).toBe('gpt-5.4-high[1m]'); expect(normalizeCodexLegacyModelAliases('gpt-5-codex-high-fast[1m]')).toBe( @@ -138,6 +141,16 @@ describe('model-id-normalizer', () => { }); it('parses codex model tuning suffixes', () => { + expect(parseCodexModelTuningAlias('gpt-5.5-minimal')).toEqual({ + baseModel: 'gpt-5.5', + effort: 'minimal', + serviceTier: null, + }); + expect(parseCodexModelTuningAlias('gpt-5.5-low-fast')).toEqual({ + baseModel: 'gpt-5.5', + effort: 'low', + serviceTier: 'fast', + }); expect(parseCodexModelTuningAlias('gpt-5.5-high')).toEqual({ baseModel: 'gpt-5.5', effort: 'high', diff --git a/src/cliproxy/ai-providers/codex-plan-compatibility.ts b/src/cliproxy/ai-providers/codex-plan-compatibility.ts index 7a9a0ce1..291b396e 100644 --- a/src/cliproxy/ai-providers/codex-plan-compatibility.ts +++ b/src/cliproxy/ai-providers/codex-plan-compatibility.ts @@ -11,8 +11,8 @@ export type CodexPlanType = CodexQuotaResult['planType']; const FREE_SAFE_DEFAULT_MODEL = 'gpt-5.4'; const FREE_SAFE_FAST_MODEL = 'gpt-5.4-mini'; const CODEX_TUNING_SUFFIX_REGEX = - /(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i; -const CODEX_PAREN_SUFFIX_REGEX = /\((xhigh|high|medium)\)$/i; + /(?:-(?:minimal|low|medium|high|xhigh)(?:-fast)?|-fast(?:-(?:minimal|low|medium|high|xhigh))?)$/i; +const CODEX_PAREN_SUFFIX_REGEX = /\((minimal|low|medium|high|xhigh)\)$/i; const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i; const KNOWN_CODEX_MODELS = new Set( (getProviderCatalog('codex')?.models ?? []).map((model) => model.id.toLowerCase()) diff --git a/src/cliproxy/ai-providers/codex-reasoning-proxy.ts b/src/cliproxy/ai-providers/codex-reasoning-proxy.ts index c63852ea..a8502a82 100644 --- a/src/cliproxy/ai-providers/codex-reasoning-proxy.ts +++ b/src/cliproxy/ai-providers/codex-reasoning-proxy.ts @@ -8,7 +8,7 @@ import { } from './codex-plan-compatibility'; import { getModelMaxLevel } from '../model-catalog'; -export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh'; +export type CodexReasoningEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; export type CodexServiceTier = 'fast'; type CodexServiceTierRequestValue = 'priority'; @@ -48,7 +48,7 @@ interface ForwardJsonContext { } const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i; -const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(xhigh|high|medium|fast)$/i; +const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i; const CODEX_SERVICE_TIER_REQUEST_VALUE: Record = { fast: 'priority', }; @@ -108,13 +108,15 @@ function isKnownCodexModelId( } const EFFORT_RANK: Record = { - medium: 1, - high: 2, - xhigh: 3, + minimal: 1, + low: 2, + medium: 3, + high: 4, + xhigh: 5, }; /** All valid codex effort levels in rank order */ -const EFFORT_BY_RANK: CodexReasoningEffort[] = ['medium', 'high', 'xhigh']; +const EFFORT_BY_RANK: CodexReasoningEffort[] = ['minimal', 'low', 'medium', 'high', 'xhigh']; function minEffort(a: CodexReasoningEffort, b: CodexReasoningEffort): CodexReasoningEffort { return EFFORT_RANK[a] <= EFFORT_RANK[b] ? a : b; @@ -128,7 +130,7 @@ function capEffortAtModelMax(model: string, effort: CodexReasoningEffort): Codex const maxLevel = getModelMaxLevel('codex', model); if (!maxLevel) return effort; - // Map maxLevel to CodexReasoningEffort (only medium/high/xhigh are valid) + // Map maxLevel to CodexReasoningEffort. const maxEffort = EFFORT_BY_RANK.find((e) => e === maxLevel); if (!maxEffort) return effort; @@ -238,7 +240,13 @@ export class CodexReasoningProxy { serviceTier: CodexServiceTier | null; path: string; }> = []; - private readonly counts: Record = { medium: 0, high: 0, xhigh: 0 }; + private readonly counts: Record = { + minimal: 0, + low: 0, + medium: 0, + high: 0, + xhigh: 0, + }; constructor(config: CodexReasoningProxyConfig) { this.config = { @@ -298,7 +306,7 @@ export class CodexReasoningProxy { } /** - * Treat trailing "-high/-medium/-xhigh" and "-fast" as Codex tuning aliases + * Treat trailing effort tokens and "-fast" as Codex tuning aliases * only for known codex models. * Prevents stripping legitimate upstream model IDs that happen to end with those tokens. */ diff --git a/src/cliproxy/ai-providers/model-id-normalizer.ts b/src/cliproxy/ai-providers/model-id-normalizer.ts index 7cee2d38..7f860b32 100644 --- a/src/cliproxy/ai-providers/model-id-normalizer.ts +++ b/src/cliproxy/ai-providers/model-id-normalizer.ts @@ -26,7 +26,7 @@ const DENIED_ANTIGRAVITY_OPUS_45_REGEX = const DENIED_ANTIGRAVITY_SONNET_45_REGEX = /claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi; const CANONICAL_ANTIGRAVITY_OPUS_46_MODEL = 'claude-opus-4-6-thinking'; -const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(xhigh|high|medium|fast)$/i; +const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i; const CODEX_LEGACY_MODEL_ALIASES: Readonly> = Object.freeze({ 'gpt-5-codex': 'gpt-5.4', 'gpt-5-codex-mini': 'gpt-5.4-mini', @@ -45,7 +45,7 @@ const IFLOW_LEGACY_MODEL_ALIASES: Readonly> = Object.free 'minimax-m2.5': 'qwen3-coder-plus', }); -export type CodexModelTuningEffort = 'medium' | 'high' | 'xhigh'; +export type CodexModelTuningEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; export type CodexModelServiceTier = 'fast'; export interface CodexModelTuningAlias { diff --git a/src/cliproxy/config/thinking-config.ts b/src/cliproxy/config/thinking-config.ts index 8f5e043f..da871e40 100644 --- a/src/cliproxy/config/thinking-config.ts +++ b/src/cliproxy/config/thinking-config.ts @@ -16,11 +16,11 @@ import { getThinkingConfig } from '../../config/config-loader-facade'; /** Model tier types for thinking budget defaults */ export type ModelTier = 'opus' | 'sonnet' | 'haiku'; -const CODEX_EFFORT_REGEX = /^(medium|high|xhigh)$/i; +const CODEX_EFFORT_REGEX = /^(minimal|low|medium|high|xhigh)$/i; const CODEX_FAST_TUNING_VALUE_REGEX = - /^(?:(medium|high|xhigh)-fast|fast-(medium|high|xhigh)|fast)$/i; + /^(?:(minimal|low|medium|high|xhigh)-fast|fast-(minimal|low|medium|high|xhigh)|fast)$/i; const CODEX_TUNING_SUFFIX_REGEX = - /(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i; + /(?:-(?:minimal|low|medium|high|xhigh)(?:-fast)?|-fast(?:-(?:minimal|low|medium|high|xhigh))?)$/i; /** * Normalize model ID for provider capability lookup. @@ -32,16 +32,16 @@ function normalizeModelForThinkingLookup(model: string, provider: CLIProxyProvid if (provider !== 'codex') return providerNormalized; - // New codex suffix forms: gpt-5.4-high-fast, gpt-5.4-fast-high -> gpt-5.4 + // New codex suffix forms: gpt-5.4-low-fast, gpt-5.4-fast-high -> gpt-5.4 const codexSuffixMatch = providerNormalized.match( - /^(.*?)(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i + /^(.*?)(?:-(?:minimal|low|medium|high|xhigh)(?:-fast)?|-fast(?:-(?:minimal|low|medium|high|xhigh))?)$/i ); if (codexSuffixMatch?.[1]) { return codexSuffixMatch[1].trim(); } // Legacy codex suffix form: gpt-5.3-codex(high) -> gpt-5.3-codex - const codexLegacyMatch = providerNormalized.match(/^(.*)\((xhigh|high|medium)\)$/i); + const codexLegacyMatch = providerNormalized.match(/^(.*)\((minimal|low|medium|high|xhigh)\)$/i); if (codexLegacyMatch?.[1]) { return codexLegacyMatch[1].trim(); } diff --git a/src/cliproxy/executor/env-resolver.ts b/src/cliproxy/executor/env-resolver.ts index 3e186d29..ccce224b 100644 --- a/src/cliproxy/executor/env-resolver.ts +++ b/src/cliproxy/executor/env-resolver.ts @@ -112,7 +112,7 @@ const defaultCliproxyImageAnalysisDeps: CliproxyImageAnalysisDeps = { getLocalRuntimeApiKey: getEffectiveApiKey, }; -const CODEX_EFFORT_SUFFIX_REGEX = /^(.*)-(xhigh|high|medium)$/i; +const CODEX_EFFORT_SUFFIX_REGEX = /^(.*)-(minimal|low|medium|high|xhigh)$/i; const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i; function normalizeCodexModelForDirectUpstream(model: string): string { diff --git a/src/commands/cliproxy/variant-subcommand.ts b/src/commands/cliproxy/variant-subcommand.ts index 1a538e14..2f9fdb11 100644 --- a/src/commands/cliproxy/variant-subcommand.ts +++ b/src/commands/cliproxy/variant-subcommand.ts @@ -130,7 +130,7 @@ function formatModelOption(model: ModelEntry): string { return `${model.name}${tierBadge}`; } -const CODEX_EFFORTS_IN_ORDER = ['medium', 'high', 'xhigh'] as const; +const CODEX_EFFORTS_IN_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh'] as const; type CodexSelectableEffort = (typeof CODEX_EFFORTS_IN_ORDER)[number]; function getCodexSelectableEfforts(model: ModelEntry): CodexSelectableEffort[] { diff --git a/tests/unit/targets/codex-cliproxy-provider-config.test.ts b/tests/unit/targets/codex-cliproxy-provider-config.test.ts index 08dabe2e..8edf4edd 100644 --- a/tests/unit/targets/codex-cliproxy-provider-config.test.ts +++ b/tests/unit/targets/codex-cliproxy-provider-config.test.ts @@ -218,6 +218,18 @@ supports_websockets = false const rawText = fs.readFileSync(configPath, 'utf8'); expect(rawText).toContain('model = "gpt-5.5"'); expect(rawText).toContain('model_reasoning_effort = "xhigh"'); + }); + + it('normalizes a native Codex minimal effort alias when adding the missing provider', async () => { + fs.mkdirSync(codexHome, { recursive: true }); + fs.writeFileSync(configPath, 'model = "gpt-5.5-minimal"\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 = "minimal"'); expect(rawText).toContain('[model_providers.cliproxy]'); }); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 51d4f226..4a116127 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -819,6 +819,32 @@ process.exit(0); expect(codexConfig).not.toContain('gpt-5.5-high-fast'); }); + it('normalizes ccsxp native low 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-low-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); + const codexConfig = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8'); + expect(codexConfig).toContain('model = "gpt-5.5"'); + expect(codexConfig).toContain('model_reasoning_effort = "low"'); + expect(codexConfig).toContain('service_tier = "priority"'); + expect(codexConfig).not.toContain('gpt-5.5-low-fast'); + }); + it('loads the configured cliproxy provider env_key for ccsxp launches', () => { if (process.platform === 'win32') return; diff --git a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx index a0db2ea9..52eb5aea 100644 --- a/ui/src/components/cliproxy/provider-editor/model-config-section.tsx +++ b/ui/src/components/cliproxy/provider-editor/model-config-section.tsx @@ -228,9 +228,10 @@ export function ModelConfigSection({ ) : null} {provider === 'codex' && (

- Codex tip: suffixes -medium, -high, and -xhigh{' '} - pin reasoning effort. -fast enables the Codex fast service tier on models - that support it, including combined forms such as gpt-5.4-high-fast. + Codex tip: suffixes -minimal, -low, -medium,{' '} + -high, and -xhigh pin reasoning effort. -fast{' '} + enables the Codex fast service tier on models that support it, including combined forms + such as gpt-5.4-high-fast.

)}
diff --git a/ui/src/components/cliproxy/provider-model-selector.tsx b/ui/src/components/cliproxy/provider-model-selector.tsx index 3af5c431..350ed4d1 100644 --- a/ui/src/components/cliproxy/provider-model-selector.tsx +++ b/ui/src/components/cliproxy/provider-model-selector.tsx @@ -16,6 +16,7 @@ import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort'; import { getCodexEffortDisplay, getCodexEffortVariants, + parseCodexEffort, parseCodexServiceTier, } from '@/lib/codex-effort'; import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs'; @@ -344,6 +345,13 @@ function getModelOptionValues( : [optionValue]; } +function getRecommendedGroupKey(optionValue: string, isCodexProvider: boolean): string { + if (!isCodexProvider) return 'recommended'; + if (parseCodexServiceTier(optionValue)) return 'codex-fast'; + if (parseCodexEffort(optionValue)) return 'codex-reasoning'; + return 'recommended'; +} + export function FlexibleModelSelector({ label, description, @@ -400,7 +408,7 @@ export function FlexibleModelSelector({ return optionValues.map((optionValue) => ({ value: optionValue, - groupKey: 'recommended', + groupKey: getRecommendedGroupKey(optionValue, isCodexProvider), searchText: `${optionValue} ${model.id} ${model.name} ${routingHint?.recommendedModelId ?? ''}`, keywords: [model.tier ?? '', catalog?.provider ?? ''], triggerContent: ( @@ -555,6 +563,26 @@ export function FlexibleModelSelector({ {t('providerModelSelector.recommended')} ), }, + ...(isCodexProvider + ? [ + { + key: 'codex-reasoning', + label: ( + + {t('providerModelSelector.codexReasoningVariants')} + + ), + }, + { + key: 'codex-fast', + label: ( + + {t('providerModelSelector.codexFastVariants')} + + ), + }, + ] + : []), ...(allModelOptions.length > 0 ? [ { diff --git a/ui/src/lib/codex-effort.ts b/ui/src/lib/codex-effort.ts index 95581103..7da7355b 100644 --- a/ui/src/lib/codex-effort.ts +++ b/ui/src/lib/codex-effort.ts @@ -1,8 +1,14 @@ -export type CodexEffort = 'medium' | 'high' | 'xhigh'; +export type CodexEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'; export type CodexServiceTier = 'fast'; -const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(medium|high|xhigh|fast)$/i; -const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = ['medium', 'high', 'xhigh']; +const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i; +const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = [ + 'minimal', + 'low', + 'medium', + 'high', + 'xhigh', +]; function trimModelId(modelId: string | undefined): string { return modelId?.trim() ?? ''; diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 05d5009b..431281ee 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -225,6 +225,8 @@ const resources = { pinnedRouteStatus: 'Pinned route status:', pinnedModelNotAdvertised: 'Pinned model is not currently advertised by the proxy: {{model}}', + codexReasoningVariants: 'Reasoning variants', + codexFastVariants: 'Fast variants', }, createAuthProfileDialog: { title: 'Create New Account', @@ -2929,6 +2931,8 @@ const resources = { preferredPinnedModel: '偏好的固定模型:', pinnedRouteStatus: '固定路由状态:', pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}', + codexReasoningVariants: '推理变体', + codexFastVariants: '快速变体', }, createAuthProfileDialog: { title: '创建新账号', @@ -5517,6 +5521,8 @@ const resources = { preferredPinnedModel: 'Mô hình ghim ưu tiên:', pinnedRouteStatus: 'Trạng thái tuyến ghim:', pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}', + codexReasoningVariants: 'Biến thể lý luận', + codexFastVariants: 'Biến thể nhanh', }, createAuthProfileDialog: { title: 'Tạo tài khoản mới', @@ -8220,6 +8226,8 @@ const resources = { preferredPinnedModel: '優先ピンモデル:', pinnedRouteStatus: 'ピンルートの状態:', pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}', + codexReasoningVariants: '推論バリアント', + codexFastVariants: '高速バリアント', }, createAuthProfileDialog: { title: '新しいアカウントを作成', @@ -10946,6 +10954,8 @@ const resources = { preferredPinnedModel: '선호하는 고정 모델:', pinnedRouteStatus: '고정된 라우트 상태:', pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}', + codexReasoningVariants: '추론 변형', + codexFastVariants: '빠른 변형', }, createAuthProfileDialog: { title: '새 계정 생성', diff --git a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx index d7ba0fb7..fd2e3bd0 100644 --- a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx @@ -121,6 +121,10 @@ describe('FlexibleModelSelector', () => { await userEvent.click(screen.getByRole('button', { name: /select model/i })); + expect(screen.getByText('Reasoning variants')).toBeInTheDocument(); + expect(screen.getByText('Fast variants')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.5-minimal')).toBeInTheDocument(); + expect(screen.getByText('gpt-5.5-low')).toBeInTheDocument(); expect(screen.getByText('gpt-5.3-codex-high')).toBeInTheDocument(); expect(screen.getByText('gpt-5.3-codex-xhigh')).toBeInTheDocument(); expect(screen.getByText('gpt-5.4-high-fast')).toBeInTheDocument(); diff --git a/ui/tests/unit/ui/lib/codex-effort.test.ts b/ui/tests/unit/ui/lib/codex-effort.test.ts index ef0fc3d8..557da198 100644 --- a/ui/tests/unit/ui/lib/codex-effort.test.ts +++ b/ui/tests/unit/ui/lib/codex-effort.test.ts @@ -15,6 +15,8 @@ describe('parseCodexEffort', () => { expect(parseCodexEffort('gpt-5.4-fast-high')).toBe('high'); expect(parseCodexEffort('gpt-5.3-codex-xhigh')).toBe('xhigh'); expect(parseCodexEffort('gpt-5-mini-medium')).toBe('medium'); + expect(parseCodexEffort('gpt-5.5-low')).toBe('low'); + expect(parseCodexEffort('gpt-5.5-minimal')).toBe('minimal'); }); it('parses mixed-case suffixes', () => { @@ -23,7 +25,6 @@ describe('parseCodexEffort', () => { 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('gpt-5.4-fast')).toBeUndefined(); expect(parseCodexEffort(undefined)).toBeUndefined(); }); @@ -69,12 +70,16 @@ describe('codex effort helpers', () => { it('builds ordered codex effort variants up to the supported max level', () => { expect(getCodexEffortVariants('gpt-5.3-codex', 'xhigh')).toEqual([ 'gpt-5.3-codex', + 'gpt-5.3-codex-minimal', + 'gpt-5.3-codex-low', 'gpt-5.3-codex-medium', 'gpt-5.3-codex-high', 'gpt-5.3-codex-xhigh', ]); expect(getCodexEffortVariants('gpt-5.4-mini', 'high')).toEqual([ 'gpt-5.4-mini', + 'gpt-5.4-mini-minimal', + 'gpt-5.4-mini-low', 'gpt-5.4-mini-medium', 'gpt-5.4-mini-high', ]); @@ -84,6 +89,10 @@ describe('codex effort helpers', () => { expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([ 'gpt-5.4', 'gpt-5.4-fast', + 'gpt-5.4-minimal', + 'gpt-5.4-minimal-fast', + 'gpt-5.4-low', + 'gpt-5.4-low-fast', 'gpt-5.4-medium', 'gpt-5.4-medium-fast', 'gpt-5.4-high',