From 7870e435784a8117506dccb2ed8813925be93940 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 12 Mar 2026 01:49:16 +0900 Subject: [PATCH 1/2] fix: preserve session provider context in fallback chain (#2295) --- src/plugin/event.ts | 39 +++++++++++++++++-- src/shared/fallback-chain-from-models.test.ts | 18 ++++++++- src/shared/fallback-chain-from-models.ts | 11 ++++-- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/plugin/event.ts b/src/plugin/event.ts index 1cd23f375..1e3e5e176 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -19,11 +19,12 @@ import { import { getFallbackModelsForSession } from "../hooks/runtime-fallback/fallback-models"; import { resetMessageCursor } from "../shared"; import { getAgentConfigKey } from "../shared/agent-display-names"; +import { readConnectedProvidersCache } from "../shared/connected-providers-cache"; import { log } from "../shared/logger"; import { shouldRetryError } from "../shared/model-error-classifier"; import { buildFallbackChainFromModels } from "../shared/fallback-chain-from-models"; import { extractRetryAttempt, normalizeRetryStatusMessage } from "../shared/retry-status-utils"; -import { clearSessionModel, setSessionModel } from "../shared/session-model-state"; +import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state"; import { deleteSessionTools } from "../shared/session-tools-store"; import { lspManager } from "../tools"; @@ -165,6 +166,30 @@ export function createEventHandler(args: { const lastHandledRetryStatusKey = new Map(); const lastKnownModelBySession = new Map(); + const resolveFallbackProviderID = (sessionID: string, providerHint?: string): string => { + const sessionModel = getSessionModel(sessionID); + if (sessionModel?.providerID) { + return sessionModel.providerID; + } + + const lastKnownModel = lastKnownModelBySession.get(sessionID); + if (lastKnownModel?.providerID) { + return lastKnownModel.providerID; + } + + const connectedProvider = readConnectedProvidersCache()?.[0]; + if (connectedProvider) { + return connectedProvider; + } + + const normalizedProviderHint = providerHint?.trim(); + if (normalizedProviderHint) { + return normalizedProviderHint; + } + + return "opencode"; + }; + const dispatchToHooks = async (input: EventInput): Promise => { await Promise.resolve(hooks.autoUpdateChecker?.event?.(input)); await Promise.resolve(hooks.claudeCodeHooks?.event?.(input)); @@ -360,7 +385,10 @@ export function createEventHandler(args: { } if (agentName) { - const currentProvider = (info?.providerID as string | undefined) ?? "opencode"; + const currentProvider = resolveFallbackProviderID( + sessionID, + info?.providerID as string | undefined, + ); const rawModel = (info?.modelID as string | undefined) ?? "claude-opus-4-6"; const currentModel = normalizeFallbackModelID(rawModel); applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig); @@ -417,7 +445,7 @@ export function createEventHandler(args: { if (agentName) { const parsed = extractProviderModelFromErrorMessage(retryMessage); const lastKnown = lastKnownModelBySession.get(sessionID); - const currentProvider = parsed.providerID ?? lastKnown?.providerID ?? "opencode"; + const currentProvider = resolveFallbackProviderID(sessionID, parsed.providerID); let currentModel = parsed.modelID ?? lastKnown?.modelID ?? "claude-opus-4-6"; currentModel = normalizeFallbackModelID(currentModel); applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig); @@ -489,7 +517,10 @@ export function createEventHandler(args: { if (agentName) { const parsed = extractProviderModelFromErrorMessage(errorMessage); - const currentProvider = (props?.providerID as string) || parsed.providerID || "opencode"; + const currentProvider = resolveFallbackProviderID( + sessionID, + (props?.providerID as string | undefined) || parsed.providerID, + ); let currentModel = (props?.modelID as string) || parsed.modelID || "claude-opus-4-6"; currentModel = normalizeFallbackModelID(currentModel); applyUserConfiguredFallbackChain(sessionID, agentName, currentProvider, args.pluginConfig); diff --git a/src/shared/fallback-chain-from-models.test.ts b/src/shared/fallback-chain-from-models.test.ts index 096364594..ca605f809 100644 --- a/src/shared/fallback-chain-from-models.test.ts +++ b/src/shared/fallback-chain-from-models.test.ts @@ -1,4 +1,5 @@ -import { describe, test, expect } from "bun:test" +declare const require: (name: string) => any +const { describe, test, expect } = require("bun:test") import { buildFallbackChainFromModels, parseFallbackModelEntry } from "./fallback-chain-from-models" describe("fallback-chain-from-models", () => { @@ -32,6 +33,21 @@ describe("fallback-chain-from-models", () => { }) }) + test("uses opencode as absolute fallback provider when context provider is missing", () => { + //#given + const fallbackModel = "gemini-3-flash" + + //#when + const parsed = parseFallbackModelEntry(fallbackModel, undefined) + + //#then + expect(parsed).toEqual({ + providers: ["opencode"], + model: "gemini-3-flash", + variant: undefined, + }) + }) + test("builds fallback chain from normalized fallback_models input", () => { //#given const fallbackModels = ["quotio/kimi-k2.5", "gpt-5.2 medium"] diff --git a/src/shared/fallback-chain-from-models.ts b/src/shared/fallback-chain-from-models.ts index d09b4e5fe..54918f062 100644 --- a/src/shared/fallback-chain-from-models.ts +++ b/src/shared/fallback-chain-from-models.ts @@ -39,13 +39,15 @@ function parseVariantFromModel(rawModel: string): { modelID: string; variant?: s export function parseFallbackModelEntry( model: string, - defaultProviderID: string, + contextProviderID: string | undefined, + defaultProviderID = "opencode", ): FallbackEntry | undefined { const trimmed = model.trim() if (!trimmed) return undefined const parts = trimmed.split("/") - const providerID = parts.length >= 2 ? parts[0].trim() : defaultProviderID + const providerID = + parts.length >= 2 ? parts[0].trim() : (contextProviderID?.trim() || defaultProviderID) const rawModelID = parts.length >= 2 ? parts.slice(1).join("/").trim() : trimmed if (!providerID || !rawModelID) return undefined @@ -61,13 +63,14 @@ export function parseFallbackModelEntry( export function buildFallbackChainFromModels( fallbackModels: string | string[] | undefined, - defaultProviderID: string, + contextProviderID: string | undefined, + defaultProviderID = "opencode", ): FallbackEntry[] | undefined { const normalized = normalizeFallbackModels(fallbackModels) if (!normalized || normalized.length === 0) return undefined const parsed = normalized - .map((model) => parseFallbackModelEntry(model, defaultProviderID)) + .map((model) => parseFallbackModelEntry(model, contextProviderID, defaultProviderID)) .filter((entry): entry is FallbackEntry => entry !== undefined) if (parsed.length === 0) return undefined From 2912b6598ca43d6eb31117cefe56d735ce0672d1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 12 Mar 2026 11:05:31 +0900 Subject: [PATCH 2/2] fix: address Cubic findings for provider preserve fallback - Reorder resolveFallbackProviderID: providerHint now checked before global connected-provider cache - Revert require('bun:test') hack to standard ESM import in fallback-chain-from-models.test.ts Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin/event.ts | 10 +++++----- src/shared/fallback-chain-from-models.test.ts | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/plugin/event.ts b/src/plugin/event.ts index 1e3e5e176..096d6a64f 100644 --- a/src/plugin/event.ts +++ b/src/plugin/event.ts @@ -177,16 +177,16 @@ export function createEventHandler(args: { return lastKnownModel.providerID; } - const connectedProvider = readConnectedProvidersCache()?.[0]; - if (connectedProvider) { - return connectedProvider; - } - const normalizedProviderHint = providerHint?.trim(); if (normalizedProviderHint) { return normalizedProviderHint; } + const connectedProvider = readConnectedProvidersCache()?.[0]; + if (connectedProvider) { + return connectedProvider; + } + return "opencode"; }; diff --git a/src/shared/fallback-chain-from-models.test.ts b/src/shared/fallback-chain-from-models.test.ts index ca605f809..cdfbb9daf 100644 --- a/src/shared/fallback-chain-from-models.test.ts +++ b/src/shared/fallback-chain-from-models.test.ts @@ -1,5 +1,4 @@ -declare const require: (name: string) => any -const { describe, test, expect } = require("bun:test") +import { describe, test, expect } from "bun:test" import { buildFallbackChainFromModels, parseFallbackModelEntry } from "./fallback-chain-from-models" describe("fallback-chain-from-models", () => {