fix(cliproxy): handle edge cases in thinking validation

- model-catalog: add null guard and trim for modelId lookup
- thinking-validator: add 100 char length limit, accept "8192.0" format
- cliproxy-executor: error on empty --thinking= value
- use-thinking-config: handle HTML error pages, return cleanup fn
This commit is contained in:
kaitranntt
2026-01-21 17:52:16 -05:00
parent 299d96c011
commit ca490a9f4e
4 changed files with 42 additions and 4 deletions
+7
View File
@@ -337,6 +337,13 @@ export async function execClaudeWithCLIProxy(
const thinkingEqArg = argsWithoutProxy.find((arg) => arg.startsWith('--thinking=')); const thinkingEqArg = argsWithoutProxy.find((arg) => arg.startsWith('--thinking='));
if (thinkingEqArg) { if (thinkingEqArg) {
const val = thinkingEqArg.substring('--thinking='.length); const val = thinkingEqArg.substring('--thinking='.length);
// Handle empty value after equals (--thinking=)
if (!val || val.trim() === '') {
console.error(fail('--thinking requires a value'));
console.error(' Examples: --thinking=low, --thinking=8192, --thinking=off');
console.error(' Levels: minimal, low, medium, high, xhigh, auto');
process.exit(1);
}
// Parse as number if numeric, otherwise keep as string (level name) // Parse as number if numeric, otherwise keep as string (level name)
const numVal = parseInt(val, 10); const numVal = parseInt(val, 10);
thinkingOverride = !isNaN(numVal) ? numVal : val; thinkingOverride = !isNaN(numVal) ? numVal : val;
+2 -2
View File
@@ -188,8 +188,8 @@ export function getProviderCatalog(provider: CLIProxyProvider): ProviderCatalog
*/ */
export function findModel(provider: CLIProxyProvider, modelId: string): ModelEntry | undefined { export function findModel(provider: CLIProxyProvider, modelId: string): ModelEntry | undefined {
const catalog = MODEL_CATALOG[provider]; const catalog = MODEL_CATALOG[provider];
if (!catalog) return undefined; if (!catalog || !modelId) return undefined;
const normalizedId = modelId.toLowerCase(); const normalizedId = modelId.trim().toLowerCase();
return catalog.models.find((m) => m.id.toLowerCase() === normalizedId); return catalog.models.find((m) => m.id.toLowerCase() === normalizedId);
} }
+17 -1
View File
@@ -139,6 +139,15 @@ export function validateThinking(
): ThinkingValidationResult { ): ThinkingValidationResult {
const thinking = getModelThinkingSupport(provider, modelId); const thinking = getModelThinkingSupport(provider, modelId);
// Handle string length limit to prevent performance issues
if (typeof value === 'string' && value.length > 100) {
return {
valid: false,
value: 'off',
warning: 'Thinking value too long (max 100 chars). Using "off".',
};
}
// Handle empty string explicitly // Handle empty string explicitly
if (typeof value === 'string' && value.trim() === '') { if (typeof value === 'string' && value.trim() === '') {
return { return {
@@ -221,7 +230,14 @@ function validateBudgetThinking(
} else { } else {
// Strict number parsing: reject partial parses like "123abc" // Strict number parsing: reject partial parses like "123abc"
const parsed = Number(value); const parsed = Number(value);
if (isNaN(parsed) || !Number.isFinite(parsed) || value.trim() !== String(parsed)) { const trimmed = value.trim();
// Accept trailing .0 for whole numbers (e.g., "8192.0" → 8192)
const isWholeWithDecimal = Number.isInteger(parsed) && trimmed === `${parsed}.0`;
if (
isNaN(parsed) ||
!Number.isFinite(parsed) ||
(trimmed !== String(parsed) && !isWholeWithDecimal)
) {
// Not a valid number, try to find closest level // Not a valid number, try to find closest level
const closest = findClosestLevel(value, Object.keys(THINKING_LEVEL_BUDGETS)); const closest = findClosestLevel(value, Object.keys(THINKING_LEVEL_BUDGETS));
if (closest) { if (closest) {
@@ -37,7 +37,14 @@ export function useThinkingConfig() {
setError(null); setError(null);
const res = await fetch('/api/thinking', { signal: controller.signal }); const res = await fetch('/api/thinking', { signal: controller.signal });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!res.ok) throw new Error('Failed to load Thinking config'); if (!res.ok) {
// Handle HTML error pages gracefully
const contentType = res.headers.get('content-type');
if (contentType?.includes('text/html')) {
throw new Error(`Server error (${res.status})`);
}
throw new Error('Failed to load Thinking config');
}
const data = await res.json(); const data = await res.json();
setConfig(data.config || DEFAULT_THINKING_CONFIG); setConfig(data.config || DEFAULT_THINKING_CONFIG);
// W4: Store lastModified for optimistic locking // W4: Store lastModified for optimistic locking
@@ -53,6 +60,9 @@ export function useThinkingConfig() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
// Return cleanup function for AbortController
return () => controller.abort();
}, []); }, []);
const saveConfig = useCallback( const saveConfig = useCallback(
@@ -84,6 +94,11 @@ export function useThinkingConfig() {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!res.ok) { if (!res.ok) {
// Handle HTML error pages gracefully
const contentType = res.headers.get('content-type');
if (contentType?.includes('text/html')) {
throw new Error(`Server error (${res.status})`);
}
const data = await res.json(); const data = await res.json();
// W4: Handle conflict (409) with user-friendly message // W4: Handle conflict (409) with user-friendly message
if (res.status === 409) { if (res.status === 409) {