c69038bddc
When model cache is empty, the fallback chain resolution was blindly trusting connected providers without verifying if the model actually exists. This caused errors when a provider (e.g., opencode) was marked as connected but didn't have the requested model (e.g., claude-haiku-4-5). Now skips fallback chain entirely when model cache is unavailable and falls through to system default, letting OpenCode handle the resolution.
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { log } from "./logger"
|
|
import { fuzzyMatchModel } from "./model-availability"
|
|
import type { FallbackEntry } from "./model-requirements"
|
|
import { readConnectedProvidersCache } from "./connected-providers-cache"
|
|
|
|
export type ModelResolutionInput = {
|
|
userModel?: string
|
|
inheritedModel?: string
|
|
systemDefault?: string
|
|
}
|
|
|
|
export type ModelSource =
|
|
| "override"
|
|
| "provider-fallback"
|
|
| "system-default"
|
|
|
|
export type ModelResolutionResult = {
|
|
model: string
|
|
source: ModelSource
|
|
variant?: string
|
|
}
|
|
|
|
export type ExtendedModelResolutionInput = {
|
|
userModel?: string
|
|
fallbackChain?: FallbackEntry[]
|
|
availableModels: Set<string>
|
|
systemDefaultModel?: string
|
|
}
|
|
|
|
function normalizeModel(model?: string): string | undefined {
|
|
const trimmed = model?.trim()
|
|
return trimmed || undefined
|
|
}
|
|
|
|
export function resolveModel(input: ModelResolutionInput): string | undefined {
|
|
return (
|
|
normalizeModel(input.userModel) ??
|
|
normalizeModel(input.inheritedModel) ??
|
|
input.systemDefault
|
|
)
|
|
}
|
|
|
|
export function resolveModelWithFallback(
|
|
input: ExtendedModelResolutionInput,
|
|
): ModelResolutionResult | undefined {
|
|
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) {
|
|
if (availableModels.size === 0) {
|
|
// When model cache is empty, we cannot verify if a provider actually has the model.
|
|
// Skip fallback chain entirely and fall through to system default.
|
|
// This prevents selecting provider/model combinations that may not exist.
|
|
log("No model cache available, skipping fallback chain to use system default")
|
|
}
|
|
|
|
for (const entry of fallbackChain) {
|
|
for (const provider of entry.providers) {
|
|
const fullModel = `${provider}/${entry.model}`
|
|
const match = fuzzyMatchModel(fullModel, availableModels, [provider])
|
|
if (match) {
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
log("No available model found in fallback chain, falling through to system default")
|
|
}
|
|
|
|
// Step 3: System default (if provided)
|
|
if (systemDefaultModel === undefined) {
|
|
log("No model resolved - systemDefaultModel not configured")
|
|
return undefined
|
|
}
|
|
|
|
log("Model resolved via system default", { model: systemDefaultModel })
|
|
return { model: systemDefaultModel, source: "system-default" }
|
|
}
|