From f6d5f6f79ff0711e6553cc7226d814a7cc153304 Mon Sep 17 00:00:00 2001 From: Jaden Date: Wed, 25 Feb 2026 17:15:13 +0900 Subject: [PATCH 1/2] fix(model-fallback): apply transformModelForProvider in getNextFallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The getNextFallback function returned raw model names from the hardcoded fallback chain without transforming them for the target provider. For example, github-copilot requires dot notation (claude-sonnet-4.6) but the fallback chain stores hyphen notation (claude-sonnet-4-6). The background-agent retry handler already calls transformModelForProvider correctly, but the sync chat.message hook in model-fallback was missing it — a copy-paste omission. Add transformModelForProvider call in getNextFallback and a test verifying github-copilot model name transformation. --- src/hooks/model-fallback/hook.test.ts | 46 +++++++++++++++++++++++++++ src/hooks/model-fallback/hook.ts | 3 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/hooks/model-fallback/hook.test.ts b/src/hooks/model-fallback/hook.test.ts index 4d30d5b0..3d3b4e74 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -3,12 +3,14 @@ import { beforeEach, describe, expect, test } from "bun:test" import { clearPendingModelFallback, createModelFallbackHook, + setSessionFallbackChain, setPendingModelFallback, } from "./hook" describe("model fallback hook", () => { beforeEach(() => { clearPendingModelFallback("ses_model_fallback_main") + clearPendingModelFallback("ses_model_fallback_ghcp") }) test("applies pending fallback on chat.message by overriding model", async () => { @@ -138,4 +140,48 @@ describe("model fallback hook", () => { expect(toastCalls.length).toBe(1) expect(toastCalls[0]?.title).toBe("Model fallback") }) + + test("transforms model names for github-copilot provider via fallback chain", async () => { + //#given + const sessionID = "ses_model_fallback_ghcp" + clearPendingModelFallback(sessionID) + + const hook = createModelFallbackHook() as unknown as { + "chat.message"?: ( + input: { sessionID: string }, + output: { message: Record; parts: Array<{ type: string; text?: string }> }, + ) => Promise + } + + // Set a custom fallback chain that routes through github-copilot + setSessionFallbackChain(sessionID, [ + { providers: ["github-copilot"], model: "claude-sonnet-4-6" }, + ]) + + const set = setPendingModelFallback( + sessionID, + "Atlas (Plan Executor)", + "github-copilot", + "claude-sonnet-4-6", + ) + expect(set).toBe(true) + + const output = { + message: { + model: { providerID: "github-copilot", modelID: "claude-sonnet-4-6" }, + }, + parts: [{ type: "text", text: "continue" }], + } + + //#when + await hook["chat.message"]?.({ sessionID }, output) + + //#then — model name should be transformed from hyphen to dot notation + expect(output.message["model"]).toEqual({ + providerID: "github-copilot", + modelID: "claude-sonnet-4.6", + }) + + clearPendingModelFallback(sessionID) + }) }) diff --git a/src/hooks/model-fallback/hook.ts b/src/hooks/model-fallback/hook.ts index fbe9deab..bbb01825 100644 --- a/src/hooks/model-fallback/hook.ts +++ b/src/hooks/model-fallback/hook.ts @@ -3,6 +3,7 @@ import { getAgentConfigKey } from "../../shared/agent-display-names" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" import { readConnectedProvidersCache, readProviderModelsCache } from "../../shared/connected-providers-cache" import { selectFallbackProvider } from "../../shared/model-error-classifier" +import { transformModelForProvider } from "../../shared/provider-model-id-transform" import { log } from "../../shared/logger" import { getTaskToastManager } from "../../features/task-toast-manager" import type { ChatMessageInput, ChatMessageHandlerOutput } from "../../plugin/chat-message" @@ -145,7 +146,7 @@ export function getNextFallback( return { providerID, - modelID: fallback.model, + modelID: transformModelForProvider(providerID, fallback.model), variant: fallback.variant, } } From 94ff673d40483eeea192bd843d385ae23f670391 Mon Sep 17 00:00:00 2001 From: east-shine Date: Wed, 25 Feb 2026 21:40:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(model-fallback):=20google=20provider?= =?UTF-8?q?=20=EB=AA=A8=EB=8D=B8=EB=AA=85=20=EB=B3=80=ED=99=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit google provider에서 gemini-3-pro → gemini-3-pro-preview 변환이 getNextFallback를 통해 정상 적용되는지 검증하는 테스트 추가. 기존 github-copilot 테스트와 동일한 패턴으로 작성. --- src/hooks/model-fallback/hook.test.ts | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/hooks/model-fallback/hook.test.ts b/src/hooks/model-fallback/hook.test.ts index 3d3b4e74..348f163a 100644 --- a/src/hooks/model-fallback/hook.test.ts +++ b/src/hooks/model-fallback/hook.test.ts @@ -11,6 +11,7 @@ describe("model fallback hook", () => { beforeEach(() => { clearPendingModelFallback("ses_model_fallback_main") clearPendingModelFallback("ses_model_fallback_ghcp") + clearPendingModelFallback("ses_model_fallback_google") }) test("applies pending fallback on chat.message by overriding model", async () => { @@ -184,4 +185,48 @@ describe("model fallback hook", () => { clearPendingModelFallback(sessionID) }) + + test("transforms model names for google provider via fallback chain", async () => { + //#given + const sessionID = "ses_model_fallback_google" + clearPendingModelFallback(sessionID) + + const hook = createModelFallbackHook() as unknown as { + "chat.message"?: ( + input: { sessionID: string }, + output: { message: Record; parts: Array<{ type: string; text?: string }> }, + ) => Promise + } + + // Set a custom fallback chain that routes through google + setSessionFallbackChain(sessionID, [ + { providers: ["google"], model: "gemini-3-pro" }, + ]) + + const set = setPendingModelFallback( + sessionID, + "Oracle", + "google", + "gemini-3-pro", + ) + expect(set).toBe(true) + + const output = { + message: { + model: { providerID: "google", modelID: "gemini-3-pro" }, + }, + parts: [{ type: "text", text: "continue" }], + } + + //#when + await hook["chat.message"]?.({ sessionID }, output) + + //#then — model name should be transformed from gemini-3-pro to gemini-3-pro-preview + expect(output.message["model"]).toEqual({ + providerID: "google", + modelID: "gemini-3-pro-preview", + }) + + clearPendingModelFallback(sessionID) + }) })