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
2026-01-21 17:25:33 -05:00
parent eec44d54e2
commit 19b7a49eee
5 changed files with 60 additions and 25 deletions
+13 -8
View File
@@ -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;
+19
View File
@@ -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('# ----------------------------------------------------------------------------');
+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 (
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<ThinkingTierDefaults>
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;
}
}
}
}
@@ -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,
};
}
@@ -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" />
<div className="text-sm text-blue-700 dark:text-blue-300">
<p className="font-medium">Supported Providers</p>
<p className="mt-1 text-blue-600 dark:text-blue-400">
Thinking budget works with: <strong>agy</strong> (Antigravity),{' '}
<strong>gemini</strong> (with thinking models). Other providers may ignore this
setting.
</p>
<ul className="mt-1 space-y-0.5 text-blue-600 dark:text-blue-400">
<li>
Thinking budget: <strong>agy</strong>, <strong>gemini</strong> (token-based)
</li>
<li>
Reasoning effort: <strong>codex</strong> (level-based: medium/high/xhigh)
</li>
</ul>
</div>
</div>