import type { FallbackEntry } from "./model-requirement-types.js" import type { FallbackModelObject } from "./fallback-model-object.js" import { normalizeModel } from "./model-normalization.js" import { resolveModelPipeline } from "./model-resolution-pipeline.js" import { KNOWN_VARIANTS } from "./known-variants.js" import type { ConnectedProvidersAdapter } from "./connected-providers-cache.js" import * as connectedProvidersCache from "./connected-providers-cache.js" export type ModelResolutionInput = { userModel?: string inheritedModel?: string systemDefault?: string } export type ModelSource = | "override" | "category-default" | "provider-fallback" | "system-default" export type ModelResolutionResult = { model: string source: ModelSource variant?: string } export type ExtendedModelResolutionInput = { uiSelectedModel?: string userModel?: string userFallbackModels?: string[] categoryDefaultModel?: string fallbackChain?: FallbackEntry[] availableModels: Set systemDefaultModel?: string } export function resolveModel(input: ModelResolutionInput): string | undefined { return ( normalizeModel(input.userModel) ?? normalizeModel(input.inheritedModel) ?? input.systemDefault ) } export function resolveModelWithFallback( input: ExtendedModelResolutionInput, connectedProvidersAdapter: ConnectedProvidersAdapter = connectedProvidersCache, ): ModelResolutionResult | undefined { const { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input const resolved = resolveModelPipeline({ intent: { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel }, constraints: { availableModels }, policy: { fallbackChain, systemDefaultModel }, }, connectedProvidersAdapter) if (!resolved) { return undefined } return { model: resolved.model, source: resolved.provenance, variant: resolved.variant, } } /** * Normalizes fallback_models config to a mixed array. * Accepts string, string[], or mixed arrays of strings and FallbackModelObject entries. */ export function normalizeFallbackModels( models: string | (string | FallbackModelObject)[] | undefined, ): (string | FallbackModelObject)[] | undefined { if (!models) return undefined if (typeof models === "string") return [models] return models } /** * Extracts plain model strings from a mixed fallback models array. * Object entries are flattened to "model" or "model(variant)" strings. * Use this when consumers need string[] (e.g., resolveModelForDelegateTask). */ export function flattenToFallbackModelStrings( models: (string | FallbackModelObject)[] | undefined, ): string[] | undefined { if (!models) return undefined return models.map((entry) => { if (typeof entry === "string") return entry const variant = entry.variant if (variant) { // Strip any supported inline variant syntax before appending explicit override. // Supports both parenthesized and space-suffix forms so we don't emit // invalid strings like "provider/model high(low)". const model = entry.model .replace(/\([^()]+\)\s*$/, "") .replace(/\s+([a-z][a-z0-9_-]*)\s*$/i, (_match: string, suffix: string) => { const normalized = String(suffix).toLowerCase() return KNOWN_VARIANTS.has(normalized) ? "" : _match }) .trim() return `${model}(${variant})` } return entry.model }) }