feat(thinking): improve config validation and codex support

- Add shouldShowWarnings() helper for cleaner warning control
- Improve applyThinkingSuffix() regex for accurate suffix detection
- Fix provider_overrides validation for nested tier structure
- Add thinking section to YAML config export with documentation
- Update UI to clarify codex uses reasoning effort levels
- Remove unused setManualOverride hook
This commit is contained in:
kaitranntt committed 2026-01-21 17:25:33 -05:00
1 parent eec44d54e2
commit 19b7a49eee
5 files changed
+60 -25

No files matched your search

+13 -8
View File
@@ -25,6 +25,14 @@ import { supportsThinking } from './model-catalog';
import { ThinkingConfig } from '../config/unified-config-types'; import { ThinkingConfig } from '../config/unified-config-types';
import { validateThinking } from './thinking-validator'; 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 */ /** Settings file structure for user overrides */
interface ProviderSettings { interface ProviderSettings {
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
@@ -54,14 +62,11 @@ export function detectTierFromModel(modelName: string): ModelTier {
* @param model - Base model name * @param model - Base model name
* @param thinkingValue - Level name (e.g., 'high') or numeric budget * @param thinkingValue - Level name (e.g., 'high') or numeric budget
* @returns Model name with thinking suffix, e.g., "gemini-3-pro-preview(high)" * @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 { export function applyThinkingSuffix(model: string, thinkingValue: string | number): string {
// Don't apply if already has suffix // Don't apply if model already ends with a parenthesized suffix (e.g., "model(high)" or "model(8192)")
// NOTE: This only detects ANY paren pair, not proper nesting (e.g., "model(foo)(8192)" passes through) // Matches: ends with "(...)" where content is non-empty
if (model.includes('(') && model.includes(')')) { if (/\([^)]+\)$/.test(model)) {
return model; return model;
} }
return `${model}(${thinkingValue})`; return `${model}(${thinkingValue})`;
@@ -111,7 +116,7 @@ export function applyThinkingConfig(
const baseModel = result.ANTHROPIC_MODEL || ''; const baseModel = result.ANTHROPIC_MODEL || '';
if (!supportsThinking(provider, baseModel)) { if (!supportsThinking(provider, baseModel)) {
// U2: Warn user if they explicitly provided --thinking but model doesn't support it // 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( console.warn(
warn( warn(
`Model ${baseModel || 'unknown'} (provider: ${provider}) does not support thinking budget. --thinking flag ignored.` `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 // Validate thinking value against model capabilities
const validation = validateThinking(provider, baseModel, thinkingValue); const validation = validateThinking(provider, baseModel, thinkingValue);
if (validation.warning && thinkingConfig.show_warnings !== false) { if (validation.warning && shouldShowWarnings(thinkingConfig)) {
console.warn(warn(validation.warning)); console.warn(warn(validation.warning));
} }
thinkingValue = validation.value; thinkingValue = validation.value;
+19
View File
@@ -470,6 +470,25 @@ function generateYamlWithComments(config: UnifiedConfig): string {
lines.push(''); 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) // Dashboard auth section (only if configured)
if (config.dashboard_auth?.enabled) { if (config.dashboard_auth?.enabled) {
lines.push('# ----------------------------------------------------------------------------'); lines.push('# ----------------------------------------------------------------------------');
+20 -4
View File
@@ -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<string, Partial<ThinkingTierDefaults>>)
if (updates.provider_overrides !== undefined) { if (updates.provider_overrides !== undefined) {
if ( if (
typeof updates.provider_overrides !== 'object' || typeof updates.provider_overrides !== 'object' ||
@@ -352,19 +352,35 @@ router.put('/thinking', (req: Request, res: Response): void => {
return; return;
} }
const validLevels = [...VALID_THINKING_LEVELS] as string[]; 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() === '') { if (typeof provider !== 'string' || provider.trim() === '') {
res res
.status(400) .status(400)
.json({ error: 'Invalid provider_overrides: keys must be non-empty strings' }); .json({ error: 'Invalid provider_overrides: keys must be non-empty strings' });
return; return;
} }
if (typeof level !== 'string' || !validLevels.includes(level)) { // tierOverrides should be Partial<ThinkingTierDefaults>
if (typeof tierOverrides !== 'object' || tierOverrides === null) {
res.status(400).json({ 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; 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;
}
}
} }
} }
@@ -141,13 +141,6 @@ export function useThinkingConfig() {
[saveConfig] [saveConfig]
); );
const setManualOverride = useCallback(
(value: string | number | undefined) => {
saveConfig({ mode: 'manual', override: value });
},
[saveConfig]
);
return { return {
config: config || DEFAULT_THINKING_CONFIG, config: config || DEFAULT_THINKING_CONFIG,
loading, loading,
@@ -159,6 +152,5 @@ export function useThinkingConfig() {
setMode, setMode,
setTierDefault, setTierDefault,
setShowWarnings, setShowWarnings,
setManualOverride,
}; };
} }
@@ -110,11 +110,14 @@ export default function ThinkingSection() {
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" /> <Info className="w-4 h-4 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" />
<div className="text-sm text-blue-700 dark:text-blue-300"> <div className="text-sm text-blue-700 dark:text-blue-300">
<p className="font-medium">Supported Providers</p> <p className="font-medium">Supported Providers</p>
<p className="mt-1 text-blue-600 dark:text-blue-400"> <ul className="mt-1 space-y-0.5 text-blue-600 dark:text-blue-400">
Thinking budget works with: <strong>agy</strong> (Antigravity),{' '} <li>
<strong>gemini</strong> (with thinking models). Other providers may ignore this Thinking budget: <strong>agy</strong>, <strong>gemini</strong> (token-based)
setting. </li>
</p> <li>
Reasoning effort: <strong>codex</strong> (level-based: medium/high/xhigh)
</li>
</ul>
</div> </div>
</div> </div>