mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-08 22:24:09 +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
|
### 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-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-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.
|
- **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', () => {
|
it('maps paid-only free-plan models to safe fallbacks', () => {
|
||||||
expect(getFreePlanFallbackCodexModel('gpt-5.5')).toBe('gpt-5.4');
|
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-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-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.5-fast-high')).toBe('gpt-5.4');
|
||||||
expect(getFreePlanFallbackCodexModel('gpt-5.3-codex')).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');
|
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');
|
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 () => {
|
it('skips reasoning injection when disableEffort is enabled', async () => {
|
||||||
let capturedBody: JsonRecord | null = null;
|
let capturedBody: JsonRecord | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,10 @@ describe('model-id-normalizer', () => {
|
|||||||
it('normalizes legacy codex aliases to the current supported model IDs', () => {
|
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')).toBe('gpt-5.4');
|
||||||
expect(normalizeCodexLegacyModelAliases('gpt-5-codex-mini[1m]')).toBe('gpt-5.4-mini[1m]');
|
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-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-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[1m]')).toBe('gpt-5.4-high[1m]');
|
||||||
expect(normalizeCodexLegacyModelAliases('gpt-5-codex-high-fast[1m]')).toBe(
|
expect(normalizeCodexLegacyModelAliases('gpt-5-codex-high-fast[1m]')).toBe(
|
||||||
@@ -138,6 +141,16 @@ describe('model-id-normalizer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('parses codex model tuning suffixes', () => {
|
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({
|
expect(parseCodexModelTuningAlias('gpt-5.5-high')).toEqual({
|
||||||
baseModel: 'gpt-5.5',
|
baseModel: 'gpt-5.5',
|
||||||
effort: 'high',
|
effort: 'high',
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export type CodexPlanType = CodexQuotaResult['planType'];
|
|||||||
const FREE_SAFE_DEFAULT_MODEL = 'gpt-5.4';
|
const FREE_SAFE_DEFAULT_MODEL = 'gpt-5.4';
|
||||||
const FREE_SAFE_FAST_MODEL = 'gpt-5.4-mini';
|
const FREE_SAFE_FAST_MODEL = 'gpt-5.4-mini';
|
||||||
const CODEX_TUNING_SUFFIX_REGEX =
|
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;
|
||||||
const CODEX_PAREN_SUFFIX_REGEX = /\((xhigh|high|medium)\)$/i;
|
const CODEX_PAREN_SUFFIX_REGEX = /\((minimal|low|medium|high|xhigh)\)$/i;
|
||||||
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
||||||
const KNOWN_CODEX_MODELS = new Set(
|
const KNOWN_CODEX_MODELS = new Set(
|
||||||
(getProviderCatalog('codex')?.models ?? []).map((model) => model.id.toLowerCase())
|
(getProviderCatalog('codex')?.models ?? []).map((model) => model.id.toLowerCase())
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from './codex-plan-compatibility';
|
} from './codex-plan-compatibility';
|
||||||
import { getModelMaxLevel } from '../model-catalog';
|
import { getModelMaxLevel } from '../model-catalog';
|
||||||
|
|
||||||
export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh';
|
export type CodexReasoningEffort = 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||||
export type CodexServiceTier = 'fast';
|
export type CodexServiceTier = 'fast';
|
||||||
type CodexServiceTierRequestValue = 'priority';
|
type CodexServiceTierRequestValue = 'priority';
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ interface ForwardJsonContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
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> = {
|
const CODEX_SERVICE_TIER_REQUEST_VALUE: Record<CodexServiceTier, CodexServiceTierRequestValue> = {
|
||||||
fast: 'priority',
|
fast: 'priority',
|
||||||
};
|
};
|
||||||
@@ -108,13 +108,15 @@ function isKnownCodexModelId(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EFFORT_RANK: Record<CodexReasoningEffort, number> = {
|
const EFFORT_RANK: Record<CodexReasoningEffort, number> = {
|
||||||
medium: 1,
|
minimal: 1,
|
||||||
high: 2,
|
low: 2,
|
||||||
xhigh: 3,
|
medium: 3,
|
||||||
|
high: 4,
|
||||||
|
xhigh: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** All valid codex effort levels in rank order */
|
/** 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 {
|
function minEffort(a: CodexReasoningEffort, b: CodexReasoningEffort): CodexReasoningEffort {
|
||||||
return EFFORT_RANK[a] <= EFFORT_RANK[b] ? a : b;
|
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);
|
const maxLevel = getModelMaxLevel('codex', model);
|
||||||
if (!maxLevel) return effort;
|
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);
|
const maxEffort = EFFORT_BY_RANK.find((e) => e === maxLevel);
|
||||||
if (!maxEffort) return effort;
|
if (!maxEffort) return effort;
|
||||||
|
|
||||||
@@ -238,7 +240,13 @@ export class CodexReasoningProxy {
|
|||||||
serviceTier: CodexServiceTier | null;
|
serviceTier: CodexServiceTier | null;
|
||||||
path: string;
|
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) {
|
constructor(config: CodexReasoningProxyConfig) {
|
||||||
this.config = {
|
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.
|
* only for known codex models.
|
||||||
* Prevents stripping legitimate upstream model IDs that happen to end with those tokens.
|
* 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 =
|
const DENIED_ANTIGRAVITY_SONNET_45_REGEX =
|
||||||
/claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi;
|
/claude-sonnet-4(?:[.-])5(?:-thinking)?(?=(?:$|[^a-z0-9]))/gi;
|
||||||
const CANONICAL_ANTIGRAVITY_OPUS_46_MODEL = 'claude-opus-4-6-thinking';
|
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({
|
const CODEX_LEGACY_MODEL_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||||
'gpt-5-codex': 'gpt-5.4',
|
'gpt-5-codex': 'gpt-5.4',
|
||||||
'gpt-5-codex-mini': 'gpt-5.4-mini',
|
'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',
|
'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 type CodexModelServiceTier = 'fast';
|
||||||
|
|
||||||
export interface CodexModelTuningAlias {
|
export interface CodexModelTuningAlias {
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import { getThinkingConfig } from '../../config/config-loader-facade';
|
|||||||
/** Model tier types for thinking budget defaults */
|
/** Model tier types for thinking budget defaults */
|
||||||
export type ModelTier = 'opus' | 'sonnet' | 'haiku';
|
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 =
|
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 =
|
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.
|
* Normalize model ID for provider capability lookup.
|
||||||
@@ -32,16 +32,16 @@ function normalizeModelForThinkingLookup(model: string, provider: CLIProxyProvid
|
|||||||
|
|
||||||
if (provider !== 'codex') return providerNormalized;
|
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(
|
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]) {
|
if (codexSuffixMatch?.[1]) {
|
||||||
return codexSuffixMatch[1].trim();
|
return codexSuffixMatch[1].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy codex suffix form: gpt-5.3-codex(high) -> gpt-5.3-codex
|
// 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]) {
|
if (codexLegacyMatch?.[1]) {
|
||||||
return codexLegacyMatch[1].trim();
|
return codexLegacyMatch[1].trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const defaultCliproxyImageAnalysisDeps: CliproxyImageAnalysisDeps = {
|
|||||||
getLocalRuntimeApiKey: getEffectiveApiKey,
|
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;
|
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
|
||||||
|
|
||||||
function normalizeCodexModelForDirectUpstream(model: string): string {
|
function normalizeCodexModelForDirectUpstream(model: string): string {
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function formatModelOption(model: ModelEntry): string {
|
|||||||
return `${model.name}${tierBadge}`;
|
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];
|
type CodexSelectableEffort = (typeof CODEX_EFFORTS_IN_ORDER)[number];
|
||||||
|
|
||||||
function getCodexSelectableEfforts(model: ModelEntry): CodexSelectableEffort[] {
|
function getCodexSelectableEfforts(model: ModelEntry): CodexSelectableEffort[] {
|
||||||
|
|||||||
@@ -218,6 +218,18 @@ supports_websockets = false
|
|||||||
const rawText = fs.readFileSync(configPath, 'utf8');
|
const rawText = fs.readFileSync(configPath, 'utf8');
|
||||||
expect(rawText).toContain('model = "gpt-5.5"');
|
expect(rawText).toContain('model = "gpt-5.5"');
|
||||||
expect(rawText).toContain('model_reasoning_effort = "xhigh"');
|
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]');
|
expect(rawText).toContain('[model_providers.cliproxy]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -819,6 +819,32 @@ process.exit(0);
|
|||||||
expect(codexConfig).not.toContain('gpt-5.5-high-fast');
|
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', () => {
|
it('loads the configured cliproxy provider env_key for ccsxp launches', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
@@ -228,9 +228,10 @@ export function ModelConfigSection({
|
|||||||
) : null}
|
) : null}
|
||||||
{provider === 'codex' && (
|
{provider === 'codex' && (
|
||||||
<p className="text-[11px] text-muted-foreground mb-3 rounded-md border bg-muted/30 px-2.5 py-2">
|
<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>{' '}
|
Codex tip: suffixes <code>-minimal</code>, <code>-low</code>, <code>-medium</code>,{' '}
|
||||||
pin reasoning effort. <code>-fast</code> enables the Codex fast service tier on models
|
<code>-high</code>, and <code>-xhigh</code> pin reasoning effort. <code>-fast</code>{' '}
|
||||||
that support it, including combined forms such as <code>gpt-5.4-high-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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { CodexEffort, CodexServiceTier } from '@/lib/codex-effort';
|
|||||||
import {
|
import {
|
||||||
getCodexEffortDisplay,
|
getCodexEffortDisplay,
|
||||||
getCodexEffortVariants,
|
getCodexEffortVariants,
|
||||||
|
parseCodexEffort,
|
||||||
parseCodexServiceTier,
|
parseCodexServiceTier,
|
||||||
} from '@/lib/codex-effort';
|
} from '@/lib/codex-effort';
|
||||||
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
|
import { getResolvedCatalogModels, getSupplementalCatalogModels } from '@/lib/model-catalogs';
|
||||||
@@ -344,6 +345,13 @@ function getModelOptionValues(
|
|||||||
: [optionValue];
|
: [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({
|
export function FlexibleModelSelector({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
@@ -400,7 +408,7 @@ export function FlexibleModelSelector({
|
|||||||
|
|
||||||
return optionValues.map((optionValue) => ({
|
return optionValues.map((optionValue) => ({
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
groupKey: 'recommended',
|
groupKey: getRecommendedGroupKey(optionValue, isCodexProvider),
|
||||||
searchText: `${optionValue} ${model.id} ${model.name} ${routingHint?.recommendedModelId ?? ''}`,
|
searchText: `${optionValue} ${model.id} ${model.name} ${routingHint?.recommendedModelId ?? ''}`,
|
||||||
keywords: [model.tier ?? '', catalog?.provider ?? ''],
|
keywords: [model.tier ?? '', catalog?.provider ?? ''],
|
||||||
triggerContent: (
|
triggerContent: (
|
||||||
@@ -555,6 +563,26 @@ export function FlexibleModelSelector({
|
|||||||
<span className="text-xs text-primary">{t('providerModelSelector.recommended')}</span>
|
<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
|
...(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';
|
export type CodexServiceTier = 'fast';
|
||||||
|
|
||||||
const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(medium|high|xhigh|fast)$/i;
|
const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(minimal|low|medium|high|xhigh|fast)$/i;
|
||||||
const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = ['medium', 'high', 'xhigh'];
|
const CODEX_EFFORTS_IN_ORDER: readonly CodexEffort[] = [
|
||||||
|
'minimal',
|
||||||
|
'low',
|
||||||
|
'medium',
|
||||||
|
'high',
|
||||||
|
'xhigh',
|
||||||
|
];
|
||||||
|
|
||||||
function trimModelId(modelId: string | undefined): string {
|
function trimModelId(modelId: string | undefined): string {
|
||||||
return modelId?.trim() ?? '';
|
return modelId?.trim() ?? '';
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ const resources = {
|
|||||||
pinnedRouteStatus: 'Pinned route status:',
|
pinnedRouteStatus: 'Pinned route status:',
|
||||||
pinnedModelNotAdvertised:
|
pinnedModelNotAdvertised:
|
||||||
'Pinned model is not currently advertised by the proxy: {{model}}',
|
'Pinned model is not currently advertised by the proxy: {{model}}',
|
||||||
|
codexReasoningVariants: 'Reasoning variants',
|
||||||
|
codexFastVariants: 'Fast variants',
|
||||||
},
|
},
|
||||||
createAuthProfileDialog: {
|
createAuthProfileDialog: {
|
||||||
title: 'Create New Account',
|
title: 'Create New Account',
|
||||||
@@ -2929,6 +2931,8 @@ const resources = {
|
|||||||
preferredPinnedModel: '偏好的固定模型:',
|
preferredPinnedModel: '偏好的固定模型:',
|
||||||
pinnedRouteStatus: '固定路由状态:',
|
pinnedRouteStatus: '固定路由状态:',
|
||||||
pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}',
|
pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}',
|
||||||
|
codexReasoningVariants: '推理变体',
|
||||||
|
codexFastVariants: '快速变体',
|
||||||
},
|
},
|
||||||
createAuthProfileDialog: {
|
createAuthProfileDialog: {
|
||||||
title: '创建新账号',
|
title: '创建新账号',
|
||||||
@@ -5517,6 +5521,8 @@ const resources = {
|
|||||||
preferredPinnedModel: 'Mô hình ghim ưu tiên:',
|
preferredPinnedModel: 'Mô hình ghim ưu tiên:',
|
||||||
pinnedRouteStatus: 'Trạng thái tuyến ghim:',
|
pinnedRouteStatus: 'Trạng thái tuyến ghim:',
|
||||||
pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}',
|
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: {
|
createAuthProfileDialog: {
|
||||||
title: 'Tạo tài khoản mới',
|
title: 'Tạo tài khoản mới',
|
||||||
@@ -8220,6 +8226,8 @@ const resources = {
|
|||||||
preferredPinnedModel: '優先ピンモデル:',
|
preferredPinnedModel: '優先ピンモデル:',
|
||||||
pinnedRouteStatus: 'ピンルートの状態:',
|
pinnedRouteStatus: 'ピンルートの状態:',
|
||||||
pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}',
|
pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}',
|
||||||
|
codexReasoningVariants: '推論バリアント',
|
||||||
|
codexFastVariants: '高速バリアント',
|
||||||
},
|
},
|
||||||
createAuthProfileDialog: {
|
createAuthProfileDialog: {
|
||||||
title: '新しいアカウントを作成',
|
title: '新しいアカウントを作成',
|
||||||
@@ -10946,6 +10954,8 @@ const resources = {
|
|||||||
preferredPinnedModel: '선호하는 고정 모델:',
|
preferredPinnedModel: '선호하는 고정 모델:',
|
||||||
pinnedRouteStatus: '고정된 라우트 상태:',
|
pinnedRouteStatus: '고정된 라우트 상태:',
|
||||||
pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}',
|
pinnedModelNotAdvertised: '고정된 모델이 현재 프록시에서 알리지 않고 있습니다: {{model}}',
|
||||||
|
codexReasoningVariants: '추론 변형',
|
||||||
|
codexFastVariants: '빠른 변형',
|
||||||
},
|
},
|
||||||
createAuthProfileDialog: {
|
createAuthProfileDialog: {
|
||||||
title: '새 계정 생성',
|
title: '새 계정 생성',
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ describe('FlexibleModelSelector', () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: /select model/i }));
|
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-high')).toBeInTheDocument();
|
||||||
expect(screen.getByText('gpt-5.3-codex-xhigh')).toBeInTheDocument();
|
expect(screen.getByText('gpt-5.3-codex-xhigh')).toBeInTheDocument();
|
||||||
expect(screen.getByText('gpt-5.4-high-fast')).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.4-fast-high')).toBe('high');
|
||||||
expect(parseCodexEffort('gpt-5.3-codex-xhigh')).toBe('xhigh');
|
expect(parseCodexEffort('gpt-5.3-codex-xhigh')).toBe('xhigh');
|
||||||
expect(parseCodexEffort('gpt-5-mini-medium')).toBe('medium');
|
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', () => {
|
it('parses mixed-case suffixes', () => {
|
||||||
@@ -23,7 +25,6 @@ describe('parseCodexEffort', () => {
|
|||||||
|
|
||||||
it('returns undefined for unsuffixed or unsupported values', () => {
|
it('returns undefined for unsuffixed or unsupported values', () => {
|
||||||
expect(parseCodexEffort('gpt-5.3-codex')).toBeUndefined();
|
expect(parseCodexEffort('gpt-5.3-codex')).toBeUndefined();
|
||||||
expect(parseCodexEffort('gpt-5.3-codex-low')).toBeUndefined();
|
|
||||||
expect(parseCodexEffort('gpt-5.4-fast')).toBeUndefined();
|
expect(parseCodexEffort('gpt-5.4-fast')).toBeUndefined();
|
||||||
expect(parseCodexEffort(undefined)).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', () => {
|
it('builds ordered codex effort variants up to the supported max level', () => {
|
||||||
expect(getCodexEffortVariants('gpt-5.3-codex', 'xhigh')).toEqual([
|
expect(getCodexEffortVariants('gpt-5.3-codex', 'xhigh')).toEqual([
|
||||||
'gpt-5.3-codex',
|
'gpt-5.3-codex',
|
||||||
|
'gpt-5.3-codex-minimal',
|
||||||
|
'gpt-5.3-codex-low',
|
||||||
'gpt-5.3-codex-medium',
|
'gpt-5.3-codex-medium',
|
||||||
'gpt-5.3-codex-high',
|
'gpt-5.3-codex-high',
|
||||||
'gpt-5.3-codex-xhigh',
|
'gpt-5.3-codex-xhigh',
|
||||||
]);
|
]);
|
||||||
expect(getCodexEffortVariants('gpt-5.4-mini', 'high')).toEqual([
|
expect(getCodexEffortVariants('gpt-5.4-mini', 'high')).toEqual([
|
||||||
'gpt-5.4-mini',
|
'gpt-5.4-mini',
|
||||||
|
'gpt-5.4-mini-minimal',
|
||||||
|
'gpt-5.4-mini-low',
|
||||||
'gpt-5.4-mini-medium',
|
'gpt-5.4-mini-medium',
|
||||||
'gpt-5.4-mini-high',
|
'gpt-5.4-mini-high',
|
||||||
]);
|
]);
|
||||||
@@ -84,6 +89,10 @@ describe('codex effort helpers', () => {
|
|||||||
expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([
|
expect(getCodexEffortVariants('gpt-5.4', 'high', ['fast'])).toEqual([
|
||||||
'gpt-5.4',
|
'gpt-5.4',
|
||||||
'gpt-5.4-fast',
|
'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',
|
||||||
'gpt-5.4-medium-fast',
|
'gpt-5.4-medium-fast',
|
||||||
'gpt-5.4-high',
|
'gpt-5.4-high',
|
||||||
|
|||||||
Reference in New Issue
Block a user