diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 10c9b5db..5abc9d44 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -25,6 +25,14 @@ import { supportsThinking } from './model-catalog'; import { ThinkingConfig } from '../config/unified-config-types'; import { validateThinking } from './thinking-validator'; +/** + * Check if warnings should be shown based on thinking config. + * Defaults to true if show_warnings is not explicitly false. + */ +function shouldShowWarnings(thinkingConfig: ThinkingConfig): boolean { + return thinkingConfig.show_warnings !== false; +} + /** Settings file structure for user overrides */ interface ProviderSettings { env: NodeJS.ProcessEnv; @@ -54,14 +62,11 @@ export function detectTierFromModel(modelName: string): ModelTier { * @param model - Base model name * @param thinkingValue - Level name (e.g., 'high') or numeric budget * @returns Model name with thinking suffix, e.g., "gemini-3-pro-preview(high)" - * - * @note Paren matching only handles one level. Nested parens like "model(foo)(8192)" - * will be treated as already having suffix and returned unchanged. */ export function applyThinkingSuffix(model: string, thinkingValue: string | number): string { - // Don't apply if already has suffix - // NOTE: This only detects ANY paren pair, not proper nesting (e.g., "model(foo)(8192)" passes through) - if (model.includes('(') && model.includes(')')) { + // Don't apply if model already ends with a parenthesized suffix (e.g., "model(high)" or "model(8192)") + // Matches: ends with "(...)" where content is non-empty + if (/\([^)]+\)$/.test(model)) { return model; } return `${model}(${thinkingValue})`; @@ -111,7 +116,7 @@ export function applyThinkingConfig( const baseModel = result.ANTHROPIC_MODEL || ''; if (!supportsThinking(provider, baseModel)) { // U2: Warn user if they explicitly provided --thinking but model doesn't support it - if (thinkingOverride !== undefined && thinkingConfig.show_warnings !== false) { + if (thinkingOverride !== undefined && shouldShowWarnings(thinkingConfig)) { console.warn( warn( `Model ${baseModel || 'unknown'} (provider: ${provider}) does not support thinking budget. --thinking flag ignored.` @@ -140,7 +145,7 @@ export function applyThinkingConfig( // Validate thinking value against model capabilities const validation = validateThinking(provider, baseModel, thinkingValue); - if (validation.warning && thinkingConfig.show_warnings !== false) { + if (validation.warning && shouldShowWarnings(thinkingConfig)) { console.warn(warn(validation.warning)); } thinkingValue = validation.value; diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 134a2558..273de189 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -470,6 +470,25 @@ function generateYamlWithComments(config: UnifiedConfig): string { lines.push(''); } + // Thinking section (extended thinking/reasoning configuration) + if (config.thinking) { + lines.push('# ----------------------------------------------------------------------------'); + lines.push('# Thinking: Extended thinking/reasoning budget configuration'); + lines.push('# Controls reasoning depth for supported providers (agy, gemini, codex).'); + lines.push('#'); + lines.push('# Modes: auto (use tier_defaults), off (disable), manual (--thinking flag only)'); + lines.push('# Levels: minimal (512), low (1K), medium (8K), high (24K), xhigh (32K), auto'); + lines.push('# Override: Set global override value (number or level name)'); + lines.push('# Provider overrides: Per-provider tier defaults'); + lines.push('# ----------------------------------------------------------------------------'); + lines.push( + yaml + .dump({ thinking: config.thinking }, { indent: 2, lineWidth: -1, quotingType: '"' }) + .trim() + ); + lines.push(''); + } + // Dashboard auth section (only if configured) if (config.dashboard_auth?.enabled) { lines.push('# ----------------------------------------------------------------------------'); diff --git a/src/web-server/routes/misc-routes.ts b/src/web-server/routes/misc-routes.ts index 12228ac0..712b56f7 100644 --- a/src/web-server/routes/misc-routes.ts +++ b/src/web-server/routes/misc-routes.ts @@ -341,7 +341,7 @@ router.put('/thinking', (req: Request, res: Response): void => { } } - // C4: Validate provider_overrides if provided + // C4: Validate provider_overrides if provided (nested structure: Record>) if (updates.provider_overrides !== undefined) { if ( typeof updates.provider_overrides !== 'object' || @@ -352,19 +352,35 @@ router.put('/thinking', (req: Request, res: Response): void => { return; } const validLevels = [...VALID_THINKING_LEVELS] as string[]; - for (const [provider, level] of Object.entries(updates.provider_overrides)) { + const validTiers = ['opus', 'sonnet', 'haiku']; + for (const [provider, tierOverrides] of Object.entries(updates.provider_overrides)) { if (typeof provider !== 'string' || provider.trim() === '') { res .status(400) .json({ error: 'Invalid provider_overrides: keys must be non-empty strings' }); return; } - if (typeof level !== 'string' || !validLevels.includes(level)) { + // tierOverrides should be Partial + if (typeof tierOverrides !== 'object' || tierOverrides === null) { res.status(400).json({ - error: `Invalid level for provider ${provider}: must be one of ${validLevels.join(', ')}`, + error: `Invalid provider_overrides for ${provider}: must be an object with tier→level mapping`, }); return; } + for (const [tier, level] of Object.entries(tierOverrides)) { + if (!validTiers.includes(tier)) { + res.status(400).json({ + error: `Invalid tier '${tier}' in provider_overrides.${provider}: must be one of ${validTiers.join(', ')}`, + }); + return; + } + if (typeof level !== 'string' || !validLevels.includes(level)) { + res.status(400).json({ + error: `Invalid level for provider_overrides.${provider}.${tier}: must be one of ${validLevels.join(', ')}`, + }); + return; + } + } } } diff --git a/ui/src/pages/settings/hooks/use-thinking-config.ts b/ui/src/pages/settings/hooks/use-thinking-config.ts index 9c59bbb7..1c686ff6 100644 --- a/ui/src/pages/settings/hooks/use-thinking-config.ts +++ b/ui/src/pages/settings/hooks/use-thinking-config.ts @@ -141,13 +141,6 @@ export function useThinkingConfig() { [saveConfig] ); - const setManualOverride = useCallback( - (value: string | number | undefined) => { - saveConfig({ mode: 'manual', override: value }); - }, - [saveConfig] - ); - return { config: config || DEFAULT_THINKING_CONFIG, loading, @@ -159,6 +152,5 @@ export function useThinkingConfig() { setMode, setTierDefault, setShowWarnings, - setManualOverride, }; } diff --git a/ui/src/pages/settings/sections/thinking/index.tsx b/ui/src/pages/settings/sections/thinking/index.tsx index e4754d2c..6268e7fd 100644 --- a/ui/src/pages/settings/sections/thinking/index.tsx +++ b/ui/src/pages/settings/sections/thinking/index.tsx @@ -110,11 +110,14 @@ export default function ThinkingSection() {

Supported Providers

-

- Thinking budget works with: agy (Antigravity),{' '} - gemini (with thinking models). Other providers may ignore this - setting. -

+
    +
  • + Thinking budget: agy, gemini (token-based) +
  • +
  • + Reasoning effort: codex (level-based: medium/high/xhigh) +
  • +