Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions src/shared/model-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,22 +489,23 @@ describe("resolveModelWithFallback", () => {
expect(logSpy).toHaveBeenCalledWith("No available model found in fallback chain, falling through to system default")
})

test("returns undefined when availableModels empty and no connected providers cache exists", () => {
test("uses first fallback entry when availableModels empty and no connected providers cache exists", () => {
// #given - both model cache and connected-providers cache are missing (first run)
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-5" },
],
availableModels: new Set(),
systemDefaultModel: undefined, // no system default configured
systemDefaultModel: undefined,
}

// #when
const result = resolveModelWithFallback(input)

// #then - should return undefined to let OpenCode use Provider.defaultModel()
expect(result).toBeUndefined()
// #then - should use first fallback entry (fixes category model ignored bug)
expect(result!.model).toBe("anthropic/claude-opus-4-5")
expect(result!.source).toBe("provider-fallback")
cacheSpy.mockRestore()
})

Expand Down Expand Up @@ -568,25 +569,26 @@ describe("resolveModelWithFallback", () => {
cacheSpy.mockRestore()
})

test("falls through to system default when no cache and systemDefaultModel is provided", () => {
// #given - no cache but system default is configured
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-5" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3-pro",
}

// #when
const result = resolveModelWithFallback(input)

// #then - should fall through to system default
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.source).toBe("system-default")
cacheSpy.mockRestore()
})
test("uses first fallback entry when no cache exists (fixes category model ignored bug)", () => {
// #given - no cache but fallback chain is configured
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-5", variant: "max" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3-pro",
}

// #when
const result = resolveModelWithFallback(input)

// #then - should use first fallback entry, NOT system default
expect(result!.model).toBe("anthropic/claude-opus-4-5")
expect(result!.source).toBe("provider-fallback")
expect(result!.variant).toBe("max")
cacheSpy.mockRestore()
})

test("returns system default when fallbackChain is not provided", () => {
// #given
Expand Down
13 changes: 12 additions & 1 deletion src/shared/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,18 @@ export function resolveModelWithFallback(
const connectedSet = connectedProviders ? new Set(connectedProviders) : null

if (connectedSet === null) {
log("Model fallback chain skipped (no connected providers cache) - falling through to system default")
// FIX: Use fallback chain first entry instead of skipping to systemDefault
const firstEntry = fallbackChain[0]
if (firstEntry && firstEntry.providers.length > 0) {
const model = `${firstEntry.providers[0]}/${firstEntry.model}`
log("Model resolved via fallback chain (no cache, using first entry)", {
provider: firstEntry.providers[0],
model: firstEntry.model,
variant: firstEntry.variant,
})
return { model, source: "provider-fallback", variant: firstEntry.variant }
}
log("Model fallback chain empty - falling through to system default")
} else {
for (const entry of fallbackChain) {
for (const provider of entry.providers) {
Expand Down
Loading