{"version":3,"file":"model-router.d.ts","sourceRoot":"","sources":["../../../../src/core/long-horizon/adaptive/model-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,UAAU,EAAiC,MAAM,0BAA0B,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,IAAI,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,eAAe,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAElH,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClC,QAAQ,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IACxC,MAAM,EAAE,mBAAmB,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACjD,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACjD,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAWD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAS9F;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,YAAiB,GACxB,WAAW,CAmGb;AAoBD,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC","sourcesContent":["/**\n * Role-based model routing.\n *\n * Deterministic, policy-constrained selection of a provider/model for a role.\n * Routing consumes structured evidence (risk, type, phase, remaining budget,\n * health, failures, stall, required independence) and returns a structured,\n * explainable `reasonCodes` decision. A model may *recommend* escalation but\n * never self-authorizes it — escalation requires its own bounded, evidence-\n * backed policy evaluation (see `escalation.ts`).\n */\n\nimport { type CapabilityRegistry, type costTierOf, roleCompatibility, tierWithin } from \"./capability-registry.js\";\nimport type { HealthLevel as ProviderHealthLevel } from \"./provider-health.js\";\nimport type { ModelCapabilities, ModelRole, ModelRolePolicy, ModelRouteDecision, RoutingInput } from \"./types.js\";\n\n/** A candidate model registered for routing. */\nexport interface RouteCandidate {\n\tprovider: string;\n\tmodel: string;\n\tprofile: ModelCapabilities | null;\n\tcostTier: ReturnType<typeof costTierOf>;\n\thealth: ProviderHealthLevel;\n}\n\nexport interface RouterConfig {\n\tregistry: CapabilityRegistry;\n\trolePolicies: Record<ModelRole, ModelRolePolicy>;\n\tcandidates: RouteCandidate[];\n\thealthState: Record<string, ProviderHealthLevel>;\n\tmaxEscalationLevel: number;\n}\n\nexport interface RouteOptions {\n\tescalationLevel?: number;\n\tdeniedModels?: string[];\n\tforceModel?: { provider: string; model: string };\n}\n\nconst ROLE_CAPABILITY_BY_TASK: Record<string, string[]> = {\n\tcode: [\"supportsTools\", \"supportsCodeGeneration\"],\n\tresearch: [\"supportsResearchSynthesis\"],\n\treview: [\"supportsCodeReview\"],\n\tsynthesis: [\"supportsCheapSummarization\"],\n\trepair: [\"supportsToolCallRepair\"],\n\trecovery: [\"supportsTools\", \"supportsCodeGeneration\"],\n};\n\n/**\n * Compute the capability requirements for a routing input by combining the\n * role policy's required capabilities with task-type requirements.\n */\nexport function requiredCapabilitiesFor(policy: ModelRolePolicy, input: RoutingInput): string[] {\n\tconst required = new Set(policy.requiredCapabilities);\n\tfor (const cap of ROLE_CAPABILITY_BY_TASK[input.taskType] ?? []) {\n\t\trequired.add(cap);\n\t}\n\tif (input.requiredTools && input.requiredTools.length > 0) {\n\t\trequired.add(\"supportsTools\");\n\t}\n\treturn [...required];\n}\n\nexport interface RouteResult {\n\tdecision: ModelRouteDecision | null;\n\trejections: string[];\n}\n\n/**\n * Deterministic routing. Returns a decision or a list of rejections (reason\n * codes) when no candidate satisfies policy.\n */\nexport function routeForRole(\n\tconfig: RouterConfig,\n\tinput: RoutingInput,\n\tpolicy: ModelRolePolicy,\n\toptions: RouteOptions = {},\n): RouteResult {\n\tconst required = requiredCapabilitiesFor(policy, input);\n\tconst rejections: string[] = [];\n\n\t// Respect forced model override (used only by deterministic policy, never by\n\t// model self-authorization).\n\tif (options.forceModel) {\n\t\tconst forced = config.candidates.find(\n\t\t\t(c) => c.provider === options.forceModel?.provider && c.model === options.forceModel?.model,\n\t\t);\n\t\tif (forced) {\n\t\t\tconst compat = roleCompatibility(forced.profile, required);\n\t\t\tif (compat.compatible) {\n\t\t\t\treturn {\n\t\t\t\t\tdecision: {\n\t\t\t\t\t\trole: input.role,\n\t\t\t\t\t\tprovider: forced.provider,\n\t\t\t\t\t\tmodel: forced.model,\n\t\t\t\t\t\treasonCodes: [\"FORCED_BY_POLICY\", \"CAPABILITY_COMPATIBLE\", ...compat.reasons],\n\t\t\t\t\t\tfallbackChain: [],\n\t\t\t\t\t\tescalationLevel: 0,\n\t\t\t\t\t},\n\t\t\t\t\trejections,\n\t\t\t\t};\n\t\t\t}\n\t\t\trejections.push(`FORCED_MODEL_INCOMPATIBLE:${forced.provider}/${forced.model}`);\n\t\t}\n\t}\n\n\t// Filter by denied models and allowed providers.\n\tconst allowedProviders = policy.allowedProviders;\n\tconst deniedModels = new Set([...(policy.deniedModels ?? []), ...(options.deniedModels ?? [])]);\n\n\t// Cost-tier cap.\n\tconst maxTier = policy.maximumCostTier;\n\n\t// Determine escalation depth budget.\n\tconst escalationLevel = options.escalationLevel ?? 0;\n\tif (escalationLevel > config.maxEscalationLevel) {\n\t\trejections.push(\"ESCALATION_BUDGET_EXHAUSTED\");\n\t}\n\n\t// Build list of compatible candidates in declared order (deterministic).\n\tconst fallbackChain: string[] = [];\n\tlet chosen: RouteCandidate | null = null;\n\n\tfor (const candidate of config.candidates) {\n\t\tconst key = `${candidate.provider}/${candidate.model}`;\n\t\tif ((candidate.profile?.model && deniedModels.has(candidate.model)) || deniedModels.has(key)) {\n\t\t\trejections.push(`DENIED_MODEL:${key}`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (allowedProviders && !allowedProviders.includes(candidate.provider)) {\n\t\t\trejections.push(`DENIED_PROVIDER:${candidate.provider}`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (candidate.health === \"unhealthy\") {\n\t\t\trejections.push(`UNHEALTHY:${key}`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (candidate.health === \"degraded\" && candidate.costTier === \"premium\") {\n\t\t\trejections.push(`DEGRADED_PREMIUM:${key}`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!tierWithin(candidate.costTier, maxTier)) {\n\t\t\trejections.push(`COST_TIER:${key}`);\n\t\t\tcontinue;\n\t\t}\n\t\tfallbackChain.push(key);\n\n\t\tconst compat = roleCompatibility(candidate.profile, required);\n\t\tif (!compat.compatible) {\n\t\t\trejections.push(`INCOMPATIBLE:${key}:${compat.reasons.join(\",\")}`);\n\t\t\tif (!chosen) {\n\t\t\t\t// Keep the first *compatible-required* candidate even if optional\n\t\t\t\t// preferred caps are missing, but only if required caps pass.\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (!chosen) {\n\t\t\tchosen = candidate;\n\t\t}\n\t\t// Prefer candidates satisfying more preferred capabilities (deterministic ties).\n\t}\n\n\tif (!chosen) {\n\t\treturn { decision: null, rejections };\n\t}\n\n\tconst decision: ModelRouteDecision = {\n\t\trole: input.role,\n\t\tprovider: chosen.provider,\n\t\tmodel: chosen.model,\n\t\treasonCodes: [\"CAPABILITY_COMPATIBLE\", ...preferredReasons(chosen, policy)],\n\t\testimatedBudgetImpact: estimateBudgetImpact(chosen.profile),\n\t\tfallbackChain,\n\t\tescalationLevel,\n\t};\n\treturn { decision, rejections };\n}\n\nfunction preferredReasons(candidate: RouteCandidate, policy: ModelRolePolicy): string[] {\n\tconst reasons: string[] = [];\n\tfor (const pref of policy.preferredCapabilities ?? []) {\n\t\tif ((candidate.profile as Record<string, boolean | \"unknown\"> | null)?.[pref] === true) {\n\t\t\treasons.push(`PREFERRED:${pref}`);\n\t\t}\n\t}\n\treturn reasons;\n}\n\nfunction estimateBudgetImpact(profile: ModelCapabilities | null): number | undefined {\n\tif (!profile?.pricing) return undefined;\n\tconst input = profile.pricing.inputPerMillion ?? 0;\n\tconst output = profile.pricing.outputPerMillion ?? 0;\n\t// Rough per-million-input equivalent cost of a typical 1M input / 50K output call.\n\treturn +(input + output * 0.05).toFixed(6);\n}\n\nexport { hasAllCapabilities } from \"./capability-registry.js\";\n"]}