diff --git a/open-sse/services/modelCapabilities.ts b/open-sse/services/modelCapabilities.ts index de86e1fd7a..b3822e9672 100644 --- a/open-sse/services/modelCapabilities.ts +++ b/open-sse/services/modelCapabilities.ts @@ -48,3 +48,54 @@ export function supportsToolCalling(modelStr: string): boolean { return !blocked; } + +// Models that do NOT support reasoning/thinking parameters. +// AG (Antigravity) claude-sonnet-4-6 routes through a Google internal API +// that returns 400 if thinking params are included. +const REASONING_UNSUPPORTED_PATTERNS = [ + "antigravity/claude-sonnet-4-6", + "antigravity/claude-sonnet-4-5", + "antigravity/claude-sonnet-4", + "ag/claude-sonnet-4-6", + "ag/claude-sonnet-4-5", + "ag/claude-sonnet-4", +]; + +function getRegistryReasoningFlag(providerIdOrAlias: string, modelId: string): boolean | null { + const providerAlias = PROVIDER_ID_TO_ALIAS[providerIdOrAlias] || providerIdOrAlias; + const models = PROVIDER_MODELS[providerAlias]; + if (!Array.isArray(models)) return null; + const found = models.find((m) => m?.id === modelId); + if (!found) return null; + return typeof found.supportsReasoning === "boolean" ? found.supportsReasoning : null; +} + +/** + * Returns whether a model supports reasoning/thinking parameters. + * + * Decision order: + * 1) Provider registry metadata (supportsReasoning flag) when available. + * 2) Explicit denylist for known unsupported models (e.g. AG Claude Sonnet). + * 3) Default true (pass through — safe, provider will ignore if unsupported). + */ +export function supportsReasoning(modelStr: string): boolean { + const parsed = parseModel(modelStr); + const provider = parsed.provider || parsed.providerAlias || ""; + const model = parsed.model || modelStr; + + if (provider) { + const fromRegistry = getRegistryReasoningFlag(provider, model); + if (fromRegistry !== null) return fromRegistry; + } + + const normalized = String(modelStr || "").toLowerCase(); + if (!normalized) return true; + + const blocked = REASONING_UNSUPPORTED_PATTERNS.some((pattern) => + normalized === pattern || + normalized.endsWith(`/${pattern}`) || + normalized.includes(pattern) + ); + + return !blocked; +} diff --git a/open-sse/services/thinkingBudget.ts b/open-sse/services/thinkingBudget.ts index 4028037c97..e47cfbc9fb 100644 --- a/open-sse/services/thinkingBudget.ts +++ b/open-sse/services/thinkingBudget.ts @@ -14,6 +14,7 @@ export const ThinkingMode = { }; import { capThinkingBudget, getDefaultThinkingBudget } from "@/shared/constants/modelSpecs"; +import { supportsReasoning } from "./modelCapabilities.ts"; // Effort → budget token mapping export const EFFORT_BUDGETS = { @@ -151,6 +152,13 @@ export function applyThinkingBudget(body, config = null) { const cfg = config || _config; if (!body || typeof body !== "object") return body; + // Early exit: strip ALL reasoning/thinking params for models that don't support them. + // Sending thinking params to unsupported models (e.g. AG claude-sonnet-4-6) causes 400 errors. + const modelStr = typeof body.model === "string" ? body.model : ""; + if (modelStr && !supportsReasoning(modelStr)) { + return stripThinkingConfig(body); + } + // Pre-processing: convert string thinkingLevel to numeric budget let processed = normalizeThinkingLevel(body);