feat: support minimal codex effort aliases

This commit is contained in:
Tam Nhu Tran
2026-05-20 11:11:46 -04:00
parent f0ad68d446
commit 6569eed15b
18 changed files with 204 additions and 29 deletions
+1
View File
@@ -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.
@@ -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');
@@ -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;
@@ -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',
@@ -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())
@@ -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<CodexServiceTier, CodexServiceTierRequestValue> = {
fast: 'priority',
};
@@ -108,13 +108,15 @@ function isKnownCodexModelId(
}
const EFFORT_RANK: Record<CodexReasoningEffort, number> = {
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<CodexReasoningEffort, number> = { medium: 0, high: 0, xhigh: 0 };
private readonly counts: Record<CodexReasoningEffort, number> = {
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.
*/
@@ -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<Record<string, string>> = 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<Record<string, string>> = 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 {
+6 -6
View File
@@ -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();
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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[] {
@@ -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]');
});
@@ -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;
@@ -228,9 +228,10 @@ export function ModelConfigSection({
) : null}
{provider === 'codex' && (
<p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2">
Codex tip: suffixes <code>-medium</code>, <code>-high</code>, and <code>-xhigh</code>{' '}
pin reasoning effort. <code>-fast</code> enables the Codex fast service tier on models
that support it, including combined forms such as <code>gpt-5.4-high-fast</code>.
Codex tip: suffixes <code>-minimal</code>, <code>-low</code>, <code>-medium</code>,{' '}
<code>-high</code>, and <code>-xhigh</code> pin reasoning effort. <code>-fast</code>{' '}
enables the Codex fast service tier on models that support it, including combined forms
such as <code>gpt-5.4-high-fast</code>.
</p>
)}
<div className="space-y-4">
@@ -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({
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
),
},
...(isCodexProvider
? [
{
key: 'codex-reasoning',
label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.codexReasoningVariants')}
</span>
),
},
{
key: 'codex-fast',
label: (
<span className="text-xs text-amber-600">
{t('providerModelSelector.codexFastVariants')}
</span>
),
},
]
: []),
...(allModelOptions.length > 0
? [
{
+9 -3
View File
@@ -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() ?? '';
+10
View File
@@ -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: '새 계정 생성',
@@ -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();
+10 -1
View File
@@ -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',