fix(model-resolver): skip fallback chain when model availability cannot be verified
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.
This commit is contained in:
@@ -356,8 +356,10 @@ describe("resolveModelWithFallback", () => {
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("uses connected provider when availableModels empty but connected providers cache exists", () => {
|
||||
test("skips fallback chain when availableModels empty even if connected providers cache exists", () => {
|
||||
// #given - model cache missing but connected-providers cache exists
|
||||
// This scenario caused bugs: provider is connected but may not have the model available
|
||||
// Fix: When we can't verify model availability, skip fallback chain entirely
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai", "google"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
@@ -370,9 +372,32 @@ describe("resolveModelWithFallback", () => {
|
||||
// #when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should use openai (second provider) since anthropic not in connected cache
|
||||
expect(result!.model).toBe("openai/claude-opus-4-5")
|
||||
expect(result!.source).toBe("provider-fallback")
|
||||
// #then - should fall through to system default (NOT use connected provider blindly)
|
||||
expect(result!.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.source).toBe("system-default")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("prevents selecting model from provider that may not have it (bug reproduction)", () => {
|
||||
// #given - user removed anthropic oauth, has quotio, but explore agent fallback has opencode
|
||||
// opencode may be "connected" but doesn't have claude-haiku-4-5
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["quotio", "opencode"])
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" },
|
||||
],
|
||||
availableModels: new Set(), // no model cache available
|
||||
systemDefaultModel: "quotio/claude-opus-4-5-20251101",
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then - should NOT return opencode/claude-haiku-4-5 (model may not exist)
|
||||
// should fall through to system default which user has configured
|
||||
expect(result!.model).toBe("quotio/claude-opus-4-5-20251101")
|
||||
expect(result!.source).toBe("system-default")
|
||||
expect(result!.model).not.toBe("opencode/claude-haiku-4-5")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
|
||||
@@ -55,29 +55,10 @@ export function resolveModelWithFallback(
|
||||
// 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")
|
||||
}
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user