feat: support Codex fast service-tier aliases

* feat: support Codex fast service-tier aliases

* fix: send Codex fast tier as priority
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-07 15:34:05 -04:00
1 parent a8d412a0eb
commit 74d73748ee
17 files changed
+471 -86

No files matched your search

@@ -16,6 +16,8 @@ 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-xhigh')).toBe('gpt-5.4');
expect(getFreePlanFallbackCodexModel('gpt-5.5-high-fast')).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');
expect(getFreePlanFallbackCodexModel('gpt-5.3-codex(high)')).toBe('gpt-5.4');
@@ -147,6 +147,60 @@ describe('CodexReasoningProxy extended-context compatibility', () => {
expect((capturedBody?.reasoning as JsonRecord | undefined)?.effort).toBe('high');
});
it('translates codex fast model suffixes into service_tier', 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 highFastResponse = await postJson(
`http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`,
{
model: 'gpt-5.4-high-fast',
messages: [],
}
);
const fastHighResponse = await postJson(
`http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`,
{
model: 'gpt-5.4-fast-high',
messages: [],
}
);
proxy.stop();
expect(highFastResponse.statusCode).toBe(200);
expect(fastHighResponse.statusCode).toBe(200);
expect(capturedBodies[0]?.model).toBe('gpt-5.4');
expect((capturedBodies[0]?.reasoning as JsonRecord | undefined)?.effort).toBe('high');
expect(capturedBodies[0]?.service_tier).toBe('priority');
expect(capturedBodies[1]?.model).toBe('gpt-5.4');
expect((capturedBodies[1]?.reasoning as JsonRecord | undefined)?.effort).toBe('high');
expect(capturedBodies[1]?.service_tier).toBe('priority');
});
it('skips reasoning injection when disableEffort is enabled', async () => {
let capturedBody: JsonRecord | null = null;
@@ -189,6 +243,49 @@ describe('CodexReasoningProxy extended-context compatibility', () => {
expect((capturedBody?.reasoning as JsonRecord | undefined)?.effort).toBeUndefined();
});
it('keeps fast service tier when disableEffort is enabled', async () => {
let capturedBody: JsonRecord | null = null;
const upstream = http.createServer((req, res) => {
let rawBody = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
rawBody += chunk;
});
req.on('end', () => {
capturedBody = 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',
},
disableEffort: true,
});
const proxyPort = await proxy.start();
const response = await postJson(
`http://127.0.0.1:${proxyPort}/api/provider/codex/v1/messages`,
{
model: 'gpt-5.4-high-fast',
messages: [],
}
);
proxy.stop();
expect(response.statusCode).toBe(200);
expect(capturedBody?.model).toBe('gpt-5.4');
expect((capturedBody?.reasoning as JsonRecord | undefined)?.effort).toBeUndefined();
expect(capturedBody?.service_tier).toBe('priority');
});
it('does not strip unknown model ids that merely end with "-high"', async () => {
let capturedBody: JsonRecord | null = null;
@@ -123,10 +123,17 @@ describe('model-id-normalizer', () => {
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-high')).toBe('gpt-5.4-high');
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(
'gpt-5.4-high-fast[1m]'
);
expect(normalizeModelIdForProvider('gpt-5.2-codex', 'codex')).toBe('gpt-5.2');
expect(normalizeModelIdForProvider('gpt-5.1-codex-mini', 'codex')).toBe('gpt-5.4-mini');
expect(canonicalizeModelIdForProvider('gpt-5-codex-high', 'codex')).toBe('gpt-5.4-high');
expect(canonicalizeModelIdForProvider('gpt-5-codex-fast-high', 'codex')).toBe(
'gpt-5.4-high-fast'
);
});
});
@@ -10,7 +10,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_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
const CODEX_TUNING_SUFFIX_REGEX =
/(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i;
const CODEX_PAREN_SUFFIX_REGEX = /\((xhigh|high|medium)\)$/i;
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
const KNOWN_CODEX_MODELS = new Set(
@@ -57,7 +58,7 @@ export function normalizeCodexModelId(model: string): string {
.trim()
.replace(EXTENDED_CONTEXT_SUFFIX_REGEX, '')
.replace(CODEX_PAREN_SUFFIX_REGEX, '')
.replace(CODEX_EFFORT_SUFFIX_REGEX, '')
.replace(CODEX_TUNING_SUFFIX_REGEX, '')
.trim();
return normalizeModelIdForProvider(stripped, 'codex').trim().toLowerCase();
}
@@ -9,6 +9,8 @@ import {
import { getModelMaxLevel } from '../model-catalog';
export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh';
export type CodexServiceTier = 'fast';
type CodexServiceTierRequestValue = 'priority';
export interface CodexReasoningModelMap {
opusModel?: string;
@@ -41,10 +43,15 @@ interface ForwardJsonContext {
requestedModel: string | null;
attemptedUpstreamModel: string | null;
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
retryCount: number;
}
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(xhigh|high|medium|fast)$/i;
const CODEX_SERVICE_TIER_REQUEST_VALUE: Record<CodexServiceTier, CodexServiceTierRequestValue> = {
fast: 'priority',
};
function stripExtendedContextSuffix(model: string): string {
return model.replace(EXTENDED_CONTEXT_SUFFIX_REGEX, '').trim();
@@ -58,16 +65,35 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function parseModelEffortSuffix(
model: string
): { upstreamModel: string; effort: CodexReasoningEffort } | null {
function parseModelTuningSuffix(model: string): {
upstreamModel: string;
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
} | null {
const normalizedModel = stripExtendedContextSuffix(model);
const match = normalizedModel.match(/^(.*)-(xhigh|high|medium)$/i);
if (!match) return null;
const upstreamModel = match[1]?.trim();
const effort = match[2]?.toLowerCase() as CodexReasoningEffort;
let upstreamModel = normalizedModel;
let effort: CodexReasoningEffort | null = null;
let serviceTier: CodexServiceTier | null = null;
for (let consumed = 0; consumed < 2; consumed += 1) {
const match = upstreamModel.match(CODEX_TUNING_SUFFIX_TOKEN_REGEX);
if (!match?.[1]) break;
const token = match[1].toLowerCase();
if (token === 'fast') {
if (serviceTier) break;
serviceTier = 'fast';
} else {
if (effort) break;
effort = token as CodexReasoningEffort;
}
upstreamModel = upstreamModel.slice(0, -match[0].length).trim();
}
if (!effort && !serviceTier) return null;
if (!upstreamModel) return null;
return { upstreamModel, effort };
return { upstreamModel, effort, serviceTier };
}
function isKnownCodexModelId(
@@ -154,19 +180,36 @@ export function getEffortForModel(
export function injectReasoningEffortIntoBody(
body: unknown,
effort: CodexReasoningEffort
): unknown {
return injectCodexRequestTuningIntoBody(body, { effort, serviceTier: null });
}
export function injectCodexRequestTuningIntoBody(
body: unknown,
tuning: {
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
}
): unknown {
if (!isRecord(body)) return body;
// OpenAI Responses API knob: reasoning: { effort: "..." }
// Always override effort (user expectation).
const existingReasoning = isRecord(body.reasoning) ? body.reasoning : {};
return {
...body,
reasoning: {
const tunedBody = { ...body };
if (tuning.effort) {
tunedBody.reasoning = {
...existingReasoning,
effort,
},
};
effort: tuning.effort,
};
}
if (tuning.serviceTier) {
tunedBody.service_tier = CODEX_SERVICE_TIER_REQUEST_VALUE[tuning.serviceTier];
}
return tunedBody;
}
export class CodexReasoningProxy {
@@ -191,7 +234,8 @@ export class CodexReasoningProxy {
at: string;
model: string | null;
upstreamModel: string | null;
effort: CodexReasoningEffort;
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
path: string;
}> = [];
private readonly counts: Record<CodexReasoningEffort, number> = { medium: 0, high: 0, xhigh: 0 };
@@ -226,14 +270,21 @@ export class CodexReasoningProxy {
private buildForwardBody(
body: unknown,
upstreamModel: string | null,
effort: CodexReasoningEffort | null
tuning: {
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
}
): unknown {
const withUpstreamModel =
upstreamModel && isRecord(body) ? { ...body, model: upstreamModel } : body;
if (this.config.disableEffort || !effort) {
const effort = this.config.disableEffort ? null : tuning.effort;
if (!effort && !tuning.serviceTier) {
return withUpstreamModel;
}
return injectReasoningEffortIntoBody(withUpstreamModel, effort);
return injectCodexRequestTuningIntoBody(withUpstreamModel, {
effort,
serviceTier: tuning.serviceTier,
});
}
private sendBufferedResponse(
@@ -247,14 +298,17 @@ export class CodexReasoningProxy {
}
/**
* Treat trailing "-high/-medium/-xhigh" as an effort alias only for known codex models.
* Treat trailing "-high/-medium/-xhigh" and "-fast" as Codex tuning aliases
* only for known codex models.
* Prevents stripping legitimate upstream model IDs that happen to end with those tokens.
*/
private parseEffortAlias(
model: string | null
): { upstreamModel: string; effort: CodexReasoningEffort } | null {
private parseTuningAlias(model: string | null): {
upstreamModel: string;
effort: CodexReasoningEffort | null;
serviceTier: CodexServiceTier | null;
} | null {
if (!model) return null;
const parsed = parseModelEffortSuffix(model);
const parsed = parseModelTuningSuffix(model);
if (!parsed) return null;
if (!isKnownCodexModelId(parsed.upstreamModel, this.modelEffort)) {
return null;
@@ -296,11 +350,21 @@ export class CodexReasoningProxy {
private record(
model: string | null,
upstreamModel: string | null,
effort: CodexReasoningEffort,
effort: CodexReasoningEffort | null,
serviceTier: CodexServiceTier | null,
path: string
): void {
this.counts[effort] += 1;
this.recent.push({ at: new Date().toISOString(), model, upstreamModel, effort, path });
if (effort) {
this.counts[effort] += 1;
}
this.recent.push({
at: new Date().toISOString(),
model,
upstreamModel,
effort,
serviceTier,
path,
});
if (this.recent.length > 50) this.recent.shift();
}
@@ -418,12 +482,13 @@ export class CodexReasoningProxy {
? stripExtendedContextSuffix(originalModel)
: null;
// Support "model aliases" like `gpt-5.2-codex-xhigh` by translating to:
// - upstream model: `gpt-5.2-codex`
// - reasoning.effort: `xhigh`
// Support "model aliases" like `gpt-5.4-high-fast` by translating to:
// - upstream model: `gpt-5.4`
// - reasoning.effort: `high`
// - service_tier: `priority` (Codex request value for fast mode)
//
// This allows tier→effort mapping without inventing upstream model IDs.
const suffixParsed = this.parseEffortAlias(normalizedRequestModel);
// This allows tier/speed mapping without inventing upstream model IDs.
const suffixParsed = this.parseTuningAlias(normalizedRequestModel);
const requestedUpstreamModel = suffixParsed?.upstreamModel ?? normalizedRequestModel;
const rememberedFallback = this.getRememberedFallback(requestedUpstreamModel);
const upstreamModel = rememberedFallback ?? requestedUpstreamModel;
@@ -436,14 +501,15 @@ export class CodexReasoningProxy {
: !this.config.disableEffort
? requestedEffort
: null;
const rewritten = this.buildForwardBody(parsed, upstreamModel, effort);
const serviceTier = suffixParsed?.serviceTier ?? null;
const rewritten = this.buildForwardBody(parsed, upstreamModel, { effort, serviceTier });
if (effort) {
this.record(originalModel, upstreamModel, effort, requestPath);
if (effort || serviceTier) {
this.record(originalModel, upstreamModel, effort, serviceTier, requestPath);
this.trace(
`[${new Date().toISOString()}] model=${originalModel ?? 'null'} upstreamModel=${
upstreamModel ?? 'null'
} effort=${effort} path=${requestPath}`
} effort=${effort ?? 'null'} serviceTier=${serviceTier ?? 'null'} path=${requestPath}`
);
} else {
this.log(`[disabled] model=${originalModel ?? 'null'} -> passthrough (no reasoning)`);
@@ -458,6 +524,7 @@ export class CodexReasoningProxy {
requestedModel: requestedUpstreamModel,
attemptedUpstreamModel: upstreamModel,
effort,
serviceTier,
retryCount: 0,
});
} catch (error) {
@@ -630,7 +697,10 @@ export class CodexReasoningProxy {
!this.config.disableEffort && context.effort
? capEffortAtModelMax(fallbackModel, context.effort)
: null;
const retryBody = this.buildForwardBody(body, fallbackModel, retryEffort);
const retryBody = this.buildForwardBody(body, fallbackModel, {
effort: retryEffort,
serviceTier: context.serviceTier,
});
this.log(
`Upstream rejected model "${context.attemptedUpstreamModel}". Retrying ${context.requestPath} with "${fallbackModel}".`
@@ -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_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i;
const CODEX_TUNING_SUFFIX_TOKEN_REGEX = /-(xhigh|high|medium|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',
@@ -65,15 +65,31 @@ function splitBaseModelAndSuffix(model: string): { baseModel: string; suffix: st
};
}
function splitCodexEffortSuffix(model: string): { baseModel: string; suffix: string } {
const match = model.match(CODEX_EFFORT_SUFFIX_REGEX);
if (!match?.[0]) {
return { baseModel: model, suffix: '' };
function splitCodexTuningSuffix(model: string): { baseModel: string; suffix: string } {
let baseModel = model;
let effort: string | null = null;
let serviceTier: 'fast' | null = null;
for (let consumed = 0; consumed < 2; consumed += 1) {
const match = baseModel.match(CODEX_TUNING_SUFFIX_TOKEN_REGEX);
if (!match?.[1]) break;
const token = match[1].toLowerCase();
if (token === 'fast') {
if (serviceTier) break;
serviceTier = 'fast';
} else {
if (effort) break;
effort = token;
}
baseModel = baseModel.slice(0, -match[0].length);
}
const suffix = [effort, serviceTier].filter(Boolean).join('-');
return {
baseModel: model.slice(0, -match[0].length),
suffix: match[0],
baseModel,
suffix: suffix ? `-${suffix}` : '',
};
}
@@ -109,11 +125,11 @@ export function isIFlowProvider(provider: ProviderLike): boolean {
return provider.trim().toLowerCase() === 'iflow';
}
/** Strip Codex effort suffixes while preserving trailing config suffixes. */
/** Strip Codex effort/service-tier suffixes while preserving trailing config suffixes. */
export function stripCodexEffortSuffix(model: string): string {
const trimmed = trimModelId(model);
const { baseModel, suffix } = splitBaseModelAndSuffix(trimmed);
const { baseModel: withoutEffort } = splitCodexEffortSuffix(baseModel);
const { baseModel: withoutEffort } = splitCodexTuningSuffix(baseModel);
return `${withoutEffort}${suffix}`;
}
@@ -121,12 +137,12 @@ export function stripCodexEffortSuffix(model: string): string {
export function normalizeCodexLegacyModelAliases(model: string): string {
const trimmed = trimModelId(model);
const { baseModel, suffix } = splitBaseModelAndSuffix(trimmed);
const { baseModel: baseWithoutEffort, suffix: effortSuffix } = splitCodexEffortSuffix(baseModel);
const { baseModel: baseWithoutEffort, suffix: tuningSuffix } = splitCodexTuningSuffix(baseModel);
const replacement = CODEX_LEGACY_MODEL_ALIASES[baseWithoutEffort.trim().toLowerCase()];
if (!replacement) {
return trimmed;
}
return `${replacement}${effortSuffix}${suffix}`;
return `${replacement}${tuningSuffix}${suffix}`;
}
/**
+14 -8
View File
@@ -16,9 +16,15 @@ 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_FAST_TUNING_VALUE_REGEX =
/^(?:(medium|high|xhigh)-fast|fast-(medium|high|xhigh)|fast)$/i;
const CODEX_TUNING_SUFFIX_REGEX =
/(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i;
/**
* Normalize model ID for provider capability lookup.
* Codex may carry effort suffixes in either legacy "(high)" or current "-high" forms.
* Codex may carry effort/service-tier suffixes in legacy "(high)" or current "-high-fast" forms.
*/
function normalizeModelForThinkingLookup(model: string, provider: CLIProxyProvider): string {
const withoutExtendedContext = model.replace(/\[1m\]$/i, '').trim();
@@ -26,8 +32,10 @@ function normalizeModelForThinkingLookup(model: string, provider: CLIProxyProvid
if (provider !== 'codex') return providerNormalized;
// New codex suffix form: gpt-5.3-codex-high -> gpt-5.3-codex
const codexSuffixMatch = providerNormalized.match(/^(.*)-(xhigh|high|medium)$/i);
// New codex suffix forms: gpt-5.4-high-fast, gpt-5.4-fast-high -> gpt-5.4
const codexSuffixMatch = providerNormalized.match(
/^(.*?)(?:-(?:xhigh|high|medium)(?:-fast)?|-fast(?:-(?:xhigh|high|medium))?)$/i
);
if (codexSuffixMatch?.[1]) {
return codexSuffixMatch[1].trim();
}
@@ -82,8 +90,6 @@ function applyThinkingSuffixForProvider(
thinkingValue: string | number,
provider?: CLIProxyProvider
): string {
const codexEffortRegex = /^(medium|high|xhigh)$/i;
const codexModelSuffixRegex = /-(medium|high|xhigh)$/i;
const parenthesizedSuffixMatch = model.match(/\(([^)]+)\)$/);
// Existing parenthesized suffix:
@@ -93,7 +99,7 @@ function applyThinkingSuffixForProvider(
if (parenthesizedSuffixMatch) {
if (provider === 'codex') {
const normalizedParensValue = parenthesizedSuffixMatch[1]?.trim().toLowerCase() || '';
if (codexEffortRegex.test(normalizedParensValue)) {
if (CODEX_EFFORT_REGEX.test(normalizedParensValue)) {
return model.replace(/\([^)]+\)$/, `-${normalizedParensValue}`);
}
}
@@ -110,11 +116,11 @@ function applyThinkingSuffixForProvider(
}
if (provider === 'codex') {
if (codexModelSuffixRegex.test(model)) {
if (CODEX_TUNING_SUFFIX_REGEX.test(model)) {
return model;
}
const normalized = String(thinkingValue).trim().toLowerCase();
if (codexEffortRegex.test(normalized)) {
if (CODEX_EFFORT_REGEX.test(normalized) || CODEX_FAST_TUNING_VALUE_REGEX.test(normalized)) {
return `${model}-${normalized}`;
}
}
+4
View File
@@ -66,6 +66,8 @@ export interface ModelEntry {
extendedContext?: boolean;
/** Whether model can read image inputs natively without the Image transformer */
nativeImageInput?: boolean;
/** Additional Codex service-tier suffixes supported by this model. */
codexServiceTiers?: Array<'fast'>;
}
/**
@@ -200,6 +202,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
maxLevel: 'xhigh',
dynamicAllowed: false,
},
codexServiceTiers: ['fast'],
},
{
id: 'gpt-5.4',
@@ -211,6 +214,7 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
maxLevel: 'xhigh',
dynamicAllowed: false,
},
codexServiceTiers: ['fast'],
},
{
id: 'gpt-5.4-mini',
+70 -12
View File
@@ -130,6 +130,64 @@ function formatModelOption(model: ModelEntry): string {
return `${model.name}${tierBadge}`;
}
const CODEX_EFFORTS_IN_ORDER = ['medium', 'high', 'xhigh'] as const;
type CodexSelectableEffort = (typeof CODEX_EFFORTS_IN_ORDER)[number];
function getCodexSelectableEfforts(model: ModelEntry): CodexSelectableEffort[] {
const maxLevel = model.thinking?.maxLevel;
const maxIndex = CODEX_EFFORTS_IN_ORDER.findIndex((effort) => effort === maxLevel);
if (maxIndex < 0) return [];
return CODEX_EFFORTS_IN_ORDER.slice(0, maxIndex + 1);
}
function getCodexModelOptionValues(model: ModelEntry, modelId: string): string[] {
const serviceTiers = model.codexServiceTiers ?? [];
const optionValues: string[] = [modelId];
for (const serviceTier of serviceTiers) {
optionValues.push(`${modelId}-${serviceTier}`);
}
for (const effort of getCodexSelectableEfforts(model)) {
const effortModelId = `${modelId}-${effort}`;
optionValues.push(effortModelId);
for (const serviceTier of serviceTiers) {
optionValues.push(`${effortModelId}-${serviceTier}`);
}
}
return optionValues;
}
function getModelOptionsForProvider(
provider: CLIProxyProvider,
catalog: NonNullable<ReturnType<typeof getProviderCatalog>>,
routing: CliproxyProviderRoutingHints | undefined
): Array<{ id: string; label: string }> {
return catalog.models.flatMap((model) => {
const modelId = getSelectableModelId(model.id, routing);
const optionValues =
provider === 'codex' ? getCodexModelOptionValues(model, modelId) : [modelId];
return optionValues.map((optionValue) => ({
id: optionValue,
label:
provider === 'codex'
? `${optionValue} (${formatModelOption(model)})`
: formatModelOption(model),
}));
});
}
function getDefaultModelOptionIndex(
modelOptions: Array<{ id: string }>,
catalog: NonNullable<ReturnType<typeof getProviderCatalog>>,
routing: CliproxyProviderRoutingHints | undefined
): number {
const defaultModelId = getSelectableModelId(catalog.defaultModel, routing);
const defaultIdx = modelOptions.findIndex((option) => option.id === defaultModelId);
return defaultIdx >= 0 ? defaultIdx : 0;
}
function getSelectableModelId(
modelId: string,
routing: CliproxyProviderRoutingHints | undefined
@@ -207,13 +265,13 @@ async function selectTierConfig(
const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider];
const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) {
const modelOptions = catalog.models.map((m) => ({
id: getSelectableModelId(m.id, routing),
label: formatModelOption(m),
}));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
const modelOptions = getModelOptionsForProvider(
provider as CLIProxyProvider,
catalog,
routing
);
model = await InteractivePrompt.selectFromList(`Model for ${tierName}:`, modelOptions, {
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
defaultIndex: getDefaultModelOptionIndex(modelOptions, catalog, routing),
});
}
}
@@ -468,13 +526,13 @@ export async function handleCreate(
const routing = (await getCatalogRoutingSnapshot()).routing[provider as CLIProxyProvider];
const catalog = getProviderCatalog(provider as CLIProxyProvider);
if (catalog) {
const modelOptions = catalog.models.map((m) => ({
id: getSelectableModelId(m.id, routing),
label: formatModelOption(m),
}));
const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel);
const modelOptions = getModelOptionsForProvider(
provider as CLIProxyProvider,
catalog,
routing
);
model = await InteractivePrompt.selectFromList('Select model:', modelOptions, {
defaultIndex: defaultIdx >= 0 ? defaultIdx : 0,
defaultIndex: getDefaultModelOptionIndex(modelOptions, catalog, routing),
});
}
}