diff --git a/src/web-server/routes/misc-routes.ts b/src/web-server/routes/misc-routes.ts index f88339b4..53cc1306 100644 --- a/src/web-server/routes/misc-routes.ts +++ b/src/web-server/routes/misc-routes.ts @@ -11,7 +11,15 @@ import { loadOrCreateUnifiedConfig, saveUnifiedConfig, getGlobalEnvConfig, + getThinkingConfig, } from '../../config/unified-config-loader'; +import type { ThinkingConfig } from '../../config/unified-config-types'; +import { + THINKING_BUDGET_MIN, + THINKING_BUDGET_MAX, + VALID_THINKING_LEVELS, + THINKING_OFF_VALUES, +} from '../../cliproxy'; import { validateFilePath } from './route-helpers'; const router = Router(); @@ -221,4 +229,99 @@ router.put('/global-env', (req: Request, res: Response): void => { } }); +// ==================== Thinking Configuration ==================== + +/** + * GET /api/thinking - Get thinking budget configuration + * Returns the thinking section from config.yaml + */ +router.get('/thinking', (_req: Request, res: Response): void => { + try { + const config = getThinkingConfig(); + res.json({ config }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + +/** + * PUT /api/thinking - Update thinking budget configuration + * Updates the thinking section in config.yaml + */ +router.put('/thinking', (req: Request, res: Response): void => { + try { + const updates = req.body as Partial; + const config = loadOrCreateUnifiedConfig(); + + // Validate mode if provided + if (updates.mode !== undefined) { + const validModes = ['auto', 'off', 'manual']; + if (!validModes.includes(updates.mode)) { + res.status(400).json({ error: `Invalid mode: must be one of ${validModes.join(', ')}` }); + return; + } + } + + // Validate override if provided (budget or level) + if (updates.override !== undefined) { + if (typeof updates.override === 'number') { + if ( + !Number.isFinite(updates.override) || + updates.override < THINKING_BUDGET_MIN || + updates.override > THINKING_BUDGET_MAX + ) { + res.status(400).json({ + error: `Invalid override: must be between ${THINKING_BUDGET_MIN} and ${THINKING_BUDGET_MAX}`, + }); + return; + } + } else if (typeof updates.override === 'string') { + const normalizedValue = updates.override.toLowerCase().trim(); + const validValues = [...VALID_THINKING_LEVELS, ...THINKING_OFF_VALUES] as readonly string[]; + if (!validValues.includes(normalizedValue)) { + res.status(400).json({ + error: `Invalid override: must be a level name (${VALID_THINKING_LEVELS.join(', ')}) or a number`, + }); + return; + } + } + } + + // Validate tier_defaults if provided + if (updates.tier_defaults !== undefined) { + const validLevels = [...VALID_THINKING_LEVELS] as string[]; + for (const [tier, level] of Object.entries(updates.tier_defaults)) { + if (!['opus', 'sonnet', 'haiku'].includes(tier)) { + res.status(400).json({ error: `Invalid tier: ${tier}` }); + return; + } + if (!validLevels.includes(level)) { + res.status(400).json({ + error: `Invalid level for ${tier}: must be one of ${validLevels.join(', ')}`, + }); + return; + } + } + } + + // Update thinking section + config.thinking = { + mode: updates.mode ?? config.thinking?.mode ?? 'auto', + override: updates.override ?? config.thinking?.override, + tier_defaults: { + opus: updates.tier_defaults?.opus ?? config.thinking?.tier_defaults?.opus ?? 'high', + sonnet: updates.tier_defaults?.sonnet ?? config.thinking?.tier_defaults?.sonnet ?? 'medium', + haiku: updates.tier_defaults?.haiku ?? config.thinking?.tier_defaults?.haiku ?? 'low', + }, + provider_overrides: updates.provider_overrides ?? config.thinking?.provider_overrides, + show_warnings: updates.show_warnings ?? config.thinking?.show_warnings ?? true, + }; + + saveUnifiedConfig(config); + res.json({ success: true, config: config.thinking }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + export default router;