Files
oh-my-opencode/src/shared/model-resolver.ts
T
justsisyphus 8a9d966a3d fix(model-resolver): skip fallback chain when no cache exists
When no provider cache exists, skip the fallback chain entirely and let
OpenCode use Provider.defaultModel() as the final fallback. This prevents
incorrect model selection when the plugin loads before providers connect.

- Remove forced first-entry fallback when no cache
- Add log messages for cache miss scenarios
- Update tests for new behavior
2026-01-28 13:31:03 +09:00

105 lines
3.3 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) {
const connectedProviders = readConnectedProvidersCache()
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
// 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 }
}
}
}
log("No matching provider in connected cache, falling through to 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" }
}