feat(cliproxy): add model-specific reasoning effort caps

Add maxLevel field to ThinkingSupport interface to cap reasoning
effort at model's maximum supported level. This fixes Issue #344
where gpt-5-mini fails with xhigh reasoning (only supports high).

- Add Codex provider to model catalog with gpt-5.2-codex and gpt-5-mini
- Add capLevelAtMax() and capEffortAtModelMax() for runtime capping
- Update UI presets with new Codex models and tier mappings
This commit is contained in:
kaitranntt
2026-01-21 14:27:46 -05:00
parent 2f584802bd
commit eec44d54e2
4 changed files with 152 additions and 19 deletions
+26 -1
View File
@@ -1,6 +1,7 @@
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import { URL } from 'url'; import { URL } from 'url';
import { getModelMaxLevel } from './model-catalog';
export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh'; export type CodexReasoningEffort = 'medium' | 'high' | 'xhigh';
@@ -51,10 +52,32 @@ const EFFORT_RANK: Record<CodexReasoningEffort, number> = {
xhigh: 3, xhigh: 3,
}; };
/** All valid codex effort levels in rank order */
const EFFORT_BY_RANK: CodexReasoningEffort[] = ['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;
} }
/**
* Cap effort at model's max level from catalog.
* Returns the capped effort (or original if no cap applies).
*/
function capEffortAtModelMax(model: string, effort: CodexReasoningEffort): CodexReasoningEffort {
const maxLevel = getModelMaxLevel('codex', model);
if (!maxLevel) return effort;
// Map maxLevel to CodexReasoningEffort (only medium/high/xhigh are valid)
const maxEffort = EFFORT_BY_RANK.find((e) => e === maxLevel);
if (!maxEffort) return effort;
// Cap if effort exceeds max
if (EFFORT_RANK[effort] > EFFORT_RANK[maxEffort]) {
return maxEffort;
}
return effort;
}
export function buildCodexModelEffortMap( export function buildCodexModelEffortMap(
models: CodexReasoningModelMap, models: CodexReasoningModelMap,
defaultEffort: CodexReasoningEffort = 'medium' defaultEffort: CodexReasoningEffort = 'medium'
@@ -85,7 +108,9 @@ export function getEffortForModel(
defaultEffort: CodexReasoningEffort defaultEffort: CodexReasoningEffort
): CodexReasoningEffort { ): CodexReasoningEffort {
if (!model) return defaultEffort; if (!model) return defaultEffort;
return modelEffort.get(model) ?? defaultEffort; const effort = modelEffort.get(model) ?? defaultEffort;
// Apply model-specific cap from catalog
return capEffortAtModelMax(model, effort);
} }
export function injectReasoningEffortIntoBody( export function injectReasoningEffortIntoBody(
+43
View File
@@ -20,6 +20,8 @@ export interface ThinkingSupport {
max?: number; max?: number;
/** Valid level names (for levels type) */ /** Valid level names (for levels type) */
levels?: string[]; levels?: string[];
/** Maximum reasoning effort level (caps effort at this level for levels type) */
maxLevel?: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
/** Whether zero/disabled thinking is allowed */ /** Whether zero/disabled thinking is allowed */
zeroAllowed?: boolean; zeroAllowed?: boolean;
/** Whether dynamic/auto thinking is allowed */ /** Whether dynamic/auto thinking is allowed */
@@ -135,6 +137,35 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
}, },
], ],
}, },
codex: {
provider: 'codex',
displayName: 'Copilot Codex',
defaultModel: 'gpt-5.2-codex',
models: [
{
id: 'gpt-5.2-codex',
name: 'GPT-5.2 Codex',
description: 'Full reasoning support (xhigh)',
thinking: {
type: 'levels',
levels: ['medium', 'high', 'xhigh'],
maxLevel: 'xhigh',
dynamicAllowed: false,
},
},
{
id: 'gpt-5-mini',
name: 'GPT-5 Mini',
description: 'Capped at high reasoning (no xhigh)',
thinking: {
type: 'levels',
levels: ['medium', 'high'],
maxLevel: 'high',
dynamicAllowed: false,
},
},
],
},
}; };
/** /**
@@ -208,6 +239,18 @@ export function getModelThinkingSupport(
return model?.thinking; return model?.thinking;
} }
/**
* Get the maximum reasoning effort level for a model.
* Returns undefined if model has no cap or is not in catalog.
*/
export function getModelMaxLevel(
provider: CLIProxyProvider,
modelId: string
): ThinkingSupport['maxLevel'] | undefined {
const thinking = getModelThinkingSupport(provider, modelId);
return thinking?.maxLevel;
}
/** /**
* Check if model supports thinking/reasoning * Check if model supports thinking/reasoning
*/ */
+59 -16
View File
@@ -40,12 +40,40 @@ export const THINKING_LEVEL_BUDGETS: Record<string, number> = {
xhigh: 32768, xhigh: 32768,
}; };
/**
* Level rank for comparison (higher = more intensive)
*/
export const THINKING_LEVEL_RANK: Record<string, number> = {
minimal: 1,
low: 2,
medium: 3,
high: 4,
xhigh: 5,
};
/** /**
* Valid thinking level names * Valid thinking level names
*/ */
export const VALID_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'auto'] as const; export const VALID_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'auto'] as const;
export type ThinkingLevel = (typeof VALID_THINKING_LEVELS)[number]; export type ThinkingLevel = (typeof VALID_THINKING_LEVELS)[number];
/**
* Cap a level at the model's maximum supported level.
* Returns the capped level and whether capping occurred.
*/
export function capLevelAtMax(
level: string,
maxLevel: string | undefined
): { level: string; capped: boolean } {
if (!maxLevel) return { level, capped: false };
const levelRank = THINKING_LEVEL_RANK[level] ?? 0;
const maxRank = THINKING_LEVEL_RANK[maxLevel] ?? 5;
if (levelRank > maxRank) {
return { level: maxLevel, capped: true };
}
return { level, capped: false };
}
/** /**
* Special thinking values * Special thinking values
*/ */
@@ -263,6 +291,24 @@ function validateLevelThinking(
modelId: string modelId: string
): ThinkingValidationResult { ): ThinkingValidationResult {
const validLevels = thinking.levels ?? []; const validLevels = thinking.levels ?? [];
const maxLevel = thinking.maxLevel;
// Helper to apply maxLevel cap and build result
const applyMaxCap = (level: string, baseWarning?: string): ThinkingValidationResult => {
const { level: cappedLevel, capped } = capLevelAtMax(level, maxLevel);
const warnings: string[] = [];
if (baseWarning) warnings.push(baseWarning);
if (capped) {
warnings.push(
`Level "${level}" exceeds max "${maxLevel}" for ${modelId}. Capped to "${cappedLevel}".`
);
}
return {
valid: true,
value: cappedLevel,
warning: warnings.length > 0 ? warnings.join(' ') : undefined,
};
};
// If numeric, try to map to closest level by budget // If numeric, try to map to closest level by budget
if (typeof value === 'number') { if (typeof value === 'number') {
@@ -279,11 +325,10 @@ function validateLevelThinking(
} }
} }
return { return applyMaxCap(
valid: true, closestLevel,
value: closestLevel, `Model ${modelId} uses named levels. Mapped budget ${value} to "${closestLevel}".`
warning: `Model ${modelId} uses named levels. Mapped budget ${value} to "${closestLevel}".`, );
};
} }
// String level // String level
@@ -291,17 +336,16 @@ function validateLevelThinking(
// Check if it's a valid level for this model // Check if it's a valid level for this model
if (validLevels.includes(normalizedLevel)) { if (validLevels.includes(normalizedLevel)) {
return { valid: true, value: normalizedLevel }; return applyMaxCap(normalizedLevel);
} }
// Try to find closest match // Try to find closest match
const closest = findClosestLevel(normalizedLevel, validLevels); const closest = findClosestLevel(normalizedLevel, validLevels);
if (closest) { if (closest) {
return { return applyMaxCap(
valid: true, closest,
value: closest, `Level "${value}" not valid for ${modelId}. Mapped to "${closest}".`
warning: `Level "${value}" not valid for ${modelId}. Mapped to "${closest}".`, );
};
} }
// Try to map from standard level names to model's levels // Try to map from standard level names to model's levels
@@ -321,11 +365,10 @@ function validateLevelThinking(
const mapped = standardToModelLevel[normalizedLevel]; const mapped = standardToModelLevel[normalizedLevel];
if (mapped) { if (mapped) {
return { return applyMaxCap(
valid: true, mapped,
value: mapped, `Level "${value}" mapped to "${mapped}" for ${modelId} (available: ${validLevels.join(', ')}).`
warning: `Level "${value}" mapped to "${mapped}" for ${modelId} (available: ${validLevels.join(', ')}).`, );
};
} }
// Default to first level // Default to first level
+24 -2
View File
@@ -114,12 +114,34 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
codex: { codex: {
provider: 'codex', provider: 'codex',
displayName: 'Codex', displayName: 'Codex',
defaultModel: 'gpt-5.1-codex-max', defaultModel: 'gpt-5.2-codex',
models: [ models: [
{
id: 'gpt-5.2-codex',
name: 'GPT-5.2 Codex',
description: 'Full reasoning support (xhigh)',
presetMapping: {
default: 'gpt-5.2-codex',
opus: 'gpt-5.2-codex',
sonnet: 'gpt-5.2-codex',
haiku: 'gpt-5-mini',
},
},
{
id: 'gpt-5-mini',
name: 'GPT-5 Mini',
description: 'Fast, capped at high reasoning (no xhigh)',
presetMapping: {
default: 'gpt-5-mini',
opus: 'gpt-5.2-codex',
sonnet: 'gpt-5-mini',
haiku: 'gpt-5-mini',
},
},
{ {
id: 'gpt-5.1-codex-max', id: 'gpt-5.1-codex-max',
name: 'Codex Max (5.1)', name: 'Codex Max (5.1)',
description: 'Most capable Codex model', description: 'Legacy most capable Codex model',
presetMapping: { presetMapping: {
default: 'gpt-5.1-codex-max', default: 'gpt-5.1-codex-max',
opus: 'gpt-5.1-codex-max-high', opus: 'gpt-5.1-codex-max-high',