diff --git a/src/shared/model-resolver.test.ts b/src/shared/model-resolver.test.ts index eba415e4..a081705c 100644 --- a/src/shared/model-resolver.test.ts +++ b/src/shared/model-resolver.test.ts @@ -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() }) diff --git a/src/shared/model-resolver.ts b/src/shared/model-resolver.ts index 627d1ab1..d58533db 100644 --- a/src/shared/model-resolver.ts +++ b/src/shared/model-resolver.ts @@ -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) {