2026-01-22 22:43:27 +09:00
|
|
|
import { log } from "./logger"
|
|
|
|
|
import { fuzzyMatchModel } from "./model-availability"
|
|
|
|
|
import type { FallbackEntry } from "./model-requirements"
|
2026-01-26 11:53:41 +09:00
|
|
|
import { readConnectedProvidersCache } from "./connected-providers-cache"
|
2026-01-22 22:43:27 +09:00
|
|
|
|
2026-01-17 12:51:03 -05:00
|
|
|
export type ModelResolutionInput = {
|
2026-01-22 22:43:27 +09:00
|
|
|
userModel?: string
|
|
|
|
|
inheritedModel?: string
|
2026-01-26 17:01:08 +09:00
|
|
|
systemDefault?: string
|
2026-01-22 22:43:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ModelSource =
|
|
|
|
|
| "override"
|
|
|
|
|
| "provider-fallback"
|
|
|
|
|
| "system-default"
|
|
|
|
|
|
|
|
|
|
export type ModelResolutionResult = {
|
|
|
|
|
model: string
|
|
|
|
|
source: ModelSource
|
2026-01-23 02:20:32 +09:00
|
|
|
variant?: string
|
2026-01-22 22:43:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ExtendedModelResolutionInput = {
|
|
|
|
|
userModel?: string
|
|
|
|
|
fallbackChain?: FallbackEntry[]
|
|
|
|
|
availableModels: Set<string>
|
2026-01-26 17:01:08 +09:00
|
|
|
systemDefaultModel?: string
|
2026-01-22 22:43:27 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-17 12:51:03 -05:00
|
|
|
function normalizeModel(model?: string): string | undefined {
|
2026-01-22 22:43:27 +09:00
|
|
|
const trimmed = model?.trim()
|
|
|
|
|
return trimmed || undefined
|
2026-01-17 12:51:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-26 17:01:08 +09:00
|
|
|
export function resolveModel(input: ModelResolutionInput): string | undefined {
|
2026-01-22 22:43:27 +09:00
|
|
|
return (
|
|
|
|
|
normalizeModel(input.userModel) ??
|
|
|
|
|
normalizeModel(input.inheritedModel) ??
|
|
|
|
|
input.systemDefault
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveModelWithFallback(
|
|
|
|
|
input: ExtendedModelResolutionInput,
|
2026-01-26 17:01:08 +09:00
|
|
|
): ModelResolutionResult | undefined {
|
2026-01-22 22:43:27 +09:00
|
|
|
const { userModel, fallbackChain, availableModels, systemDefaultModel } = input
|
|
|
|
|
|
|
|
|
|
// Step 1: Override
|
|
|
|
|
const normalizedUserModel = normalizeModel(userModel)
|
|
|
|
|
if (normalizedUserModel) {
|
|
|
|
|
log("Model resolved via override", { model: normalizedUserModel })
|
|
|
|
|
return { model: normalizedUserModel, source: "override" }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Provider fallback chain (with availability check)
|
|
|
|
|
if (fallbackChain && fallbackChain.length > 0) {
|
2026-01-23 15:38:54 +09:00
|
|
|
if (availableModels.size === 0) {
|
2026-01-26 11:53:41 +09:00
|
|
|
const connectedProviders = readConnectedProvidersCache()
|
|
|
|
|
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
|
|
|
|
|
|
2026-01-28 13:31:03 +09:00
|
|
|
// When no cache exists at all, skip fallback chain and fall through to system default
|
|
|
|
|
// This allows OpenCode to use Provider.defaultModel() as the final fallback
|
|
|
|
|
if (connectedSet === null) {
|
|
|
|
|
log("No cache available, skipping fallback chain to use system default")
|
|
|
|
|
} else {
|
|
|
|
|
for (const entry of fallbackChain) {
|
|
|
|
|
for (const provider of entry.providers) {
|
|
|
|
|
if (connectedSet.has(provider)) {
|
|
|
|
|
const model = `${provider}/${entry.model}`
|
|
|
|
|
log("Model resolved via fallback chain (no model cache, using connected provider)", {
|
|
|
|
|
provider,
|
|
|
|
|
model: entry.model,
|
|
|
|
|
variant: entry.variant,
|
|
|
|
|
})
|
|
|
|
|
return { model, source: "provider-fallback", variant: entry.variant }
|
|
|
|
|
}
|
2026-01-26 11:53:41 +09:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 13:31:03 +09:00
|
|
|
log("No matching provider in connected cache, falling through to system default")
|
2026-01-26 11:53:41 +09:00
|
|
|
}
|
2026-01-23 15:38:54 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:43:27 +09:00
|
|
|
for (const entry of fallbackChain) {
|
|
|
|
|
for (const provider of entry.providers) {
|
|
|
|
|
const fullModel = `${provider}/${entry.model}`
|
|
|
|
|
const match = fuzzyMatchModel(fullModel, availableModels, [provider])
|
|
|
|
|
if (match) {
|
2026-01-23 02:20:32 +09:00
|
|
|
log("Model resolved via fallback chain (availability confirmed)", { provider, model: entry.model, match, variant: entry.variant })
|
|
|
|
|
return { model: match, source: "provider-fallback", variant: entry.variant }
|
2026-01-22 22:43:27 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-23 10:55:42 +09:00
|
|
|
log("No available model found in fallback chain, falling through to system default")
|
2026-01-22 22:43:27 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-26 17:01:08 +09:00
|
|
|
// Step 3: System default (if provided)
|
|
|
|
|
if (systemDefaultModel === undefined) {
|
|
|
|
|
log("No model resolved - systemDefaultModel not configured")
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 22:43:27 +09:00
|
|
|
log("Model resolved via system default", { model: systemDefaultModel })
|
|
|
|
|
return { model: systemDefaultModel, source: "system-default" }
|
2026-01-17 12:51:03 -05:00
|
|
|
}
|