mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +00:00
feat: support minimal codex effort aliases
This commit is contained in:
1 parent
f0ad68d446
commit
6569eed15b
18 files changed
+204
-29
No files matched your search
@@ -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[] {
|
||||
|
||||
Reference in new issue
Block a user