mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat: support minimal codex effort aliases
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -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() ?? '';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user