From 605de6ada4c6be185c534cd70720e5caeac1680c Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 11 Mar 2026 18:02:41 +0900 Subject: [PATCH] fix(look-at): preserve hardcoded multimodal variants --- .../look-at/multimodal-fallback-chain.test.js | 38 ++++++++++++++++ .../look-at/multimodal-fallback-chain.ts | 44 ++++++++++++++++--- 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/tools/look-at/multimodal-fallback-chain.test.js diff --git a/src/tools/look-at/multimodal-fallback-chain.test.js b/src/tools/look-at/multimodal-fallback-chain.test.js new file mode 100644 index 000000000..6c91bf95d --- /dev/null +++ b/src/tools/look-at/multimodal-fallback-chain.test.js @@ -0,0 +1,38 @@ +describe("buildMultimodalLookerFallbackChain", () => { + it("preserves hardcoded variant metadata when dynamic and hardcoded entries share the same model", async () => { + // given + const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain") + const visionCapableModels = [ + { providerID: "openai", modelID: "gpt-5.4" }, + { providerID: "opencode", modelID: "gpt-5.4" }, + ] + + // when + const result = buildMultimodalLookerFallbackChain(visionCapableModels) + const matchingEntries = result.filter((entry) => entry.model === "gpt-5.4") + + // then + expect(matchingEntries).toHaveLength(1) + expect(matchingEntries[0]).toEqual({ + providers: ["openai", "opencode"], + model: "gpt-5.4", + variant: "medium", + }) + }) + + it("merges missing hardcoded providers into an existing dynamic entry", async () => { + // given + const { buildMultimodalLookerFallbackChain } = await import("./multimodal-fallback-chain") + const visionCapableModels = [{ providerID: "openai", modelID: "gpt-5.4" }] + + // when + const result = buildMultimodalLookerFallbackChain(visionCapableModels) + + // then + expect(result[0]).toEqual({ + providers: ["openai", "opencode"], + model: "gpt-5.4", + variant: "medium", + }) + }) +}) diff --git a/src/tools/look-at/multimodal-fallback-chain.ts b/src/tools/look-at/multimodal-fallback-chain.ts index 2e0f65de1..00044fffa 100644 --- a/src/tools/look-at/multimodal-fallback-chain.ts +++ b/src/tools/look-at/multimodal-fallback-chain.ts @@ -19,14 +19,14 @@ export function isHardcodedMultimodalFallbackModel(model: VisionCapableModel): b export function buildMultimodalLookerFallbackChain( visionCapableModels: VisionCapableModel[], ): FallbackEntry[] { - const seen = new Set() + const entryIndexByKey = new Map() const fallbackChain: FallbackEntry[] = [] for (const visionCapableModel of visionCapableModels) { const key = getFullModelKey(visionCapableModel.providerID, visionCapableModel.modelID) - if (seen.has(key)) continue + if (entryIndexByKey.has(key)) continue - seen.add(key) + entryIndexByKey.set(key, fallbackChain.length) fallbackChain.push({ providers: [visionCapableModel.providerID], model: visionCapableModel.modelID, @@ -37,13 +37,45 @@ export function buildMultimodalLookerFallbackChain( const providerModelKeys = entry.providers.map((providerID) => getFullModelKey(providerID, entry.model), ) - if (providerModelKeys.every((key) => seen.has(key))) { + + const existingIndexes = [...new Set( + providerModelKeys + .map((key) => entryIndexByKey.get(key)) + .filter((index): index is number => index !== undefined), + )] + + if (existingIndexes.length > 0) { + const [targetIndex, ...duplicateIndexes] = existingIndexes + const targetEntry = fallbackChain[targetIndex] + const mergedProviders = new Set(targetEntry.providers) + + for (const providerID of entry.providers) { + mergedProviders.add(providerID) + } + + for (const duplicateIndex of duplicateIndexes) { + const duplicateEntry = fallbackChain[duplicateIndex] + for (const providerID of duplicateEntry.providers) { + mergedProviders.add(providerID) + } + duplicateEntry.providers = [] + } + + targetEntry.providers = [...mergedProviders] + targetEntry.variant ??= entry.variant + + for (const providerID of targetEntry.providers) { + entryIndexByKey.set(getFullModelKey(providerID, targetEntry.model), targetIndex) + } + continue } - providerModelKeys.forEach((key) => seen.add(key)) + providerModelKeys.forEach((key) => { + entryIndexByKey.set(key, fallbackChain.length) + }) fallbackChain.push(entry) } - return fallbackChain + return fallbackChain.filter((entry) => entry.providers.length > 0) }