mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-18 08:24:33 +00:00
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:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user