fix(runtime-fallback): make fallback provider selection provider-agnostic (fixes #2303)

This commit is contained in:
MoerAI
2026-03-19 21:02:34 +09:00
committed by sspark-kisane
parent d2a49428b9
commit 0e610a72bc
4 changed files with 82 additions and 3 deletions
+44
View File
@@ -255,6 +255,50 @@ describe("model fallback hook", () => {
clearPendingModelFallback(sessionID)
})
test("uses connected preferred provider when fallback entry providers are disconnected", async () => {
//#given
const sessionID = "ses_model_fallback_preferred_provider"
clearPendingModelFallback(sessionID)
readConnectedProvidersCacheMock.mockReturnValue(["provider-x"])
const hook = createModelFallbackHook() as unknown as {
"chat.message"?: (
input: { sessionID: string },
output: { message: Record<string, unknown>; parts: Array<{ type: string; text?: string }> },
) => Promise<void>
}
setSessionFallbackChain(sessionID, [
{ providers: ["provider-y"], model: "fallback-model" },
])
expect(
setPendingModelFallback(
sessionID,
"Sisyphus (Ultraworker)",
"provider-x",
"current-model",
),
).toBe(true)
const output = {
message: {
model: { providerID: "provider-x", modelID: "current-model" },
},
parts: [{ type: "text", text: "continue" }],
}
//#when
await hook["chat.message"]?.({ sessionID }, output)
//#then
expect(output.message["model"]).toEqual({
providerID: "provider-x",
modelID: "fallback-model",
})
clearPendingModelFallback(sessionID)
})
test("shows toast when fallback is applied", async () => {
//#given
const toastCalls: Array<{ title: string; message: string }> = []
+9 -2
View File
@@ -130,14 +130,21 @@ export function getNextFallback(
const providerModelsCache = readProviderModelsCache()
const connectedProviders = providerModelsCache?.connected ?? readConnectedProvidersCache()
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
const connectedSet = connectedProviders
? new Set(connectedProviders.map((provider) => provider.toLowerCase()))
: null
const isReachable = (entry: FallbackEntry): boolean => {
if (!connectedSet) return true
// Gate only on provider connectivity. Provider model lists can be stale/incomplete,
// especially after users manually add models to opencode.json.
return entry.providers.some((p) => connectedSet.has(p))
if (entry.providers.some((provider) => connectedSet.has(provider.toLowerCase()))) {
return true
}
const preferredProvider = state.providerID.toLowerCase()
return connectedSet.has(preferredProvider)
}
while (state.attemptCount < fallbackChain.length) {