mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
feat(cliproxy): clarify codex effort mapping and preset UX
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
{
|
||||
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<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
{
|
||||
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'],
|
||||
|
||||
@@ -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<string, unknown> = {};
|
||||
@@ -167,10 +189,10 @@ export async function configureProviderModel(
|
||||
const ccsControlledEnv: Record<string, string> = {
|
||||
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<voi
|
||||
|
||||
const currentModel = getCurrentModel(provider);
|
||||
const settingsPath = getProviderSettingsPath(provider);
|
||||
const normalizedCurrentModel = currentModel
|
||||
? stripCodexEffortSuffix(currentModel, provider)
|
||||
: undefined;
|
||||
|
||||
console.error('');
|
||||
console.error(header(`${catalog.displayName} Model Configuration`));
|
||||
console.error('');
|
||||
|
||||
if (currentModel) {
|
||||
const entry = catalog.models.find((m) => 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<voi
|
||||
console.error(dim(' [DEPRECATED] = Not recommended for use'));
|
||||
console.error('');
|
||||
catalog.models.forEach((m) => {
|
||||
const isCurrent = m.id === currentModel;
|
||||
const isCurrent = m.id === normalizedCurrentModel;
|
||||
console.error(formatModelDetailed(m, isCurrent));
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string, string>).ANTHROPIC_MODEL;
|
||||
|
||||
@@ -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 <number>', 'Custom token budget (512-100000)'],
|
||||
['', ''],
|
||||
['--effort <level>', '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)
|
||||
|
||||
@@ -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<string, string>;
|
||||
};
|
||||
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<string, string>;
|
||||
};
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,12 @@ export function CustomPresetDialog({
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
{catalog?.provider === 'codex' && (
|
||||
<p className="text-[11px] text-muted-foreground 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 effort. Unsuffixed models use Thinking settings.
|
||||
</p>
|
||||
)}
|
||||
<FlexibleModelSelector
|
||||
label="Default Model"
|
||||
description="Used when no specific tier is requested"
|
||||
|
||||
@@ -132,6 +132,12 @@ export function ModelConfigSection({
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Configure which models to use for each tier
|
||||
</p>
|
||||
{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. Unsuffixed models use Thinking settings.
|
||||
</p>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<FlexibleModelSelector
|
||||
label="Default Model"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { AlertTriangle, AlertCircle, Check } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getCodexEffortDisplay } from '@/lib/codex-effort';
|
||||
|
||||
/** Model entry from catalog */
|
||||
export interface ModelEntry {
|
||||
@@ -263,6 +264,8 @@ export function FlexibleModelSelector({
|
||||
}: FlexibleModelSelectorProps) {
|
||||
// Combine catalog models (recommended) with all available models
|
||||
const catalogModelIds = new Set(catalog?.models.map((m) => m.id) || []);
|
||||
const isCodexProvider = catalog?.provider === 'codex';
|
||||
const selectedCodexEffort = isCodexProvider ? getCodexEffortDisplay(value) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
@@ -273,7 +276,19 @@ export function FlexibleModelSelector({
|
||||
<Select value={value || ''} onValueChange={onChange} disabled={disabled}>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="Select model">
|
||||
{value && <span className="truncate font-mono text-xs">{value}</span>}
|
||||
{value && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{value}</span>
|
||||
{selectedCodexEffort && (
|
||||
<Badge
|
||||
variant={selectedCodexEffort.explicit ? 'secondary' : 'outline'}
|
||||
className="text-[9px] h-4 px-1 uppercase"
|
||||
>
|
||||
{selectedCodexEffort.label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
@@ -281,19 +296,30 @@ export function FlexibleModelSelector({
|
||||
{catalog && catalog.models.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel className="text-xs text-primary">Recommended</SelectLabel>
|
||||
{catalog.models.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
{model.tier === 'paid' && (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
PAID
|
||||
</Badge>
|
||||
)}
|
||||
{value === model.id && <Check className="w-3 h-3 text-primary ml-auto" />}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{catalog.models.map((model) => {
|
||||
const codexEffort = isCodexProvider ? getCodexEffortDisplay(model.id) : null;
|
||||
return (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
{model.tier === 'paid' && (
|
||||
<Badge variant="outline" className="text-[9px] h-4 px-1">
|
||||
PAID
|
||||
</Badge>
|
||||
)}
|
||||
{codexEffort && (
|
||||
<Badge
|
||||
variant={codexEffort.explicit ? 'secondary' : 'outline'}
|
||||
className="text-[9px] h-4 px-1 uppercase"
|
||||
>
|
||||
{codexEffort.label}
|
||||
</Badge>
|
||||
)}
|
||||
{value === model.id && <Check className="w-3 h-3 text-primary ml-auto" />}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
@@ -305,14 +331,25 @@ export function FlexibleModelSelector({
|
||||
</SelectLabel>
|
||||
{allModels
|
||||
.filter((m) => !catalogModelIds.has(m.id))
|
||||
.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
{value === model.id && <Check className="w-3 h-3 text-primary ml-auto" />}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
.map((model) => {
|
||||
const codexEffort = isCodexProvider ? getCodexEffortDisplay(model.id) : null;
|
||||
return (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-mono text-xs">{model.id}</span>
|
||||
{codexEffort && (
|
||||
<Badge
|
||||
variant={codexEffort.explicit ? 'secondary' : 'outline'}
|
||||
className="text-[9px] h-4 px-1 uppercase"
|
||||
>
|
||||
{codexEffort.label}
|
||||
</Badge>
|
||||
)}
|
||||
{value === model.id && <Check className="w-3 h-3 text-primary ml-auto" />}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export type CodexEffort = 'medium' | 'high' | 'xhigh';
|
||||
|
||||
const CODEX_EFFORT_SUFFIX_REGEX = /-(medium|high|xhigh)$/i;
|
||||
|
||||
export function parseCodexEffort(modelId: string | undefined): CodexEffort | undefined {
|
||||
if (!modelId) return undefined;
|
||||
const match = modelId.trim().match(CODEX_EFFORT_SUFFIX_REGEX);
|
||||
if (!match?.[1]) return undefined;
|
||||
return match[1].toLowerCase() as CodexEffort;
|
||||
}
|
||||
|
||||
export function getCodexEffortDisplay(
|
||||
modelId: string | undefined
|
||||
): { label: string; explicit: boolean } | null {
|
||||
if (!modelId) return null;
|
||||
const effort = parseCodexEffort(modelId);
|
||||
if (effort) {
|
||||
return { label: `Pinned ${effort}`, explicit: true };
|
||||
}
|
||||
return { label: 'Auto effort', explicit: false };
|
||||
}
|
||||
@@ -138,12 +138,12 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
{
|
||||
id: 'gpt-5.3-codex',
|
||||
name: 'GPT-5.3 Codex',
|
||||
description: 'Full reasoning support (xhigh)',
|
||||
description: 'Supports up to xhigh effort',
|
||||
presetMapping: {
|
||||
default: 'gpt-5.3-codex',
|
||||
opus: 'gpt-5.3-codex',
|
||||
default: 'gpt-5.3-codex-xhigh',
|
||||
opus: 'gpt-5.3-codex-xhigh',
|
||||
sonnet: 'gpt-5.3-codex-high',
|
||||
haiku: 'gpt-5-mini',
|
||||
haiku: 'gpt-5-mini-medium',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -151,21 +151,21 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
name: 'GPT-5.2 Codex',
|
||||
description: 'Previous stable Codex model',
|
||||
presetMapping: {
|
||||
default: 'gpt-5.2-codex',
|
||||
opus: 'gpt-5.2-codex',
|
||||
default: 'gpt-5.2-codex-xhigh',
|
||||
opus: 'gpt-5.2-codex-xhigh',
|
||||
sonnet: 'gpt-5.2-codex-high',
|
||||
haiku: 'gpt-5-mini',
|
||||
haiku: 'gpt-5-mini-medium',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gpt-5-mini',
|
||||
name: 'GPT-5 Mini',
|
||||
description: 'Fast, capped at high reasoning (no xhigh)',
|
||||
description: 'Fast, capped at high effort (no xhigh)',
|
||||
presetMapping: {
|
||||
default: 'gpt-5-mini',
|
||||
opus: 'gpt-5.3-codex',
|
||||
sonnet: 'gpt-5-mini',
|
||||
haiku: 'gpt-5-mini',
|
||||
default: 'gpt-5-mini-medium',
|
||||
opus: 'gpt-5.3-codex-xhigh',
|
||||
sonnet: 'gpt-5-mini-high',
|
||||
haiku: 'gpt-5-mini-medium',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -115,7 +115,12 @@ export default function ThinkingSection() {
|
||||
Thinking budget: <strong>agy</strong>, <strong>gemini</strong> (token-based)
|
||||
</li>
|
||||
<li>
|
||||
Reasoning effort: <strong>codex</strong> (level-based: medium/high/xhigh)
|
||||
Reasoning effort: <strong>codex</strong> (suffix or <code>--effort</code>:
|
||||
medium/high/xhigh)
|
||||
</li>
|
||||
<li>
|
||||
Codex suffixes pin effort (for example <code>-high</code>); unsuffixed models use
|
||||
Thinking mode.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user