import { CODEX_GENERIC_CONTEXT_WINDOW, CODEX_GPT_5_6_CONTEXT_CAP, isCodexGpt56Tier, isCodexProductTransport, } from "./context-cap-policy"; import { applyOpenAIModelPricing } from "./model-pricing"; import { isAuditedOpenAIReasoningTransport, resolveOpenAICompat } from "./openai-completions-compat"; import type { Api, Model as ApiModel, ThinkingConfig } from "./types"; import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability"; /** User-facing thinking levels, ordered least to most intensive. */ export const enum Effort { Minimal = "minimal", Low = "low", Medium = "medium", High = "high", XHigh = "xhigh", Max = "max", } export const THINKING_EFFORTS: readonly Effort[] = [ Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max, ]; const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]; const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [ Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, ]; const DEFAULT_REASONING_EFFORTS_WITH_MAX: readonly Effort[] = [ Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.Max, ]; const DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX: readonly Effort[] = [ Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max, ]; const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High]; const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]; // Gemini 3.7 Flash dropped `minimal`; the official API returns an error for it. // https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash const GEMINI_3_7_FLASH_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High]; const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]; const GPT_5_6_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max]; const GPT_5_5_DEFAULT_EFFORT = Effort.XHigh; const KIMI_K3_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max]; const DEEPSEEK_V4_FLASH_0731_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High, Effort.Max]; const GROK_4_5_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High]; const GROK_4_6_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]; const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High]; const CLOUDFLARE_AI_GATEWAY_BASE_URL = "https://gateway.ai.cloudflare.com/v1///anthropic"; type SemVer = { major: number; minor: number; patch: number; }; type GeminiKind = "pro" | "flash"; type AnthropicKind = "opus" | "sonnet" | "fable"; type OpenAIVariant = | "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "luna" | "mini" | "max" | "nano" | "sol" | "terra"; const CODEX_GPT_5_4_PRIORITY_BY_VARIANT: Partial> = { base: 0, mini: 1, nano: 2, }; const COPILOT_GENERATED_LIMITS: Record = { "claude-opus-4.6": { contextWindow: 168000, maxTokens: 32000 }, "gpt-5.2": { contextWindow: 272000, maxTokens: 128000 }, "gpt-5.4": { contextWindow: 272000, maxTokens: 128000 }, "gpt-5.4-mini": { contextWindow: 272000, maxTokens: 128000 }, "grok-code-fast-1": { contextWindow: 192000, maxTokens: 64000 }, }; interface GeminiModel { family: "gemini"; kind: GeminiKind; version: SemVer; } interface AnthropicModel { family: "anthropic"; kind: AnthropicKind; version: SemVer; } interface OpenAIModel { family: "openai"; variant: OpenAIVariant; version: SemVer; } interface UnknownModel { family: "unknown"; id: string; } type ParsedModel = GeminiModel | AnthropicModel | OpenAIModel | UnknownModel; /** * Static fallback model injected when Cloudflare AI Gateway discovery * returns no results. Ensures the provider always has at least one usable * model entry in the catalog. */ export const CLOUDFLARE_FALLBACK_MODEL: ApiModel<"anthropic-messages"> = { id: "claude-sonnet-4-5", name: "Anthropic Sonnet 4.5", api: "anthropic-messages", provider: "cloudflare-ai-gateway", baseUrl: CLOUDFLARE_AI_GATEWAY_BASE_URL, reasoning: true, input: ["text", "image"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, }, contextWindow: 200000, maxTokens: 64000, }; const kEnrichedModel = Symbol("model-thinking.enrichedModel"); type ModelWithEnriched = ApiModel & { [kEnrichedModel]?: ApiModel }; export function isGroqCompoundReasoningUnsupported(model: Pick, "provider" | "id">): boolean { return model.provider === "groq" && (model.id === "groq/compound" || model.id === "groq/compound-mini"); } /** * Returns a copy of the model with canonical thinking metadata attached. * * This helper belongs to catalog enrichment only. Runtime consumers should * trust `model.thinking` and avoid inferring capabilities on demand. */ export function enrichModelThinking(model: ApiModel): ApiModel { const tagged = model as ModelWithEnriched; const cached = tagged[kEnrichedModel]; if (cached !== undefined) { return cached as ApiModel; } const normalizedThinking = normalizeThinkingConfig(model.thinking); let result: ApiModel; if (isGroqCompoundReasoningUnsupported(model)) { result = !model.reasoning && normalizedThinking === undefined ? model : { ...model, reasoning: false, thinking: undefined }; } else if (!model.reasoning || !modelSupportsReasoningControl(model)) { result = normalizedThinking === undefined && model.thinking === undefined ? model : { ...model, thinking: undefined }; } else { const thinking = normalizedThinking ?? inferModelThinking(model); result = thinkingsEqual(normalizedThinking, thinking) ? model : { ...model, thinking }; } // Stash the enriched copy on a non-enumerable slot so callers that hand us // the same reference twice skip the work. `enumerable: false` is critical: // many call sites build derived models via `{ ...model, ...overrides }`, // which would otherwise copy this cache slot and trick us into returning // the *original* enriched model — silently discarding the overrides. Object.defineProperty(tagged, kEnrichedModel, { value: result, enumerable: false, configurable: true, writable: true, }); return result; } /** * Returns a copy of the model with thinking metadata recomputed from the * canonical rules, replacing any existing `thinking`. */ export function refreshModelThinking(model: ApiModel): ApiModel { if (isGroqCompoundReasoningUnsupported(model)) { return !model.reasoning && model.thinking === undefined ? model : { ...model, reasoning: false, thinking: undefined }; } if (!model.reasoning || !modelSupportsReasoningControl(model)) { const normalizedThinking = normalizeThinkingConfig(model.thinking); return normalizedThinking === undefined && model.thinking === undefined ? model : { ...model, thinking: undefined }; } return { ...model, thinking: inferModelThinking(model) }; } /** * Returns whether the configured transport has an audited user-facing reasoning control. * * Custom OpenAI-compatible endpoints fail closed: declaring a model as reasoning-capable * is not enough to prove that the proxy accepts OpenAI reasoning parameters. Unknown * endpoints must opt in with `compat.supportsReasoningEffort: true`; providers using a * non-OpenAI request shape must also declare `compat.thinkingFormat`. Bundled providers * remain governed by their catalog and compatibility metadata. */ export function modelSupportsReasoningControl( model: ApiModel, resolvedBaseUrl?: string, ): boolean { if (!model.reasoning) return false; if (model.api === "openai-completions") { const completionsModel = model as ApiModel<"openai-completions">; const explicitSupport = completionsModel.compat?.supportsReasoningEffort; if (explicitSupport === false) return false; if (explicitSupport !== true && !isAuditedOpenAIReasoningTransport(completionsModel, resolvedBaseUrl)) return false; const compat = resolveOpenAICompat(completionsModel, resolvedBaseUrl); return compat.thinkingFormat !== "openai" || compat.supportsReasoningEffort; } if ( model.api === "openai-responses" || model.api === "openai-codex-responses" || model.api === "azure-openai-responses" ) { const explicitSupport = model.compat !== undefined && "supportsReasoningEffort" in model.compat ? model.compat.supportsReasoningEffort : undefined; return explicitSupport ?? isAuditedOpenAIReasoningTransport(model); } return true; } /** * Apply upstream metadata corrections to a mutable array of models. * * Each model is first normalized through `refreshModelThinking()` so generated * catalogs keep canonical thinking metadata and policy fixes in one pass. */ export function applyGeneratedModelPolicies(models: ApiModel[]): void { for (let index = 0; index < models.length; index++) { const source = models[index]!; if (source.provider === "xai" && (source.id === "grok-4.5" || source.id === "grok-4.6")) { source.reasoning = true; } if (source.provider === "alibaba-token-plan" && source.id === "deepseek-v4-flash-0731") { source.reasoning = true; source.name = "DeepSeek V4 Flash 0731"; } if (source.id.split("/").at(-1)?.toLowerCase() === "muse-spark-1.2") { source.reasoning = true; } if (source.provider === "omlx") { source.reasoning = true; } const model = refreshModelThinking(source); applyGeneratedModelPolicy(model); models[index] = model; } } /** * Link OpenAI model variants to their context promotion targets. * * When a model's context is exhausted, the agent can promote to a sibling * model with a larger context window on the same provider: * - `OpenAI code backend-spark` variants promote to `gpt-5.5`. * * `gpt-5.5` itself is a 1M-context model and is not demoted to `gpt-5.4` * (which has a smaller window), so it has no promotion target. */ export function linkOpenAIPromotionTargets(models: ApiModel[]): void { for (const candidate of models) { const parsedCandidate = parseKnownModel(candidate.id); if (parsedCandidate.family !== "openai") continue; let targetId: string | undefined; if (parsedCandidate.variant === "codex-spark") { targetId = "gpt-5.5"; } else { continue; } const fallback = models.find( model => model.provider === candidate.provider && model.api === candidate.api && model.id === targetId, ); if (!fallback) continue; candidate.contextPromotionTarget = `${fallback.provider}/${fallback.id}`; } } /** * Returns the supported thinking efforts declared on the model metadata. * * Catalog enrichment is responsible for normalizing bundled model metadata up front. * Runtime callers must treat explicit `model.thinking` on custom models as authoritative * so proxy-specific overrides from `models.yml` survive request construction. * * @throws Error when a reasoning-capable model is missing thinking metadata */ export function getSupportedEfforts(model: ApiModel): readonly Effort[] { if (!modelSupportsReasoningControl(model)) { return []; } if (!model.thinking) { throw new Error(`Model ${model.provider}/${model.id} is missing thinking metadata`); } return expandEffortRange(model.thinking); } /** * Clamps a requested thinking level against explicit model metadata. * * Non-reasoning models always resolve to `undefined`. */ export function clampThinkingLevelForModel( model: ApiModel | undefined, requested: Effort | undefined, ): Effort | undefined { if (!model) { return requested; } if (!modelSupportsReasoningControl(model) || requested === undefined) { return undefined; } const levels = getSupportedEfforts(model); if (levels.includes(requested)) { return requested; } const requestedIndex = THINKING_EFFORTS.indexOf(requested); if (requestedIndex === -1) { return undefined; } let clamped: Effort | undefined; for (const effort of levels) { if (THINKING_EFFORTS.indexOf(effort) > requestedIndex) { break; } clamped = effort; } return clamped ?? levels[0]; } export function requireSupportedEffort(model: ApiModel, effort: Effort): Effort { if (!modelSupportsReasoningControl(model)) { throw new Error(`Model ${model.provider}/${model.id} does not support thinking`); } const levels = getSupportedEfforts(model); if (!levels.includes(effort)) { throw new Error( `Thinking effort ${effort} is not supported by ${model.provider}/${model.id}. Supported efforts: ${levels.join(", ")}`, ); } return effort; } /** Maps a normalized thinking effort to Google's `thinkingLevel` enum values. */ export function mapEffortToGoogleThinkingLevel( model: ApiModel, effort: Effort, ): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" { switch (requireSupportedEffort(model, effort)) { case Effort.Minimal: return "MINIMAL"; case Effort.Low: return "LOW"; case Effort.Medium: return "MEDIUM"; case Effort.High: case Effort.XHigh: case Effort.Max: return "HIGH"; } } /** Maps a normalized thinking effort to Anthropic adaptive effort values. */ export function mapEffortToAnthropicAdaptiveEffort( model: ApiModel, effort: Effort, ): "low" | "medium" | "high" | "xhigh" | "max" { switch (requireSupportedEffort(model, effort)) { case Effort.Minimal: case Effort.Low: return "low"; case Effort.Medium: return "medium"; case Effort.High: return "high"; case Effort.XHigh: case Effort.Max: return effort === Effort.XHigh ? "xhigh" : "max"; } } /** * Returns true for Anthropic models with Opus 4.7 API restrictions: * - Sampling parameters (temperature/top_p/top_k) return 400 error * - Thinking content is omitted by default (needs display: "summarized") */ export function hasOpus47ApiRestrictions(modelId: string): boolean { const parsed = parseAnthropicModel(getCanonicalModelId(modelId)); if (!parsed) return false; return semverGte(parsed.version, "4.7") && parsed.kind === "opus"; } /** * Adaptive thinking `display` is supported starting with Anthropic Opus 4.7. * Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field. * Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to * "omitted" — thinking tokens are billed but no content streams back — so it * must opt in like Opus 4.7+ (issue #2791). * * Shares `hasOpus47ApiRestrictions` version parsing on purpose: the two * predicates describe the same API generation, and a private `claude-opus-(\d+)-(\d+)` * regex silently disagreed with it for single-component aliases (`claude-opus-5` * matched nothing while `claude-opus-5-20260101` matched), so the same model sent * a different thinking shape and beta set depending on which id string was used. * Bedrock region/inference-profile prefixes are handled by the canonical parser. */ export function supportsAnthropicAdaptiveThinkingDisplay(modelId: string): boolean { if (/claude-fable-\d/.test(modelId)) return true; return hasOpus47ApiRestrictions(modelId); } function anthropicModelHasRealXHighEffort(model: ApiModel): boolean { if (model.api !== "anthropic-messages") return false; const parsedModel = parseKnownModel(model.id); if (parsedModel.family !== "anthropic") return false; // Explicit capability predicate instead of a generic `kind === opus` gate: // Sonnet 5 officially exposes Anthropic's real xhigh and max presets on // the Messages API just like Opus 4.7+. Older Sonnet generations do not, // so the predicate stays fail-closed for them. if (parsedModel.kind === "opus") { return semverGte(parsedModel.version, "4.7"); } if (parsedModel.kind === "sonnet") { return semverGte(parsedModel.version, "5.0"); } return false; } function applyGeneratedModelPolicy(model: ApiModel): void { applyOpenAIModelPricing(model); const copilotLimits = model.provider === "github-copilot" ? COPILOT_GENERATED_LIMITS[model.id] : undefined; if (copilotLimits) { model.contextWindow = copilotLimits.contextWindow; model.maxTokens = copilotLimits.maxTokens; } if ( model.api === "openai-completions" && (model.provider === "minimax-code" || model.provider === "minimax-code-cn") ) { model.compat = { ...(model.compat ?? {}), supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: false, reasoningContentField: "reasoning_content", }; delete model.compat.thinkingFormat; } if (model.provider === "omlx" && model.api === "openai-completions") { model.compat = { ...(model.compat ?? {}), supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, thinkingFormat: "qwen-chat-template", reasoningContentField: "reasoning_content", }; } model.name = scrubGeneratedModelName(model.name); if ( model.api === "openai-completions" && model.provider === "opencode-go" && (model.id === "deepseek-v4-flash" || model.id === "deepseek-v4-pro") ) { model.compat = { ...(model.compat ?? {}), supportsToolChoice: false, reasoningContentField: "reasoning_content", requiresReasoningContentForToolCalls: true, }; } const parsedModel = parseKnownModel(model.id); const applyPatchToolType = inferGeneratedApplyPatchToolType(model, parsedModel); if (applyPatchToolType) { model.applyPatchToolType = applyPatchToolType; } else { delete model.applyPatchToolType; } if ( (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") && isClaudeForcedToolChoiceIncapableModelId(model.id) ) { // Claude Mythos accepts tools but rejects forced tool use (Anthropic // 400: "tool_choice forces tool use is not compatible with this model"). model.compat = { ...(model.compat ?? {}), toolChoiceSupport: "auto" } as typeof model.compat; } if (parsedModel.family === "anthropic") { applyAnthropicCatalogPolicy(model, parsedModel); } if (parsedModel.family === "openai") { applyOpenAICatalogPolicy(model, parsedModel); } // GLM-5.2 (Zhipu/ZAI): ships a 1M lossless context window, but the bundled // catalog copied GLM-5.1's 200K and that stale value survives generate-models // (provider-scoped models bypass the models.dev refresh in applyGlobalModelsDevFallback). // Pin to the true 1M so context-cap / auto-compaction thresholds aren't tripped ~5x early. if (model.provider === "zai" && model.id === "glm-5.2") { model.contextWindow = 1_000_000; } // GLM-5.3 always thinks and exposes only low/high/max reasoning_effort. // https://z.ai/blog/glm-5.3#api-changes-in-glm-5-3 if (model.provider === "zai" && model.id === "glm-5.3") { model.thinking = { mode: "effort", minLevel: Effort.Low, maxLevel: Effort.Max, defaultLevel: Effort.Max, levels: [Effort.Low, Effort.High, Effort.Max], }; } // Groq's agentic `compound` systems reject `reasoning_effort` outright // (400 "`reasoning_effort` is not supported with this model", verified // 2026-08-23), yet models.dev advertises them as reasoning models. Drop the // thinking config so GJC never sends the field. if (isGroqCompoundReasoningUnsupported(model)) { model.reasoning = false; delete model.thinking; } // Kilo's Ox Alpha catalog row advertises reasoning, vision, a 1,048,576-token // context, a 131,072-token output ceiling, and low/high/max variants. Its // OpenAI-compatible `/models` shape exposes those fields outside the generic // discovery parser, so pin the reviewed provider-specific contract here. if (model.provider === "kilo" && model.id === "stealth/ox-alpha") { model.reasoning = true; model.input = ["text", "image"]; model.contextWindow = 1_048_576; model.maxTokens = 131_072; model.thinking = { mode: "effort", minLevel: Effort.Low, maxLevel: Effort.Max, defaultLevel: Effort.Max, levels: [Effort.Low, Effort.High, Effort.Max], }; } if (model.provider === "alibaba-token-plan" && model.id === "deepseek-v4-flash-0731") { model.contextWindow = 1_000_000; model.maxTokens = 384_000; model.compat = { ...(model.compat ?? {}), supportsDeveloperRole: false, supportsReasoningEffort: true, reasoningContentField: "reasoning_content", requiresReasoningContentForToolCalls: true, }; } if (model.provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6")) { model.maxTokens = Math.min(model.maxTokens, 64_000); } // MiniMax-M3's official Token Plan routes expose a 1M context window. // Scope the correction to the four first-class regional MiniMax routes // (canonical id plus the Anthropic Token Plan `[1m]` id); unrelated // catalog aliases and providers keep their own contracts. if ( (model.id === "MiniMax-M3" || model.id === "MiniMax-M3[1m]") && (model.provider === "minimax" || model.provider === "minimax-cn" || model.provider === "minimax-code" || model.provider === "minimax-code-cn") ) { model.contextWindow = 1_000_000; } } function scrubGeneratedModelName(name: string): string { return name .replaceAll("Claude", "Anthropic") .replaceAll("claude", "anthropic") .replaceAll("Codex", "OpenAI code") .replaceAll("codex", "openai-code"); } function applyAnthropicCatalogPolicy(model: ApiModel, parsedModel: AnthropicModel): void { // Anthropic model Opus 4.5: models.dev reports 3x the correct cache pricing. if (model.provider === "anthropic" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.5")) { model.cost.cacheRead = 0.5; model.cost.cacheWrite = 6.25; } // Bedrock Opus 4.6: upstream metadata is stale for cache pricing and context. if (model.provider === "amazon-bedrock" && parsedModel.kind === "opus" && semverEqual(parsedModel.version, "4.6")) { model.cost.cacheRead = 0.5; model.cost.cacheWrite = 6.25; model.contextWindow = 1000000; model.maxTokens = 128000; } } function inferGeneratedApplyPatchToolType( model: ApiModel, parsedModel: ParsedModel, ): ApiModel["applyPatchToolType"] { if (parsedModel.family !== "openai" || parsedModel.version.major !== 5) { return undefined; } if (model.provider === "openai" && model.api === "openai-responses") { return "freeform"; } if (model.provider === "openai-codex" && model.api === "openai-codex-responses") { return "freeform"; } return undefined; } function applyGpt55ContextWindow(model: ApiModel, parsedModel: OpenAIModel): boolean { if (parsedModel.variant === "base" && semverEqual(parsedModel.version, "5.5")) { // JetBrains AI serves GPT through its own gateway, which enforces a probed // 922K prompt cap for every GPT model regardless of the first-party figure. // Its bundled value is measured, so leave it alone. if (model.provider === "jetbrains-junie") { return true; } // The first-party OpenAI GPT-5.5 model advertises a 1M total window, but // the OpenAI code backend request path still enforces the smaller prompt // budget. GJC's `contextWindow` is the usable prompt/input cap, not the // marketing total window; using 1M here delays compaction and makes the UI // promise space that `/responses/compact`/agent turns cannot actually use. model.contextWindow = model.provider === "openai-codex" || model.api === "openai-codex-responses" ? CODEX_GENERIC_CONTEXT_WINDOW : 1_000_000; return true; } return false; } function applyGpt56ContextWindow(model: ApiModel): boolean { if (!isCodexGpt56Tier(model) || !isCodexProductTransport(model)) { return false; } // Force the enforced 372K window: the OpenAI code backend metadata still // under-reports the GPT-5.6 tier budget, and smaller observed values would // otherwise keep the tier at the old 272K cap. First-party OpenAI is untouched. model.contextWindow = CODEX_GPT_5_6_CONTEXT_CAP.enforced; return true; } function applyOpenAICatalogPolicy(model: ApiModel, parsedModel: OpenAIModel): void { if (applyGpt55ContextWindow(model, parsedModel)) { return; } if (applyGpt56ContextWindow(model)) { return; } // OpenAI code backend models: 400K figure includes output budget; input window is 272K. if (parsedModel.variant.startsWith("codex") && parsedModel.variant !== "codex-spark") { model.contextWindow = CODEX_GENERIC_CONTEXT_WINDOW; return; } // GPT-5.4 mini/nano use plain OpenAI IDs on the OpenAI code backend transport, but OpenAI code backend still // enforces the lower prompt budget for these variants. OpenAI code backend discovery can also // report inconsistent priorities for the GPT-5.4 family, so normalize by parsed // variant instead of special-casing raw model ids. if (model.api === "openai-codex-responses" && semverEqual(parsedModel.version, "5.4")) { const normalizedPriority = CODEX_GPT_5_4_PRIORITY_BY_VARIANT[parsedModel.variant]; if (normalizedPriority !== undefined) { model.priority = normalizedPriority; } if (parsedModel.variant === "mini" || parsedModel.variant === "nano") { model.contextWindow = CODEX_GENERIC_CONTEXT_WINDOW; } } } function inferDefaultEffort(model: ApiModel, parsedModel: ParsedModel): Effort | undefined { if (model.provider === "kimi-code" && model.id === "k3") { return Effort.High; } if ( parsedModel.family === "openai" && model.provider === "openai-codex" && semverEqual(parsedModel.version, "5.5") ) { return GPT_5_5_DEFAULT_EFFORT; } if (model.provider === "omlx") { return Effort.Medium; } return undefined; } function inferModelThinking(model: ApiModel): ThinkingConfig { const parsedModel = parseKnownModel(model.id); const efforts = inferSupportedEfforts(parsedModel, model); const minLevel = efforts[0]; const maxLevel = efforts.at(-1); if (!minLevel || !maxLevel) { throw new Error(`Model ${model.provider}/${model.id} resolved to an empty thinking range`); } const config: ThinkingConfig = { mode: inferThinkingControlMode(model, parsedModel), minLevel, maxLevel, }; const defaultLevel = inferDefaultEffort(model, parsedModel); if (defaultLevel && efforts.includes(defaultLevel)) { config.defaultLevel = defaultLevel; } // Encode explicit levels only when the inferred set has gaps the min..max range cannot represent. const minIndex = THINKING_EFFORTS.indexOf(minLevel); const maxIndex = THINKING_EFFORTS.indexOf(maxLevel); const expandedRange = THINKING_EFFORTS.slice(minIndex, maxIndex + 1); if (expandedRange.length !== efforts.length) { config.levels = efforts; } return config; } function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): ThinkingConfig | undefined { if (!thinking || expandEffortRange(thinking).length === 0) { return undefined; } return thinking; } function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig | undefined): boolean { if (left === right) return true; if (!left || !right) return false; if ( left.mode !== right.mode || left.minLevel !== right.minLevel || left.maxLevel !== right.maxLevel || left.defaultLevel !== right.defaultLevel ) return false; const leftLevels = left.levels; const rightLevels = right.levels; if (leftLevels === rightLevels) return true; if (!leftLevels || !rightLevels) return false; if (leftLevels.length !== rightLevels.length) return false; return leftLevels.every((level, index) => level === rightLevels[index]); } function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] { if (thinking.levels && thinking.levels.length > 0) { return thinking.levels; } const minIndex = THINKING_EFFORTS.indexOf(thinking.minLevel); const maxIndex = THINKING_EFFORTS.indexOf(thinking.maxLevel); if (minIndex === -1 || maxIndex === -1 || minIndex > maxIndex) { return []; } return THINKING_EFFORTS.slice(minIndex, maxIndex + 1); } function inferSupportedEfforts(parsedModel: ParsedModel, model: ApiModel): readonly Effort[] { if (model.provider === "xai" && model.id === "grok-4.5") { return GROK_4_5_EFFORTS; } if (model.provider === "xai" && model.id === "grok-4.6") { return GROK_4_6_EFFORTS; } if (model.provider === "kimi-code" && model.id === "k3") { return KIMI_K3_EFFORTS; } if (model.provider === "alibaba-token-plan" && model.id === "deepseek-v4-flash-0731") { return DEEPSEEK_V4_FLASH_0731_EFFORTS; } switch (parsedModel.family) { case "openai": return inferOpenAISupportedEfforts(parsedModel); case "gemini": return inferGeminiSupportedEfforts(parsedModel); case "anthropic": return inferAnthropicSupportedEfforts(parsedModel, model); case "unknown": return inferFallbackEfforts(model); } } function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] { if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) { return GPT_5_1_CODEX_MINI_EFFORTS; } if (semverGte(model.version, "5.6")) { return GPT_5_6_PLUS_EFFORTS; } if (semverGte(model.version, "5.2")) { return GPT_5_2_PLUS_EFFORTS; } return DEFAULT_REASONING_EFFORTS; } function inferGeminiSupportedEfforts(model: GeminiModel): readonly Effort[] { if (!semverGte(model.version, "3.0")) { return DEFAULT_REASONING_EFFORTS; } if (model.kind === "pro") { return GEMINI_3_PRO_EFFORTS; } if (semverGte(model.version, "3.7")) { return GEMINI_3_7_FLASH_EFFORTS; } return GEMINI_3_FLASH_EFFORTS; } function inferAnthropicSupportedEfforts( parsedModel: AnthropicModel, model: ApiModel, ): readonly Effort[] { if ( (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") && semverGte(parsedModel.version, "4.6") ) { if (parsedModel.kind === "fable") { // Fable exposes Anthropic's Messages-only xhigh preset; Bedrock // Converse lacks it (same split as Opus 4.7+ below). return model.api === "anthropic-messages" ? DEFAULT_REASONING_EFFORTS_WITH_XHIGH : DEFAULT_REASONING_EFFORTS; } if (anthropicModelHasRealXHighEffort(model)) { // Opus 4.7+ and Sonnet 5 expose both Anthropic's real xhigh and // max presets on the Messages API. return DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX; } if (parsedModel.kind === "opus") { // Opus 4.6 exposes max but not the newer xhigh literal. return DEFAULT_REASONING_EFFORTS_WITH_MAX; } return DEFAULT_REASONING_EFFORTS; } return inferFallbackEfforts(model); } function inferFallbackEfforts(model: ApiModel): readonly Effort[] { // Meta documents Muse Spark 1.2 as accepting the full minimal..xhigh // reasoning range. Keep that capability provider-independent so runtime // model discovery/merge cannot downgrade the bundled OpenRouter entry to // the generic openai-completions ceiling of `high`. if (model.id.split("/").at(-1)?.toLowerCase() === "muse-spark-1.2") { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } if (model.api === "anthropic-messages") { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } if (model.name.includes("deepseek-v4")) { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } if (model.api === "bedrock-converse-stream") { return DEFAULT_REASONING_EFFORTS; } if (model.api === "openai-completions") { if (model.provider === "omlx") { return [Effort.Low, Effort.Medium, Effort.High]; } const compat = resolveOpenAICompat(model as ApiModel<"openai-completions">); if (compat.thinkingFormat === "openai" && compat.supportsReasoningEffort) { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } return DEFAULT_REASONING_EFFORTS; } // OpenAI Responses APIs encode discrete effort levels, including xhigh. if (model.api === "openai-responses" || model.api === "openai-codex-responses") { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } return DEFAULT_REASONING_EFFORTS; } function inferThinkingControlMode( model: ApiModel, parsedModel: ParsedModel, ): ThinkingConfig["mode"] { switch (model.api) { case "google-generative-ai": case "google-gemini-cli": case "google-vertex": return parsedModel.family === "gemini" && semverGte(parsedModel.version, "3.0") && parsedModel.version.major === 3 ? "google-level" : "budget"; case "anthropic-messages": if (parsedModel.family === "anthropic") { if (semverGte(parsedModel.version, "4.6")) { return "anthropic-adaptive"; } if (semverGte(parsedModel.version, "4.5")) { return "anthropic-budget-effort"; } } return "budget"; case "bedrock-converse-stream": if (parsedModel.family === "anthropic") { if ( semverGte(parsedModel.version, "4.6") && (parsedModel.kind === "opus" || parsedModel.kind === "fable") ) { return "anthropic-adaptive"; } if (semverGte(parsedModel.version, "4.5")) { return "anthropic-budget-effort"; } } return "budget"; default: return "effort"; } } function parseKnownModel(modelId: string): ParsedModel { const canonicalId = getCanonicalModelId(modelId); return ( parseGeminiModel(canonicalId) ?? parseAnthropicModel(canonicalId) ?? parseOpenAIModel(canonicalId) ?? { family: "unknown", id: canonicalId } ); } const GEMINI_SUFFIX = "-preview"; function parseGeminiModel(modelId: string): GeminiModel | null { if (modelId.endsWith(GEMINI_SUFFIX)) { modelId = modelId.slice(0, -GEMINI_SUFFIX.length); } const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash)\b/.exec(modelId); if (!match) { return null; } const version = parseSemVer(match[1]); if (!version) { return null; } return { family: "gemini", kind: match[2] as GeminiKind, version }; } function parseAnthropicModel(modelId: string): AnthropicModel | null { const match = /claude-(opus|sonnet|fable)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId); if (!match) { return null; } const version = parseSemVer(match[2]); if (!version) { return null; } return { family: "anthropic", kind: match[1] as AnthropicKind, version }; } function parseOpenAIModel(modelId: string): OpenAIModel | null { const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|luna|mini|max|nano|sol|terra))?$/.exec( modelId, ); if (!match) { return null; } const version = parseSemVer(match[1]); if (!version) { return null; } return { family: "openai", variant: (match[2] as OpenAIVariant | undefined) ?? "base", version }; } function createSemVer(major: number, minor: number, patch = 0): SemVer { return { major, minor, patch }; } // extend this table if we need anything more than 9.10 const precomputeTable: Record = {}; for (let major = 0; major <= 9; major++) { for (let minor = 0; minor <= 10; minor++) { const version = createSemVer(major, minor, 0); precomputeTable[`${major}.${minor}`] = version; precomputeTable[`${major}-${minor}`] = version; } precomputeTable[`${major}`] = createSemVer(major, 0, 0); } function parseSemVer(version: string): SemVer | null { return precomputeTable[version] ?? null; } function semverGte(left: SemVer | string, right: SemVer | string): boolean { return compareSemVer(left, right) >= 0; } function semverEqual(left: SemVer | string, right: SemVer | string): boolean { return compareSemVer(left, right) === 0; } function compareSemVer(left: SemVer | string | null, right: SemVer | string | null): number { left = typeof left === "string" ? parseSemVer(left) : left; right = typeof right === "string" ? parseSemVer(right) : right; if (!left || !right) return (left ? 1 : 0) - (right ? 1 : 0); if (left.major !== right.major) { return left.major - right.major; } if (left.minor !== right.minor) { return left.minor - right.minor; } return left.patch - right.patch; } function getCanonicalModelId(modelId: string): string { const p = modelId.lastIndexOf("/"); return p !== -1 ? modelId.slice(p + 1) : modelId; }