/** * `resolveModelPolicy` — the single model-policy resolver. Layering, later * layers win: * * 1. unconditional per-API compat defaults; * 2. host-derived flags: URL/provider detection via `hosts.ts`, including * compound host×identity branches (keyed on {@link ModelIdentity} fields, * never on model-name matching); * 3. compat-cascade axes compiled from `rules/` (pure identity- and * provider-keyed policy lives there, not here); * 4. spec-authored sparse overrides (`applyCompatOverrides`), then the * spec-undefined fixups the legacy builders applied. * * Thinking metadata resolves after compat: explicit spec thinking wins, * cascade thinking axes drive the ladder/mode/wire facts, and identity-generic * fallbacks cover unmatched targets. */ import { Effort, THINKING_EFFORTS } from "../effort"; import { hostMatchesUrl, modelMatchesHost } from "../hosts"; import { withoutUnverifiedThinking } from "../model-thinking"; import type { Api, CatalogPolicyInputs, CompatOf, ModelSpec, OpenAICompat, ResolvedAnthropicCompat, ResolvedBedrockCompat, ResolvedDevinCompat, ResolvedGoogleCompat, ResolvedOllamaCompat, ResolvedOpenAICompat, ResolvedOpenAIResponsesCompat, ResolvedOpenAISharedCompat, ThinkingConfig, } from "../types"; import { isOfficialCodexApiUrl } from "../wire/codex"; import { isAnthropicSigningProxyUrl, isAzureAnthropicRoute, isBedrockAnthropicRoute, isOfficialAnthropicApiUrl, } from "./anthropic"; import { applyCompatOverrides } from "./apply"; import { API_COMPAT_RECORDS, AXES, type CompatRecordName } from "./axes"; import { hasModelScopedEffortsRule, resolveCascade } from "./cascade"; import { compareRevision, parseRevision, type Revision } from "./revision"; import { classifyModel, stripThinkingVariantSuffix } from "./taxonomy"; import type { ModelIdentity, RequestPolicy, ResolvedAxes, ResolveTarget } from "./types"; /** Result of resolving one model spec through the compat engine. */ export interface ResolvedModelPolicy { identity: ModelIdentity; compat: CompatOf; thinking: ThinkingConfig | undefined; /** Catalog-data axis assignments (longContext, priority, …) for generation. */ catalog: Record; /** Request-shaping directives for the selected upstream. */ request: RequestPolicy; } /** Request routing context is separate from model identity and deployment. */ export interface ResolveRoute { upstream?: string; } const REQUEST_AXIS_KEYS = Object.values(AXES) .filter(axis => axis.records?.includes("request")) .map(axis => axis.key); function resolveRequestPolicy(axes: ResolvedAxes): RequestPolicy { const policy: Record = {}; for (const key of REQUEST_AXIS_KEYS) { if (key in axes.wire) policy[key] = axes.wire[key]; } return policy as RequestPolicy; } // --------------------------------------------------------------------------- // Identity facts — every check reads ModelIdentity fields, never id strings. // --------------------------------------------------------------------------- /** Structured identity plus revision comparison helpers. */ class IdentityFacts { readonly revision: Revision | undefined; constructor(readonly identity: ModelIdentity) { this.revision = identity.revision === undefined ? undefined : parseRevision(identity.revision); } is(cls: string): boolean { return this.identity.class === cls; } family(...families: string[]): boolean { return this.identity.family !== undefined && families.includes(this.identity.family); } revGte(min: string): boolean { if (!this.revision) return false; const bound = parseRevision(min); return bound !== undefined && compareRevision(this.revision, bound) >= 0; } revMajor(): number | undefined { return this.revision?.[0]; } /** Kimi K2.7-Code / K3 lines whose native hosts mandate enabled thinking. */ get kimiMandatoryThinking(): boolean { return this.is("kimi") && this.family("k2.7-code", "k3"); } /** Adaptive-thinking Claude generation floor (Opus ≥ min; Sonnet/Fable/Mythos ≥ 5; Haiku ≥ 5.5). */ anthropicAdaptiveGenAtLeast(opusMin: string): boolean { if (!this.is("anthropic")) return false; if (this.family("opus")) return this.revGte(opusMin); if (this.family("sonnet", "fable", "mythos")) return this.revGte("5"); if (this.family("haiku")) return this.revGte("5.5"); return false; } } /** * The id a spec resolves identity and rules under: its own, unless that names * no model and the row sends a model that does on the wire. A Devin Fusion * pairing runs only its lead, so the lead's reviewed rules (cache-write rate, * long-context tier) apply to it at build time. */ function policyModelId(spec: CatalogPolicyInputs): string { const wireId = spec.requestModelId; if (wireId === undefined || wireId === spec.id) return spec.id; if (classifyModel(spec.provider, spec.id).class !== "unknown") return spec.id; return classifyModel(spec.provider, wireId).class === "unknown" ? spec.id : wireId; } function resolveIdentity(spec: CatalogPolicyInputs): ModelIdentity { // Strict on purpose: ambiguous identity is a rule-authoring defect surfaced // at build/CI time, never silently degraded to `unknown`. Discovery // normalization opts into leniency through `classifyModel` directly. return classifyModel(spec.provider, policyModelId(spec)); } // --------------------------------------------------------------------------- // Axis application // --------------------------------------------------------------------------- interface AxisKeyInfo { records: readonly CompatRecordName[]; } let keyRecords: Record | undefined; function getKeyRecords(): Record { if (!keyRecords) { keyRecords = {}; for (const directive in AXES) { const axis = AXES[directive]; if (axis.set === "wire" && axis.records) { keyRecords[axis.key] = { records: axis.records }; } } } return keyRecords; } /** * Assigns resolved wire axes onto a freshly-detected compat record. An axis * lands only when the model's API maps to one of the axis's declared records * — the axes table is the single applicability authority, so optional keys * absent from the detected baseline still land. */ function applyWireAxes(compat: object, wire: Record, api: Api): void { const records = API_COMPAT_RECORDS[api]; if (!records) return; const info = getKeyRecords(); for (const key in wire) { const meta = info[key]; if (!meta?.records.some(record => records.includes(record))) continue; Reflect.set(compat, key, wire[key]); } } /** * Preserves legacy per-key merge semantics for map-valued effort remaps: a * cascade-provided `reasoningEffortMap` is the detected base and sparse spec * overrides overlay it key by key (spec keys win, unseen keys survive). */ function overlayEffortMapAxis( compat: { reasoningEffortMap: Partial> }, axes: ResolvedAxes, specCompat: OpenAICompat | undefined, ): void { const axisMap = effortRecord(axes.wire.reasoningEffortMap); if (axisMap && specCompat?.reasoningEffortMap) { compat.reasoningEffortMap = { ...axisMap, ...specCompat.reasoningEffortMap }; } } function effortList(value: unknown): readonly Effort[] | undefined { if (!Array.isArray(value)) return undefined; const out: Effort[] = []; for (const entry of value) { const effort = THINKING_EFFORTS.find(candidate => candidate === entry); if (effort === undefined) return undefined; out.push(effort); } return out; } function effortValue(value: unknown): Effort | undefined { return THINKING_EFFORTS.find(candidate => candidate === value); } function objectPayload(value: unknown): object | undefined { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined; } function effortRecord(value: unknown): Partial> | undefined { if (typeof value !== "object" || value === null) return undefined; const out: Partial> = {}; let any = false; for (const key in value) { const effort = effortValue(key); const mapped: unknown = Reflect.get(value, key); if (effort === undefined || typeof mapped !== "string") continue; out[effort] = mapped; any = true; } return any ? out : undefined; } function effortNumberRecord(value: unknown): Partial> | undefined { if (typeof value !== "object" || value === null) return undefined; const out: Partial> = {}; let any = false; for (const key in value) { const effort = effortValue(key); const mapped: unknown = Reflect.get(value, key); if (effort === undefined || typeof mapped !== "number") continue; out[effort] = mapped; any = true; } return any ? out : undefined; } function thinkingMode(value: unknown): ThinkingConfig["mode"] | undefined { switch (value) { case "effort": case "budget": case "google-level": case "anthropic-adaptive": case "anthropic-budget-effort": return value; default: return undefined; } } // --------------------------------------------------------------------------- // Detected baselines (layers 1+2) — ported from the legacy builders with // pure identity/provider policy removed (rules own it). // --------------------------------------------------------------------------- const GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000; const LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS = 300_000; // Mechanism only: provider ids identify local/proxy endpoint shape, while // loopback URL detection handles custom endpoints; neither is model policy. const LOCAL_OPENAI_COMPAT_PROVIDERS: Record = { "llama.cpp": true, "lm-studio": true, vllm: true, ollama: true, }; const PROXY_OPENAI_COMPAT_PROVIDERS: Record = { litellm: true }; function hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean { if (!baseUrl) return false; let hostname: string; try { hostname = new URL(baseUrl).hostname.toLowerCase(); if (hostname.endsWith(".")) hostname = hostname.slice(0, -1); } catch { return false; } if ( hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1" || hostname === "[::1]" ) { return true; } if (hostname.startsWith("10.")) return true; if (hostname.startsWith("192.168.")) return true; if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)) return true; if (hostname.endsWith(".local")) return true; return false; } function resolveReasoningDisableMode( thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"], ): ResolvedOpenAISharedCompat["reasoningDisableMode"] { switch (thinkingFormat) { case "openrouter": return "openrouter-enabled-false"; case "zai": case "kimi": return "zai-thinking-disabled"; case "qwen": return "qwen-enable-thinking-false"; case "qwen-chat-template": return "qwen-template-false"; case "chat-template": return "chat-template-thinking-false"; default: return "lowest-effort"; } } /** Strict official-OpenAI check: provider id `openai` and an `api.openai.com` host. */ function isOfficialOpenAIEndpoint(provider: string, baseUrl: string): boolean { if (provider !== "openai") return false; if (!baseUrl) return true; try { return new URL(baseUrl).hostname === "api.openai.com"; } catch { return false; } } interface OpenAIDetection { facts: IdentityFacts; isZai: boolean; isZhipu: boolean; isMoonshotNative: boolean; isOpenCodeHost: boolean; isOpenCodeProvider: boolean; isDirectDeepseekApi: boolean; isDeepseekReasoning: boolean; isDirectDeepseekReasoning: boolean; isVenice: boolean; requiresEnabledThinking: boolean; isXiaomiMimo: boolean; isLocalOpenAICompatBackend: boolean; isLocalServingBackend: boolean; isOpenRouter: boolean; } function detectOpenAI( spec: ModelSpec<"openai-completions" | "openrouter">, facts: IdentityFacts, reasoningCapable: boolean, ): OpenAIDetection { const provider = spec.provider; const backendProvider = spec.providerType ?? provider; const baseUrl = spec.baseUrl; const hostModel = { provider, baseUrl }; const isZai = modelMatchesHost(hostModel, "zai"); const isZhipu = modelMatchesHost(hostModel, "zhipu"); const isMoonshotNative = modelMatchesHost(hostModel, "moonshotNative"); const isXiaomiHost = modelMatchesHost(hostModel, "xiaomi"); const isDirectDeepseekApi = modelMatchesHost(hostModel, "deepseekDirect"); const isDeepseekFamily = modelMatchesHost(hostModel, "deepseekFamily") || facts.is("deepseek"); const isDeepseekReasoning = isDeepseekFamily && reasoningCapable; const isLocalOpenAICompatBackend = PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true && (LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true || hasLocalLoopbackBaseUrl(baseUrl)); return { facts, isZai, isZhipu, isMoonshotNative, isOpenCodeHost: modelMatchesHost(hostModel, "opencode"), isOpenCodeProvider: provider === "opencode-go" || provider === "opencode-zen", isDirectDeepseekApi, isDeepseekReasoning, isDirectDeepseekReasoning: isDirectDeepseekApi && isDeepseekReasoning, isVenice: modelMatchesHost(hostModel, "venice"), requiresEnabledThinking: isMoonshotNative && facts.is("kimi") && facts.family("k2.7-code"), isXiaomiMimo: isXiaomiHost && facts.is("mimo"), isLocalOpenAICompatBackend, isLocalServingBackend: isLocalOpenAICompatBackend || hasLocalLoopbackBaseUrl(baseUrl), isOpenRouter: modelMatchesHost(hostModel, "openrouter"), }; } /** * Detected chat-completions baseline. Pure identity policy (Kimi max-token * behavior, DeepSeek reasoning-content contracts, Qwen dialects, sampling * restrictions, effort maps, markup healers, …) is rule-owned and absent * here; compound host×identity branches remain, keyed on identity fields. */ function detectOpenAICompat( spec: ModelSpec<"openai-completions" | "openrouter">, d: OpenAIDetection, reasoningCapable: boolean, ): ResolvedOpenAICompat { const provider = spec.provider; const baseUrl = spec.baseUrl; const hostModel = { provider, baseUrl }; const facts = d.facts; const isCerebras = modelMatchesHost(hostModel, "cerebras"); const isCerebrasHost = hostMatchesUrl(baseUrl, "cerebras"); const isKilo = modelMatchesHost(hostModel, "kilo"); const isAlibaba = modelMatchesHost(hostModel, "alibabaDashscope"); const isXiaomiHost = modelMatchesHost(hostModel, "xiaomi"); const isGrok = modelMatchesHost(hostModel, "xai"); const isMistral = modelMatchesHost(hostModel, "mistral"); const isGoogleAistudioOpenAI = hostMatchesUrl(baseUrl, "googleAistudio"); const isOpenAIHost = modelMatchesHost(hostModel, "openai"); const isAzureHost = modelMatchesHost(hostModel, "azureOpenAI"); const isVercelGateway = modelMatchesHost(hostModel, "vercelAIGateway"); const isTogether = modelMatchesHost(hostModel, "together"); const isFireworks = hostMatchesUrl(baseUrl, "fireworks"); const isGroqHost = modelMatchesHost(hostModel, "groq"); const isMiniMaxHost = modelMatchesHost(hostModel, "minimax"); const isQwenPortal = modelMatchesHost(hostModel, "qwenPortal"); const isMoonshotKimi = facts.is("kimi") && d.isMoonshotNative; const isMoonshotKimiK3 = isMoonshotKimi && facts.family("k3"); const usesMoonshotKimiPreservedThinking = isMoonshotKimi && facts.family("k2.6"); const isAnthropicModel = modelMatchesHost(hostModel, "anthropic") || facts.is("anthropic"); const isQwen = facts.is("qwen"); const isDeepseekFamily = modelMatchesHost(hostModel, "deepseekFamily") || facts.is("deepseek"); const supportsZaiReasoningEffort = (d.isZai || d.isZhipu) && facts.is("glm") && facts.revGte("5.2"); const isNonStandard = isCerebras || isGrok || isMistral || isGoogleAistudioOpenAI || hostMatchesUrl(baseUrl, "chutes") || hostMatchesUrl(baseUrl, "deepseekFamily") || isFireworks || isAlibaba || d.isZai || d.isZhipu || isKilo || isQwen || isXiaomiHost || d.isMoonshotNative || d.isOpenCodeHost; const useMaxTokens = isMistral || d.isMoonshotNative || d.isZai || d.isZhipu || hostMatchesUrl(baseUrl, "chutes") || isFireworks || d.isDirectDeepseekApi; const supportsPromptCacheBreakpoints = isOfficialOpenAIEndpoint(provider, baseUrl) && facts.is("openai") && facts.revGte("5.6"); const supportsMultipleSystemMessagesDefault = !isMiniMaxHost && !isAlibaba && !isQwenPortal && !isQwen && (isOpenAIHost || isAzureHost || d.isOpenRouter || isCerebras || isTogether || isFireworks || isGroqHost || isDeepseekFamily || isMistral || isGrok || d.isZai || d.isZhipu || provider === "github-copilot" || provider === "zenmux"); // URL-keyed watchdogs: they also cover custom provider ids pointed at these // hosts, which provider-keyed rules (`stream-idle-timeout-ms` in // classes/{glm,kimi,mimo}.kdl) cannot match. KDL rules override them. const streamIdleTimeoutMs = facts.is("glm") && facts.revGte("5") && !facts.family("vision") && (hostMatchesUrl(baseUrl, "zai") || hostMatchesUrl(baseUrl, "zhipu") || hostMatchesUrl(baseUrl, "opencode")) ? GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS : facts.is("mimo") && hostMatchesUrl(baseUrl, "xiaomi") ? 300_000 : reasoningCapable && facts.is("kimi") && (facts.family("k3") || facts.family("k2.7-code")) && hostMatchesUrl(baseUrl, "moonshotNative") ? 300_000 : reasoningCapable && facts.is("deepseek") && hostMatchesUrl(baseUrl, "deepseekDirect") ? 300_000 : d.isLocalServingBackend ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS : undefined; const wireModelIdMode: ResolvedOpenAISharedCompat["wireModelIdMode"] = hostMatchesUrl(baseUrl, "openrouter") ? "openrouter" : "raw"; // Provider and lineage formats are rule-owned; endpoint-only dialects remain here. const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = (facts.is("kimi") && !facts.family("k3") && hostMatchesUrl(baseUrl, "moonshotNative")) || hostMatchesUrl(baseUrl, "zai") || hostMatchesUrl(baseUrl, "zhipu") || (facts.is("mimo") && hostMatchesUrl(baseUrl, "xiaomi")) ? "zai" : hostMatchesUrl(baseUrl, "openrouter") ? "openrouter" : isQwen && hostMatchesUrl(baseUrl, "nvidia") ? "qwen-chat-template" : isQwen && (isFireworks || hostMatchesUrl(baseUrl, "venice")) ? "openai" : hostMatchesUrl(baseUrl, "alibabaDashscope") || isQwen ? "qwen" : "openai"; return { supportsStore: !isNonStandard, supportsDeveloperRole: isOpenAIHost || isAzureHost, supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault, supportsReasoningEffort: !isGrok && !d.isXiaomiMimo && (!(d.isZai || d.isZhipu) || supportsZaiReasoningEffort), trustExplicitThinkingOnly: undefined, supportsReasoningParams: true, supportsSamplingParams: true, supportsPenaltyAndStopParams: !(isGrok && reasoningCapable), reasoningEffortMap: {}, supportsUsageInStreaming: !isCerebrasHost, alwaysSendMaxTokens: facts.is("kimi"), disableReasoningOnForcedToolChoice: (facts.is("kimi") && !isMoonshotKimiK3) || isAnthropicModel, disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !d.isOpenRouter, disableReasoningWithTools: false, supportsToolChoice: !d.isDirectDeepseekReasoning, supportsForcedToolChoice: !d.requiresEnabledThinking && !(d.isOpenCodeHost && d.isDeepseekReasoning), supportsNamedToolChoice: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", requiresToolResultName: isMistral, requiresAssistantAfterToolResult: isMistral, requiresThinkingAsText: isMistral, requiresMistralToolIds: isMistral, thinkingFormat, kimiApiFormat: undefined, reasoningDisableMode: d.isVenice ? "venice-disable-thinking" : resolveReasoningDisableMode(thinkingFormat), omitReasoningEffort: false, includeEncryptedReasoning: true, filterReasoningHistory: d.isOpenRouter && isAnthropicModel, thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined, reasoningContentField: "reasoning_content", mistralReasoningContentParts: undefined, requiresReasoningContentForToolCalls: (facts.is("kimi") && !d.isOpenCodeProvider) || (isDeepseekFamily && reasoningCapable) || d.isXiaomiMimo || (d.isOpenRouter && reasoningCapable), requiresReasoningContentForAllAssistantTurns: ((isDeepseekFamily && reasoningCapable) || d.isXiaomiMimo) && !d.isOpenRouter, allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !reasoningCapable) && !d.isXiaomiMimo, // Keep the typed sparse override key present for DeepSeek proxy replay. syntheticReasoningContentFallback: undefined, replayReasoningContent: d.isLocalOpenAICompatBackend, qwenPreserveThinking: (thinkingFormat === "qwen" || thinkingFormat === "qwen-chat-template") && d.isLocalOpenAICompatBackend, // Template effort support is a reviewed backend × model contract in KDL. qwenTemplateReasoningEffort: false, requiresAssistantContentForToolCalls: facts.is("kimi") || d.isDirectDeepseekReasoning, cacheControlFormat: d.isOpenRouter && isAnthropicModel ? "anthropic" : undefined, supportsPromptCacheBreakpoints, promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined, openRouterRouting: undefined, vercelGatewayRouting: undefined, isOpenRouterHost: d.isOpenRouter, wireModelIdMode, isVercelGatewayHost: isVercelGateway, supportsStrictMode: hostMatchesUrl(baseUrl, "openai") || hostMatchesUrl(baseUrl, "azureOpenAI") || hostMatchesUrl(baseUrl, "cerebras") || hostMatchesUrl(baseUrl, "together") || hostMatchesUrl(baseUrl, "openrouter") || hostMatchesUrl(baseUrl, "deepseekFamily"), extraBody: undefined, toolStrictMode: isCerebrasHost ? "all_strict" : "mixed", toolSchemaFlavor: d.isMoonshotNative || facts.is("kimi") ? "moonshot-mfjs" : d.isLocalOpenAICompatBackend ? "grammar" : undefined, streamFirstEventTimeoutMs: d.isLocalServingBackend ? 0 : undefined, streamIdleTimeoutMs, stripDeepseekSpecialTokens: false, // Present as a key so models.yml `compat` can set it; unset unless a rule assigns it. stripThinkingHistory: undefined, // URL-derived like the Responses field; unset off the official OpenAI API. officialEndpoint: isOfficialOpenAIEndpoint(spec.provider, baseUrl) || undefined, streamMarkupHealingPattern: detectStreamMarkupHealing(spec.provider, facts, baseUrl), reasoningDeltasMayBeCumulative: false, emptyLengthFinishIsContextError: false, usesOpenAIToolCallIdLimit: false, promptCacheSessionHeader: detectPromptCacheSessionHeader(baseUrl), dropThinkingWhenReasoningEffort: false, nativeKimiK3Reasoning: false, zaiReasoningEffortDialect: false, clampOutputToModelMax: d.isLocalOpenAICompatBackend, stripImageInput: false, rejectRootObjectUnion: false, retryWithoutStrictOnGrammarError: false, supportsPromptCacheKey: false, }; } /** Worker-affinity header a host routes on to keep a conversation on its cached prefix. */ function detectPromptCacheSessionHeader( baseUrl: string | undefined, ): ResolvedOpenAISharedCompat["promptCacheSessionHeader"] { if (hostMatchesUrl(baseUrl, "xai")) return "x-grok-conv-id"; if (hostMatchesUrl(baseUrl, "morph")) return "x-session-id"; return undefined; } /** * Default leaked-markup healer. Kimi/DeepSeek dedicated grammars are keyed on * identity class; official OpenAI heals nothing; everything else defaults to * the generic `thinking` healer. DSML is DeepSeek's own tool-call grammar, so * every host serving a DeepSeek model gets it — gateways, local backends, and * custom providers alike. */ function detectStreamMarkupHealing( provider: string, facts: IdentityFacts, baseUrl: string, ): ResolvedOpenAISharedCompat["streamMarkupHealingPattern"] { // The dedicated Kimi grammar targets the K2 chat template; K3 and unranked // Kimi ids keep the generic healer, matching the census. const isKimiK2 = facts.is("kimi") && facts.identity.family?.startsWith("k2") === true; if (provider === "kimi-code" || provider === "moonshot" || isKimiK2) return "kimi"; if (facts.is("deepseek")) return "dsml"; if (isOfficialOpenAIEndpoint(provider, baseUrl)) return undefined; return "thinking"; } function fixupOpenAICompat( spec: ModelSpec<"openai-completions" | "openrouter">, compat: ResolvedOpenAICompat, d: OpenAIDetection, axes: ResolvedAxes, ): void { const deepseekThinking = compat.extraBody?.thinking; if ( d.isDirectDeepseekReasoning && typeof deepseekThinking === "object" && deepseekThinking !== null && "type" in deepseekThinking && deepseekThinking.type === "enabled" ) { const extraBody = { ...compat.extraBody }; delete extraBody.thinking; compat.extraBody = Object.keys(extraBody).length > 0 ? extraBody : undefined; } if (spec.compat?.reasoningDisableMode === undefined && !("reasoningDisableMode" in axes.wire)) { compat.reasoningDisableMode = d.requiresEnabledThinking ? "omit" : d.isDirectDeepseekReasoning ? "zai-thinking-disabled" : d.isVenice ? "venice-disable-thinking" : resolveReasoningDisableMode(compat.thinkingFormat); } if ( spec.compat?.omitReasoningEffort === undefined && !("omitReasoningEffort" in axes.wire) && !compat.supportsReasoningEffort ) { compat.omitReasoningEffort = true; } const axisWhenThinking = compatReasoning(spec, axes) ? objectPayload(axes.wire.whenThinking) : undefined; const whenThinkingPolicy = spec.compat?.whenThinking ?? axisWhenThinking ?? (d.isDirectDeepseekReasoning ? { extraBody: { ...compat.extraBody, thinking: { type: "enabled" } } } : undefined); if (whenThinkingPolicy) { const variant: ResolvedOpenAICompat = { ...compat, whenThinking: undefined }; applyCompatOverrides(variant, whenThinkingPolicy); if (Reflect.get(whenThinkingPolicy, "reasoningDisableMode") === undefined) { variant.reasoningDisableMode = d.isVenice ? "venice-disable-thinking" : resolveReasoningDisableMode(variant.thinkingFormat); } if (Reflect.get(whenThinkingPolicy, "omitReasoningEffort") === undefined && !variant.supportsReasoningEffort) { variant.omitReasoningEffort = true; } compat.whenThinking = variant; } else { compat.whenThinking = undefined; } } function resolveOpenAICompletionsPolicy( spec: ModelSpec<"openai-completions" | "openrouter">, facts: IdentityFacts, axes: ResolvedAxes, ): ResolvedOpenAICompat { const reasoningCapable = compatReasoning(spec, axes); const d = detectOpenAI(spec, facts, reasoningCapable); const compat = detectOpenAICompat(spec, d, reasoningCapable); applyWireAxes(compat, axes.wire, "openai-completions"); applyCompatOverrides(compat, spec.compat); overlayEffortMapAxis(compat, axes, spec.compat); fixupOpenAICompat(spec, compat, d, axes); return compat; } /** Detected Responses-API baseline (ported from `buildOpenAIResponsesCompat`). */ function resolveOpenAIResponsesPolicy( spec: ModelSpec<"openai-responses" | "azure-openai-responses" | "openai-codex-responses" | "openrouter">, facts: IdentityFacts, axes: ResolvedAxes, api: Api, ): ResolvedOpenAIResponsesCompat { const baseUrl = spec.baseUrl ?? ""; const provider = spec.provider; const backendProvider = spec.providerType ?? provider; const hostModel = { provider, baseUrl }; const isAzure = modelMatchesHost(hostModel, "azureOpenAI"); const isOpenRouter = modelMatchesHost(hostModel, "openrouter"); const isOpenAIUrl = hostMatchesUrl(baseUrl, "openai"); const isVercelGateway = modelMatchesHost(hostModel, "vercelAIGateway"); const isXaiHost = modelMatchesHost(hostModel, "xai"); const supportsPromptCacheBreakpoints = isOfficialOpenAIEndpoint(provider, baseUrl) && facts.is("openai") && facts.revGte("5.6"); const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = isOpenRouter ? "openrouter" : "openai"; const reasoningCapable = compatReasoning(spec, axes); const isLocalServingBackend = (PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true && LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true) || hasLocalLoopbackBaseUrl(baseUrl); const isAnthropicModel = facts.is("anthropic"); const isDeepseekFamily = facts.is("deepseek"); const compat: ResolvedOpenAIResponsesCompat = { supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"), supportsStrictMode: isAzure || hostMatchesUrl(baseUrl, "openai") || hostMatchesUrl(baseUrl, "azureOpenAI") || hostMatchesUrl(baseUrl, "cerebras") || hostMatchesUrl(baseUrl, "together") || hostMatchesUrl(baseUrl, "openrouter") || hostMatchesUrl(baseUrl, "deepseekFamily"), supportsReasoningEffort: !isXaiHost, trustExplicitThinkingOnly: undefined, supportsLongPromptCacheRetention: isOpenAIUrl, supportsPromptCacheBreakpoints, promptCacheBreakpointTtl: supportsPromptCacheBreakpoints ? "30m" : undefined, // Provider ids are rule-owned; the URL arm keeps custom Azure endpoints paired. strictResponsesPairing: hostMatchesUrl(baseUrl, "azureOpenAI"), // Azure's provider id alone only implies support while its endpoint is // resolved at runtime; an explicit non-Azure baseUrl is a proxy, like Codex. supportsImageDetailOriginal: isOpenAIUrl || hostMatchesUrl(baseUrl, "azureOpenAI") || (isAzure && !baseUrl) || hostMatchesUrl(baseUrl, "openaiCodex"), supportsReasoningSummary: !isXaiHost, statefulResponses: undefined, supportsAllTurnsReasoningContext: false, supportsConfigurationUpdate: false, supportsSteering: false, requiresReasoningOffJuiceInstruction: false, stripImageInput: false, reasoningEffortMap: {}, supportsReasoningParams: true, supportsSamplingParams: true, supportsPenaltyAndStopParams: !isXaiHost, thinkingFormat, reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat), omitReasoningEffort: false, includeEncryptedReasoning: true, filterReasoningHistory: isOpenRouter && isAnthropicModel, disableReasoningOnForcedToolChoice: facts.is("kimi"), disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter, disableReasoningWithTools: false, supportsToolChoice: true, supportsForcedToolChoice: true, supportsNamedToolChoice: true, reasoningContentField: "reasoning_content", requiresReasoningContentForToolCalls: (facts.is("kimi") || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) && reasoningCapable, requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter, allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable, syntheticReasoningContentFallback: undefined, replayReasoningContent: false, qwenPreserveThinking: false, qwenTemplateReasoningEffort: false, requiresThinkingAsText: false, requiresMistralToolIds: false, requiresToolResultName: false, requiresAssistantAfterToolResult: false, requiresAssistantContentForToolCalls: facts.is("kimi"), openRouterRouting: undefined, vercelGatewayRouting: undefined, isOpenRouterHost: isOpenRouter, isVercelGatewayHost: isVercelGateway, wireModelIdMode: isOpenRouter ? "openrouter" : "raw", toolSchemaFlavor: facts.is("kimi") ? "moonshot-mfjs" : undefined, alwaysSendMaxTokens: facts.is("kimi"), clampOutputToModelMax: PROXY_OPENAI_COMPAT_PROVIDERS[backendProvider] !== true && (LOCAL_OPENAI_COMPAT_PROVIDERS[backendProvider] === true || hasLocalLoopbackBaseUrl(baseUrl)), supportsObfuscationOptOut: isOpenAIUrl || provider === "openai", storeResponses: false, connectionBoundNativeHistory: false, // The Codex backend is official at the URL its requests default to when `baseUrl` is unset, and at paths // under it such as `/codex` and `/codex/responses`, which its requests resolve to the same endpoint. officialEndpoint: isOfficialOpenAIEndpoint(provider, baseUrl) || (api === "openai-codex-responses" && isOfficialCodexApiUrl(baseUrl)), harmonyLeakMitigation: false, rejectRootObjectUnion: false, retryWithoutStrictOnGrammarError: false, cacheControlFormat: isOpenRouter && isAnthropicModel ? "anthropic" : undefined, stripDeepseekSpecialTokens: false, streamMarkupHealingPattern: detectStreamMarkupHealing(provider, facts, baseUrl), reasoningDeltasMayBeCumulative: false, emptyLengthFinishIsContextError: false, usesOpenAIToolCallIdLimit: false, promptCacheSessionHeader: detectPromptCacheSessionHeader(baseUrl), streamFirstEventTimeoutMs: isLocalServingBackend ? 0 : spec.compat?.streamFirstEventTimeoutMs, streamIdleTimeoutMs: isLocalServingBackend ? LOCAL_OPENAI_COMPAT_STREAM_IDLE_TIMEOUT_MS : spec.compat?.streamIdleTimeoutMs, }; applyWireAxes(compat, axes.wire, api); applyCompatOverrides(compat, spec.compat); overlayEffortMapAxis(compat, axes, spec.compat); if (isXaiHost && "reasoningEffortMap" in axes.wire) { // The rules carry the canonical first-party xAI effort map; re-assert it // over stale spec/cache overrides and drop clamp keys the canonical map // no longer declares (xhigh-capable SKUs keep xhigh unmapped). const canonical = effortRecord(axes.wire.reasoningEffortMap) ?? {}; compat.reasoningEffortMap = { ...compat.reasoningEffortMap, ...canonical }; for (const key of [Effort.XHigh, Effort.Max]) { if (!(key in canonical)) delete compat.reasoningEffortMap[key]; } } if (spec.compat?.reasoningDisableMode === undefined && !("reasoningDisableMode" in axes.wire)) { compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat); } if ( spec.compat?.omitReasoningEffort === undefined && !("omitReasoningEffort" in axes.wire) && !compat.supportsReasoningEffort ) { compat.omitReasoningEffort = true; } if ( provider === "xai-oauth" && axes.wire.supportsReasoningEffort === true && spec.compat?.supportsReasoningEffort !== false ) { // Stale cached rows written before a SKU joined the effort-capable // rules still carry omitReasoningEffort; the rules are the live wire // contract. compat.supportsReasoningEffort = true; compat.omitReasoningEffort = false; } return compat; } type ResponsesOnlyCompat = Omit; function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat { return { supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention, strictResponsesPairing: compat.strictResponsesPairing, supportsImageDetailOriginal: compat.supportsImageDetailOriginal, supportsObfuscationOptOut: compat.supportsObfuscationOptOut, storeResponses: compat.storeResponses, toolNamePrefix: compat.toolNamePrefix, connectionBoundNativeHistory: compat.connectionBoundNativeHistory, supportsAllTurnsReasoningContext: compat.supportsAllTurnsReasoningContext, supportsConfigurationUpdate: compat.supportsConfigurationUpdate, supportsSteering: compat.supportsSteering, officialEndpoint: compat.officialEndpoint, harmonyLeakMitigation: compat.harmonyLeakMitigation, cacheControlFormat: compat.cacheControlFormat, requiresReasoningOffJuiceInstruction: compat.requiresReasoningOffJuiceInstruction, supportsReasoningSummary: compat.supportsReasoningSummary, statefulResponses: compat.statefulResponses, isVercelGatewayHost: compat.isVercelGatewayHost, } satisfies ResponsesOnlyCompat; } function resolveAnthropicPolicy( spec: ModelSpec<"anthropic-messages">, facts: IdentityFacts, axes: ResolvedAxes, ): ResolvedAnthropicCompat { const baseUrl = spec.baseUrl; const official = isOfficialAnthropicApiUrl(baseUrl); const isCopilot = modelMatchesHost(spec, "githubCopilot"); const isZenmux = modelMatchesHost(spec, "zenmux"); const requiresThinkingEnabled = modelMatchesHost(spec, "moonshotNative") && facts.kimiMandatoryThinking; const bedrockMessagesApi = isBedrockAnthropicRoute(baseUrl); // Both routes reject the top-level tool `strict` field. const rejectsStrictTools = isAzureAnthropicRoute(baseUrl) || bedrockMessagesApi; const signingEndpoint = official || isCopilot || isZenmux || isAnthropicSigningProxyUrl(baseUrl); const compat: ResolvedAnthropicCompat = { officialEndpoint: official, signingEndpoint, supportsContextManagement: true, supportsServerCompaction: false, supportsMessageThreads: false, firstPartyProvider: false, honorsClaudeCodeBetaKillSwitch: false, supportsOutputEffort: true, disableStrictTools: rejectsStrictTools, // Present as a key so models.yml `compat` can set it; unset unless detected. bedrockMessagesApi: bedrockMessagesApi || undefined, disableAdaptiveThinking: false, allowAnthropicHeaderOverrides: false, supportsEagerToolInputStreaming: official, supportsLongCacheRetention: official, supportsMidConversationSystem: official && !facts.family("sonnet") && facts.anthropicAdaptiveGenAtLeast("4.8"), supportsTurnScopedSystem: false, supportsMidConversationToolChanges: false, supportsPerMessageEffort: false, supportsThinkingBindingControls: false, supportsBetweenToolsThinking: false, supportsForcedToolChoice: !requiresThinkingEnabled && !facts.family("fable", "mythos"), supportsSamplingParams: true, requiresToolResultId: false, requiresThinkingEnabled, requiresThinkingPresence: false, replayUnsignedThinking: !signingEndpoint && (compatReasoning(spec, axes) || modelMatchesHost(spec, "deepseekFamily")), escapeBuiltinToolNames: false, injectClaudeCodeInstruction: true, stripImageInput: false, // Unset means "canonical Anthropic image contract"; the request builder // owns those numbers. The keys must exist for a KDL rule or a // user-authored `compat` block to override them. maxImageDimension: undefined, maxImagePayloadBytes: undefined, // Present as keys so models.yml `compat` can set them; unset unless a rule assigns them. stripThinkingHistory: undefined, fastMode: undefined, disabledThinking: undefined, streamIdleTimeoutMs: spec.compat?.streamIdleTimeoutMs, }; applyWireAxes(compat, axes.wire, "anthropic-messages"); applyCompatOverrides(compat, spec.compat); return compat; } const BEDROCK_REASONING_STREAM_IDLE_TIMEOUT_MS = 600_000; function resolveBedrockPolicy(spec: ModelSpec<"bedrock-converse-stream">, axes: ResolvedAxes): ResolvedBedrockCompat { // Prompt-cache checkpoint tables are rule-owned (class/family/revision // rules under `on "amazon-bedrock"`); the baseline is the conservative // no-checkpoint shape. const compat: ResolvedBedrockCompat = { promptCacheMode: "none", supportsServerCompaction: false, supportsLongPromptCacheRetention: false, promptCacheMinimumTokens: 0, promptCacheMaximumCheckpoints: 0, supportsForcedToolChoice: true, // Opt-out: every prefix-bound Claude line on Converse takes the controls, // including custom Bedrock-wire providers; KDL or models.yml sets `false`. supportsThinkingBindingControls: true, }; // Reasoning capability is a mechanism gate; adaptive-lineage duration is rule-owned. compat.streamIdleTimeoutMs = compatReasoning(spec, axes) ? BEDROCK_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined; applyWireAxes(compat, axes.wire, "bedrock-converse-stream"); applyCompatOverrides(compat, spec.compat); return compat; } function resolveOllamaPolicy(spec: ModelSpec<"ollama-chat">, axes: ResolvedAxes): ResolvedOllamaCompat { const compat: ResolvedOllamaCompat = { stripThinkingHistory: false }; applyWireAxes(compat, axes.wire, "ollama-chat"); applyCompatOverrides(compat, spec.compat); return compat; } function resolveDevinPolicy(spec: ModelSpec<"devin-agent">, axes: ResolvedAxes): ResolvedDevinCompat { const compat: ResolvedDevinCompat = { // Rule-owned: `on-api "devin-agent"` in providers/devin.kdl opts in. trustExplicitThinkingOnly: false, modelRouter: false, supportsParallelToolCalls: false, }; applyWireAxes(compat, axes.wire, "devin-agent"); applyCompatOverrides(compat, spec.compat); return compat; } function resolveGooglePolicy( spec: ModelSpec<"google-generative-ai" | "google-vertex" | "google-gemini-cli">, axes: ResolvedAxes, ): ResolvedGoogleCompat { const compat: ResolvedGoogleCompat = { supportsFunctionPartId: false, requiresSkipThoughtSignature: false, requiresSkipThoughtSignatureOnFirstFunctionCall: false, dropUnsignedThinking: false, ccaLegacyParametersSchema: false, multimodalFunctionResponse: false, flashStreamLeakWorkaround: false, claudeThinkingBetaHeader: false, antigravityClaudeToolMode: false, stripImageInput: false, // Unset means "no image contract": images go out as sent. Keys exist so // a KDL rule or a user `compat` block can declare one. maxImageDimension: undefined, maxImagePayloadBytes: undefined, // Unset keeps the bundled catalog rows byte-identical; the key must exist for `compat` overrides to apply. base64SkipThoughtSignature: undefined, }; applyWireAxes(compat, axes.wire, spec.api); applyCompatOverrides(compat, spec.compat); return compat; } // --------------------------------------------------------------------------- // Thinking resolution // --------------------------------------------------------------------------- const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High]; const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [...DEFAULT_REASONING_EFFORTS, Effort.XHigh]; function omitsWireReasoningEffort(api: Api, compat: CompatOf): boolean { if (api !== "openai-responses" && api !== "openai-codex-responses" && api !== "azure-openai-responses") { return false; } return compat !== undefined && "supportsReasoningEffort" in compat && compat.supportsReasoningEffort === false; } function readCompatEffortMap(compat: CompatOf): Partial> | undefined { if (compat === undefined || !("reasoningEffortMap" in compat)) return undefined; const map = compat.reasoningEffortMap; return map && Object.keys(map).length > 0 ? map : undefined; } /** * URL-keyed Fireworks effort remap: covers custom provider ids pointed at the * Fireworks host, which the `fireworks`/`firepass` `thinking-effort-map` * rules cannot match. Rule and configured maps override it. */ const FIREWORKS_THINKING_EFFORT_MAP: Readonly>> = { [Effort.Minimal]: "none", }; /** Identity/API-derived default control mode; `thinking-mode` rules override. */ function defaultThinkingMode(spec: ModelSpec, facts: IdentityFacts): ThinkingConfig["mode"] { switch (spec.api) { case "google-generative-ai": case "google-gemini-cli": case "google-vertex": // Gemini 3+ `google-level` is a `thinking-mode` rule in classes/gemini.kdl. return "budget"; case "anthropic-messages": if (facts.is("anthropic")) { // Haiku stays on budget thinking until 5.5, its first adaptive generation. if (facts.revGte(facts.family("haiku") ? "5.5" : "4.6")) return "anthropic-adaptive"; if (facts.family("opus") && facts.revGte("4.5")) return "anthropic-budget-effort"; } return "budget"; case "bedrock-converse-stream": if (facts.is("anthropic")) { if (facts.anthropicAdaptiveGenAtLeast("4.6")) return "anthropic-adaptive"; if (facts.family("opus") && facts.revGte("4.5")) return "anthropic-budget-effort"; } if (facts.is("openai")) return "effort"; return "budget"; default: return "effort"; } } /** API/compat-generic fallback ladder for targets no rule covered. */ function fallbackEfforts(spec: ModelSpec, compat: CompatOf): readonly Effort[] { if (spec.api === "anthropic-messages") return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; if (spec.api === "bedrock-converse-stream") return DEFAULT_REASONING_EFFORTS; if ( (spec.api === "openai-completions" || spec.api === "openrouter") && compat !== undefined && "thinkingFormat" in compat ) { if (compat.thinkingFormat === "openai" && compat.supportsReasoningEffort) { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } return DEFAULT_REASONING_EFFORTS; } if ( spec.api === "openai-responses" || spec.api === "openai-codex-responses" || spec.api === "azure-openai-responses" ) { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } return DEFAULT_REASONING_EFFORTS; } function filterEffortMap( map: Partial>, efforts: readonly Effort[], ): Partial> | undefined { let filtered: Partial> | undefined; for (const effort of efforts) { const mapped = map[effort]; if (mapped === undefined) continue; filtered ??= {}; filtered[effort] = mapped; } return filtered; } interface RuleThinking { mode?: ThinkingConfig["mode"]; efforts?: readonly Effort[]; defaultLevel?: Effort; effortMap?: Partial>; effortBudgets?: Partial>; requiresEffort?: boolean; suppressWhenOff?: boolean; supportsDisplay?: boolean; prefixBinding?: boolean; upgradeNeutral?: boolean; unverified?: boolean; } function readRuleThinking(axes: ResolvedAxes): RuleThinking { const raw = axes.thinking; const out: RuleThinking = {}; const mode = thinkingMode(raw.mode); if (mode !== undefined) out.mode = mode; const efforts = effortList(raw.efforts); if (efforts !== undefined) out.efforts = efforts; const defaultLevel = effortValue(raw.defaultLevel); if (defaultLevel !== undefined) out.defaultLevel = defaultLevel; const effortMap = effortRecord(raw.effortMap); if (effortMap !== undefined) out.effortMap = effortMap; const effortBudgets = effortNumberRecord(raw.effortBudgets); if (effortBudgets !== undefined) out.effortBudgets = effortBudgets; if (typeof raw.requiresEffort === "boolean") out.requiresEffort = raw.requiresEffort; if (typeof raw.suppressWhenOff === "boolean") out.suppressWhenOff = raw.suppressWhenOff; if (typeof raw.supportsDisplay === "boolean") out.supportsDisplay = raw.supportsDisplay; if (typeof raw.prefixBinding === "boolean") out.prefixBinding = raw.prefixBinding; if (typeof raw.upgradeNeutral === "boolean") out.upgradeNeutral = raw.upgradeNeutral; if (typeof raw.unverified === "boolean") out.unverified = raw.unverified; return out; } /** * Compat-time reasoning capability. `axes.reasoning` also promotes targets on * reviewed effort corrections (the cascade's thinking-axis gate), but compat * may only be repaired where the matching contract opted in with * `thinking-upgrade-neutral`; everywhere else a spec that reports no reasoning * stays the authoritative capability surface. */ function compatReasoning(spec: ModelSpec, axes: ResolvedAxes): boolean { if (spec.reasoning) return true; return axes.reasoning && readRuleThinking(axes).upgradeNeutral === true; } /** Identity-derived `requiresEffort` default (mandatory-reasoning lineages). */ function impliesMandatoryReasoning(facts: IdentityFacts, modelId: string): boolean { if (facts.identity.thinkingVariant) return true; // Thinking-variant orphans: a bounded pair token anywhere in the id // (`runtime-thinking-model`, `qwen3-…-thinking-2507`) names the // reasoning-locked sibling even when it is not a collapse suffix. if (stripThinkingVariantSuffix(modelId) !== undefined) return true; return false; } function isQwenTemplateReasoningEffortCompat(compat: CompatOf): boolean { return ( compat !== undefined && "qwenTemplateReasoningEffort" in compat && compat.qwenTemplateReasoningEffort === true ); } function resolveThinkingPolicy( spec: ModelSpec, facts: IdentityFacts, axes: ResolvedAxes, compat: CompatOf, ): ThinkingConfig | undefined { const rule = readRuleThinking(axes); const explicitThinking = spec.thinking !== undefined && Array.isArray(spec.thinking.efforts) && spec.thinking.efforts.length > 0 ? spec.thinking : undefined; // An explicit wire vocabulary is authoritative when discovery reports no // reasoning (e.g. Synthetic's `none`-only off-switch): reviewed KDL must // not re-expand it into an unadvertised ladder. Absent metadata is // repaired only where KDL opts in with `thinking-upgrade-neutral` // alongside a reviewed `thinking-efforts` ladder (the cascade upgrade for // stale source capability data). On deployments whose discovery reports no // capabilities (`thinking-unverified`), an unclassified neutral spec — nothing // says whether it reasons — gets an unverified ladder the user may opt into. const unverified = !compatReasoning(spec, axes); if ( unverified && (explicitThinking !== undefined || facts.identity.class !== "unknown" || rule.unverified !== true) ) { return undefined; } if (omitsWireReasoningEffort(spec.api, compat)) return undefined; if (explicitThinking !== undefined) { return fillExplicitThinking(spec, facts, compat, explicitThinking, rule); } if (compat !== undefined && "trustExplicitThinkingOnly" in compat && compat.trustExplicitThinkingOnly === true) { return undefined; } if (rule.efforts?.length === 0) return undefined; const config: ThinkingConfig = { mode: rule.mode ?? defaultThinkingMode(spec, facts), efforts: rule.efforts ?? fallbackEfforts(spec, compat), }; if (config.efforts.length === 0) { throw new Error(`Model ${spec.provider}/${spec.id} resolved to an empty thinking range`); } const effortMap = mergeEffortMap(spec, rule.effortMap, compat, config.efforts); if (effortMap !== undefined) config.effortMap = effortMap; if (unverified) { // Off-by-default opt-in: no default level, mandatory effort, or wire off-switch. config.unverified = true; return config; } if (rule.defaultLevel !== undefined) config.defaultLevel = rule.defaultLevel; if (rule.effortBudgets !== undefined) config.effortBudgets = rule.effortBudgets; const supportsDisplay = rule.supportsDisplay ?? defaultSupportsDisplay(spec, facts); if (supportsDisplay) config.supportsDisplay = true; if (rule.prefixBinding) config.prefixBinding = true; const requiresEffort = rule.requiresEffort ?? (impliesMandatoryReasoning(facts, spec.id) || isQwenTemplateReasoningEffortCompat(compat)); if (requiresEffort) config.requiresEffort = true; if (rule.suppressWhenOff) config.suppressWhenOff = true; return config; } /** * Displayable-thinking default: an api-conditioned lineage fact (Anthropic * wire dialects streaming adaptive Claude 4.7+ thinking) that KDL cannot * express — cascade selectors have no api dimension. `thinking-supports-display` * rules override per deployment. */ function defaultSupportsDisplay(spec: ModelSpec, facts: IdentityFacts): boolean { return ( (spec.api === "anthropic-messages" || spec.api === "bedrock-converse-stream") && facts.anthropicAdaptiveGenAtLeast("4.7") ); } function mergeEffortMap( spec: ModelSpec, ruleMap: Partial> | undefined, compat: CompatOf, efforts: readonly Effort[], ): Partial> | undefined { const detected = (spec.api === "openai-completions" || spec.api === "openrouter") && modelMatchesHost({ provider: spec.provider, baseUrl: spec.baseUrl ?? "" }, "fireworks") ? FIREWORKS_THINKING_EFFORT_MAP : undefined; const configured = readCompatEffortMap(compat); if (detected === undefined && ruleMap === undefined && configured === undefined) return undefined; return filterEffortMap({ ...detected, ...ruleMap, ...configured }, efforts); } /** * Backfill missing wire/default fields onto explicit thinking metadata. * Explicit spec thinking is layer 4 of the resolution order: its ladder and * every explicitly-set field always win; only absent fields are filled from * the rules and identity-derived defaults. */ function fillExplicitThinking( spec: ModelSpec, facts: IdentityFacts, compat: CompatOf, thinking: ThinkingConfig, rule: RuleThinking, ): ThinkingConfig { const effortMap = thinking.effortMap === undefined ? mergeEffortMap(spec, rule.effortMap, compat, thinking.efforts) : undefined; const needsDisplay = thinking.supportsDisplay === undefined && (rule.supportsDisplay ?? defaultSupportsDisplay(spec, facts)); const needsRequiresEffort = thinking.requiresEffort === undefined && (rule.requiresEffort ?? (impliesMandatoryReasoning(facts, spec.id) || isQwenTemplateReasoningEffortCompat(compat))); const needsDefaultLevel = thinking.defaultLevel === undefined && rule.defaultLevel !== undefined; const needsPrefixBinding = thinking.prefixBinding === undefined && rule.prefixBinding === true; const needsEffortBudgets = thinking.effortBudgets === undefined && rule.effortBudgets !== undefined; if ( effortMap === undefined && !needsDisplay && !needsRequiresEffort && !needsDefaultLevel && !needsPrefixBinding && !needsEffortBudgets ) { return thinking; } const filled: ThinkingConfig = { ...thinking }; if (effortMap !== undefined) filled.effortMap = effortMap; if (needsDisplay) filled.supportsDisplay = true; if (needsDefaultLevel && rule.defaultLevel !== undefined) filled.defaultLevel = rule.defaultLevel; if (needsRequiresEffort) filled.requiresEffort = true; if (needsPrefixBinding) filled.prefixBinding = true; if (needsEffortBudgets) filled.effortBudgets = rule.effortBudgets; return filled; } // --------------------------------------------------------------------------- // Entry // --------------------------------------------------------------------------- function buildResolveTarget( spec: CatalogPolicyInputs, identity: ModelIdentity, providerType = spec.providerType ?? spec.provider, ): ResolveTarget { const target: ResolveTarget = { provider: providerType, api: spec.api, class: identity.class, model: policyModelId(spec), reasoning: Boolean(spec.reasoning), }; if (identity.family !== undefined) target.family = identity.family; if (identity.revision !== undefined) target.revision = identity.revision; return target; } function specUsesApi(spec: ModelSpec, api: TApi): spec is ModelSpec { return spec.api === api; } /** Resolve the request adapter assigned to a discovery backend before materialization. */ export function resolveDiscoveryApi(spec: ModelSpec, providerType: string): Api { const identity = resolveIdentity(spec); const discoveryApi = resolveCascade(buildResolveTarget(spec, identity, providerType)).catalog.discoveryApi; return typeof discoveryApi === "string" ? discoveryApi : spec.api; } /** * Catalog-data axis assignments for one model spec — the `catalog` field of * {@link resolveModelPolicy} without resolving the compat and thinking * policies, which dominate its cost on catalog-wide scans. */ export function resolveCatalogAxes(spec: CatalogPolicyInputs, route?: ResolveRoute): Record { const target = buildResolveTarget(spec, resolveIdentity(spec)); target.upstream = route?.upstream; return resolveCascade(target).catalog; } /** * Resolves the full policy surface for one model spec: structured identity, * complete compat record, thinking metadata, and catalog-data corrections. */ export function resolveModelPolicy( spec: ModelSpec, route?: ResolveRoute, ): ResolvedModelPolicy; export function resolveModelPolicy(input: ModelSpec, route?: ResolveRoute): ResolvedModelPolicy { const spec = withoutUnverifiedThinking(input); const identity = resolveIdentity(spec); const facts = new IdentityFacts(identity); const target = buildResolveTarget(spec, identity); target.upstream = route?.upstream; const axes = resolveCascade(target); let compat: CompatOf; if (specUsesApi(spec, "openrouter")) { const chat = resolveOpenAICompletionsPolicy(spec, facts, axes); const responses = resolveOpenAIResponsesPolicy(spec, facts, axes, "openrouter"); compat = { ...chat, ...pickResponsesOnly(responses) }; } else if (specUsesApi(spec, "openai-completions")) { compat = resolveOpenAICompletionsPolicy(spec, facts, axes); } else if ( specUsesApi(spec, "openai-responses") || specUsesApi(spec, "azure-openai-responses") || specUsesApi(spec, "openai-codex-responses") ) { compat = resolveOpenAIResponsesPolicy(spec, facts, axes, spec.api); } else if (specUsesApi(spec, "anthropic-messages")) { compat = resolveAnthropicPolicy(spec, facts, axes); } else if (specUsesApi(spec, "bedrock-converse-stream")) { compat = resolveBedrockPolicy(spec, axes); } else if (specUsesApi(spec, "ollama-chat")) { compat = resolveOllamaPolicy(spec, axes); } else if (specUsesApi(spec, "devin-agent")) { compat = resolveDevinPolicy(spec, axes); } else if ( specUsesApi(spec, "google-generative-ai") || specUsesApi(spec, "google-vertex") || specUsesApi(spec, "google-gemini-cli") ) { compat = resolveGooglePolicy(spec, axes); } else { compat = undefined; } return { identity, compat, thinking: resolveThinkingPolicy(spec, facts, axes, compat), catalog: axes.catalog, request: resolveRequestPolicy(axes), }; } /** * Whether reviewed rules know THIS model's effort ladder, as opposed to it * inheriting a provider-wide default or {@link resolveThinkingPolicy} falling * through to the neutral wire ladder. * * Discovery uses this to tell "omp knows this model's tiers" apart from "omp * is guessing them", so catalog-published tiers can correct the guess without * ever overriding reviewed knowledge. */ export function hasModelScopedEffortLadder(spec: ModelSpec): boolean { return hasModelScopedEffortsRule(buildResolveTarget(spec, resolveIdentity(spec))); }