From bcbc0957988e1b3d2c2373c417f123ba7e8b80f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=9E=97ObsidianGrove?= Date: Thu, 14 May 2026 18:49:05 +0800 Subject: [PATCH 1/2] fix: sync managed model aliases with visibility Co-Authored-By: Claude Opus 4.7 --- .../dashboard/providers/[id]/page.tsx | 197 ++++++++++++++---- src/app/api/provider-models/route.ts | 22 +- .../providerModels/managedAvailableModels.ts | 78 ++++++- src/lib/providerModels/managedModelImport.ts | 3 +- 4 files changed, 247 insertions(+), 53 deletions(-) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 063183b372..2b6d8bd2e0 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -363,7 +363,7 @@ interface PassthroughModelRowProps { isHidden?: boolean; copied?: string; onCopy: (text: string, key: string) => void; - onDeleteAlias: () => void; + onDeleteAlias?: () => void; t: (key: string, values?: Record) => string; showDeveloperToggle?: boolean; effectiveModelNormalize: (modelId: string, protocol?: string) => boolean; @@ -381,6 +381,7 @@ interface PassthroughModelRowProps { interface PassthroughModelsSectionProps { providerAlias: string; modelAliases: Record; + availableModels?: CompatModelRow[]; customModels?: CompatModelRow[]; copied?: string; onCopy: (text: string, key: string) => void; @@ -421,6 +422,7 @@ interface CompatibleModelsSectionProps { providerStorageAlias: string; providerDisplayAlias: string; modelAliases: Record; + availableModels?: CompatModelRow[]; customModels?: CompatModelRow[]; fallbackModels?: CompatModelRow[]; allowImport: boolean; @@ -2729,8 +2731,10 @@ export default function ProviderDetailPage() { notify.error(detail || t("failedSaveCustomModel")); return; } - // Optimistic update: refresh model meta - await fetchProviderModelMeta().catch(() => {}); + await Promise.all([ + fetchProviderModelMeta().catch(() => {}), + fetchAliases().catch(() => {}), + ]); } catch { notify.error(t("failedSaveCustomModel")); } finally { @@ -2756,7 +2760,10 @@ export default function ProviderDetailPage() { notify.error(detail || t("failedSaveCustomModel")); return; } - await fetchProviderModelMeta().catch(() => {}); + await Promise.all([ + fetchProviderModelMeta().catch(() => {}), + fetchAliases().catch(() => {}), + ]); } catch { notify.error(t("failedSaveCustomModel")); } finally { @@ -2824,6 +2831,7 @@ export default function ProviderDetailPage() { providerStorageAlias={providerStorageAlias} providerDisplayAlias={providerDisplayAlias} modelAliases={modelAliases} + availableModels={syncedAvailableModels} customModels={modelMeta.customModels} fallbackModels={compatibleFallbackModels} description={description} @@ -2883,6 +2891,7 @@ export default function ProviderDetailPage() { buildCompatMap(customModels), [customModels]); - const providerAliases = Object.entries(modelAliases).filter(([, model]: [string, any]) => - (model as string).startsWith(`${providerAlias}/`) + const providerAliases = useMemo( + () => + Object.entries(modelAliases).filter(([, model]: [string, any]) => + (model as string).startsWith(`${providerAlias}/`) + ), + [modelAliases, providerAlias] ); - const allModels = providerAliases.map(([alias, fullModel]: [string, any]) => { - const fmStr = fullModel as string; + const allModels = useMemo(() => { const prefix = `${providerAlias}/`; - const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; - const customModel = customModelMap.get(modelId); - return { - modelId, - fullModel, - alias, - displayName: alias, - source: customModel ? customModel.source || "custom" : "alias", - isHidden: isModelHidden(modelId), + const aliasByModelId = new Map(); + const fullModelByModelId = new Map(); + const rows: Array<{ + modelId: string; + fullModel: string; + alias: string | null; + displayName: string; + source: string; + isHidden: boolean; + }> = []; + const seenModelIds = new Set(); + + for (const [alias, fullModel] of providerAliases) { + const fmStr = fullModel as string; + const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; + aliasByModelId.set(modelId, alias as string); + fullModelByModelId.set(modelId, fmStr); + } + + const addModel = (model: CompatModelRow, source: string) => { + if (!model?.id || seenModelIds.has(model.id)) return; + const fullModel = fullModelByModelId.get(model.id) || `${providerAlias}/${model.id}`; + rows.push({ + modelId: model.id, + fullModel, + alias: aliasByModelId.get(model.id) || null, + displayName: model.name || model.id, + source, + isHidden: isModelHidden(model.id), + }); + seenModelIds.add(model.id); }; - }); + + for (const model of availableModels) { + addModel(model, "imported"); + } + + for (const model of customModels) { + addModel( + model, + normalizeModelCatalogSource(model.source) === "imported" ? "imported" : "custom" + ); + } + + for (const [alias, fullModel] of providerAliases) { + const fmStr = fullModel as string; + const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; + if (!modelId || seenModelIds.has(modelId)) continue; + const customModel = customModelMap.get(modelId); + rows.push({ + modelId, + fullModel: fmStr, + alias: alias as string, + displayName: alias as string, + source: customModel ? customModel.source || "custom" : "alias", + isHidden: isModelHidden(modelId), + }); + seenModelIds.add(modelId); + } + + return rows; + }, [availableModels, customModelMap, customModels, isModelHidden, providerAlias, providerAliases]); const filteredModels = allModels.filter((model) => matchesModelCatalogQuery(modelFilter, { modelId: model.modelId, @@ -4312,7 +4376,7 @@ function PassthroughModelsSection({ isHidden={isHidden} copied={copied} onCopy={onCopy} - onDeleteAlias={() => onDeleteAlias(alias)} + onDeleteAlias={alias ? () => onDeleteAlias(alias) : undefined} t={t} showDeveloperToggle effectiveModelNormalize={effectiveModelNormalize} @@ -4446,13 +4510,15 @@ function PassthroughModelRow({ showDeveloperToggle={showDeveloperToggle} disabled={compatDisabled} /> - + {onDeleteAlias && ( + + )} ); @@ -4981,6 +5047,7 @@ function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, + availableModels = [], customModels = [], fallbackModels = [], description, @@ -5026,35 +5093,75 @@ function CompatibleModelsSection({ ); const allModels = useMemo(() => { - const rows = providerAliases.map(([alias, fullModel]: [string, any]) => { - const fmStr = fullModel as string; - const prefix = `${providerStorageAlias}/`; - const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; - const customModel = customModelMap.get(modelId); - return { - modelId, - alias, - displayName: alias, - source: customModel ? customModel.source || "custom" : "alias", - isHidden: isModelHidden(modelId), - }; - }); + const prefix = `${providerStorageAlias}/`; + const aliasByModelId = new Map(); + const rows: Array<{ + modelId: string; + alias: string | null; + displayName: string; + source: string; + isHidden: boolean; + }> = []; + const seenModelIds = new Set(); - const seenModelIds = new Set(rows.map((row) => row.modelId)); - for (const model of fallbackModels) { - if (!model?.id || seenModelIds.has(model.id)) continue; + for (const [alias, fullModel] of providerAliases) { + const fmStr = fullModel as string; + const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; + aliasByModelId.set(modelId, alias as string); + } + + const addModel = (model: CompatModelRow, source: string) => { + if (!model?.id || seenModelIds.has(model.id)) return; rows.push({ modelId: model.id, - alias: null, + alias: aliasByModelId.get(model.id) || null, displayName: model.name || model.id, - source: "fallback", + source, isHidden: isModelHidden(model.id), }); seenModelIds.add(model.id); + }; + + for (const model of availableModels) { + addModel(model, "imported"); + } + + for (const model of customModels) { + addModel( + model, + normalizeModelCatalogSource(model.source) === "imported" ? "imported" : "custom" + ); + } + + for (const model of fallbackModels) { + addModel(model, "fallback"); + } + + for (const [alias, fullModel] of providerAliases) { + const fmStr = fullModel as string; + const modelId = fmStr.startsWith(prefix) ? fmStr.slice(prefix.length) : fmStr; + if (!modelId || seenModelIds.has(modelId)) continue; + const customModel = customModelMap.get(modelId); + rows.push({ + modelId, + alias: alias as string, + displayName: alias as string, + source: customModel ? customModel.source || "custom" : "alias", + isHidden: isModelHidden(modelId), + }); + seenModelIds.add(modelId); } return rows; - }, [customModelMap, fallbackModels, isModelHidden, providerAliases, providerStorageAlias]); + }, [ + availableModels, + customModelMap, + customModels, + fallbackModels, + isModelHidden, + providerAliases, + providerStorageAlias, + ]); const filteredModels = allModels.filter((model) => matchesModelCatalogQuery(modelFilter, { modelId: model.modelId, diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index 3f991647dc..3a0c1fafac 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -9,6 +9,11 @@ import { mergeModelCompatOverride, type ModelCompatPatch, } from "@/lib/localDb"; +import { + deleteManagedAvailableModelAliases, + deleteManagedAvailableModelAliasesForProvider, + syncManagedAvailableModelAliases, +} from "@/lib/providerModels/managedAvailableModels"; import { AI_PROVIDERS, isOpenAICompatibleProvider, @@ -305,9 +310,20 @@ export async function PATCH(request) { } } + const aliasChanges = + body.isHidden === true + ? { removed: await deleteManagedAvailableModelAliases(provider, modelIds), assigned: [] } + : { + removed: [], + assigned: ( + await syncManagedAvailableModelAliases(provider, modelIds, { pruneMissing: false }) + ).assignedAliases, + }; + return Response.json({ ok: true, updated: modelIds.length, + aliasChanges, models: await getCustomModels(provider), modelCompatOverrides: getModelCompatOverrides(provider), }); @@ -353,7 +369,8 @@ export async function DELETE(request) { const all = searchParams.get("all"); if (all === "true") { await replaceCustomModels(provider, [], { allowEmpty: true }); - return Response.json({ cleared: true }); + const removedAliases = await deleteManagedAvailableModelAliasesForProvider(provider); + return Response.json({ cleared: true, aliasChanges: { removed: removedAliases } }); } if (!modelId) { @@ -369,7 +386,8 @@ export async function DELETE(request) { } const removed = await removeCustomModel(provider, modelId); - return Response.json({ removed }); + const removedAliases = await deleteManagedAvailableModelAliases(provider, [modelId]); + return Response.json({ removed, aliasChanges: { removed: removedAliases } }); } catch (error) { console.error("Error removing provider model:", error); return Response.json( diff --git a/src/lib/providerModels/managedAvailableModels.ts b/src/lib/providerModels/managedAvailableModels.ts index d862435a6f..b193fa4066 100644 --- a/src/lib/providerModels/managedAvailableModels.ts +++ b/src/lib/providerModels/managedAvailableModels.ts @@ -1,6 +1,7 @@ import { deleteModelAlias, getModelAliases, + getModelIsHidden, getProviderNodeById, setModelAlias, } from "@/lib/localDb"; @@ -34,11 +35,71 @@ async function getProviderDisplayPrefix(providerId: string): Promise { return typeof prefix === "string" && prefix.trim().length > 0 ? prefix.trim() : providerId; } +function normalizeModelIds(modelIds: string[]): string[] { + return Array.from( + new Set( + modelIds.map((modelId) => (typeof modelId === "string" ? modelId.trim() : "")).filter(Boolean) + ) + ); +} + +function getManagedFullModelSet(providerId: string, modelIds: string[]): Set { + const storagePrefix = getProviderStoragePrefix(providerId); + return new Set(normalizeModelIds(modelIds).map((modelId) => `${storagePrefix}/${modelId}`)); +} + +export async function deleteManagedAvailableModelAliases( + providerId: string, + modelIds: string[] +): Promise { + if (!usesManagedAvailableModels(providerId)) return []; + + const targetFullModels = getManagedFullModelSet(providerId, modelIds); + if (targetFullModels.size === 0) return []; + + const existingAliasesRaw = await getModelAliases(); + const removedAliases: string[] = []; + + for (const [alias, value] of Object.entries(existingAliasesRaw)) { + if (typeof value !== "string" || !targetFullModels.has(value)) continue; + await deleteModelAlias(alias); + removedAliases.push(alias); + } + + return removedAliases; +} + +export async function deleteManagedAvailableModelAliasesForProvider( + providerId: string +): Promise { + if (!usesManagedAvailableModels(providerId)) return []; + + const storagePrefix = getProviderStoragePrefix(providerId); + const existingAliasesRaw = await getModelAliases(); + const removedAliases: string[] = []; + + for (const [alias, value] of Object.entries(existingAliasesRaw)) { + if (typeof value !== "string" || !value.startsWith(`${storagePrefix}/`)) continue; + await deleteModelAlias(alias); + removedAliases.push(alias); + } + + return removedAliases; +} + export async function syncManagedAvailableModelAliases( providerId: string, modelIds: string[], { pruneMissing = true }: { pruneMissing?: boolean } = {} ) { + if (!usesManagedAvailableModels(providerId)) { + return { + assignedAliases: [], + removedAliases: [], + storagePrefix: getProviderStoragePrefix(providerId), + }; + } + const storagePrefix = getProviderStoragePrefix(providerId); const displayPrefix = await getProviderDisplayPrefix(providerId); const existingAliasesRaw = await getModelAliases(); @@ -49,11 +110,7 @@ export async function syncManagedAvailableModelAliases( }) ); - const targetModelIds = Array.from( - new Set( - modelIds.map((modelId) => (typeof modelId === "string" ? modelId.trim() : "")).filter(Boolean) - ) - ); + const targetModelIds = normalizeModelIds(modelIds); const targetFullModels = new Set(targetModelIds.map((modelId) => `${storagePrefix}/${modelId}`)); const removedAliases: string[] = []; @@ -71,6 +128,17 @@ export async function syncManagedAvailableModelAliases( const assignedAliases: string[] = []; for (const modelId of targetModelIds) { + if (getModelIsHidden(providerId, modelId)) { + const fullModel = `${storagePrefix}/${modelId}`; + for (const [alias, value] of Object.entries(workingAliases)) { + if (value !== fullModel) continue; + await deleteModelAlias(alias); + delete workingAliases[alias]; + removedAliases.push(alias); + } + continue; + } + const fullModel = `${storagePrefix}/${modelId}`; const alias = resolveManagedModelAlias({ modelId, diff --git a/src/lib/providerModels/managedModelImport.ts b/src/lib/providerModels/managedModelImport.ts index 30229794d1..9e5702a50d 100644 --- a/src/lib/providerModels/managedModelImport.ts +++ b/src/lib/providerModels/managedModelImport.ts @@ -240,9 +240,10 @@ export async function importManagedModels({ let syncedAliases = 0; if (usesManagedAvailableModels(providerId) && (mode === "merge" || discoveredModels.length > 0)) { + const aliasModelIds = mode === "sync" ? syncedAvailableModels : discoveredModels; const aliasSync = await syncManagedAvailableModelAliases( providerId, - discoveredModels.map((model) => model.id), + aliasModelIds.map((model) => model.id), { pruneMissing: mode === "sync" } ); syncedAliases = aliasSync.assignedAliases.length; From 153a421354eb241031be0ef9dd0a24d59566c2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=9E=97ObsidianGrove?= Date: Thu, 14 May 2026 20:52:08 +0800 Subject: [PATCH 2/2] test: cover managed model alias lifecycle Co-Authored-By: Claude Opus 4.7 --- src/app/api/provider-models/route.ts | 7 +- tests/unit/managed-available-models.test.ts | 96 +++++++++++++++++++++ tests/unit/managed-model-import.test.ts | 66 ++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 tests/unit/managed-model-import.test.ts diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index 3a0c1fafac..40c81a687b 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -370,7 +370,10 @@ export async function DELETE(request) { if (all === "true") { await replaceCustomModels(provider, [], { allowEmpty: true }); const removedAliases = await deleteManagedAvailableModelAliasesForProvider(provider); - return Response.json({ cleared: true, aliasChanges: { removed: removedAliases } }); + return Response.json({ + cleared: true, + aliasChanges: { removed: removedAliases, assigned: [] }, + }); } if (!modelId) { @@ -387,7 +390,7 @@ export async function DELETE(request) { const removed = await removeCustomModel(provider, modelId); const removedAliases = await deleteManagedAvailableModelAliases(provider, [modelId]); - return Response.json({ removed, aliasChanges: { removed: removedAliases } }); + return Response.json({ removed, aliasChanges: { removed: removedAliases, assigned: [] } }); } catch (error) { console.error("Error removing provider model:", error); return Response.json( diff --git a/tests/unit/managed-available-models.test.ts b/tests/unit/managed-available-models.test.ts index 21a41f86a2..2635867568 100644 --- a/tests/unit/managed-available-models.test.ts +++ b/tests/unit/managed-available-models.test.ts @@ -1,10 +1,39 @@ import test from "node:test"; import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-managed-available-models-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDb = await import("../../src/lib/db/models.ts"); +const localDb = await import("../../src/lib/localDb.ts"); const { compatibleProviderSupportsModelImport, getCompatibleFallbackModels } = await import("../../src/lib/providers/managedAvailableModels.ts"); +const { + deleteManagedAvailableModelAliases, + deleteManagedAvailableModelAliasesForProvider, + syncManagedAvailableModelAliases, +} = await import("../../src/lib/providerModels/managedAvailableModels.ts"); const { getModelsByProviderId } = await import("../../src/shared/constants/models.ts"); +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + test("CC compatible fallback models mirror the OAuth Claude Code registry list", () => { assert.deepEqual( getCompatibleFallbackModels("anthropic-compatible-cc-demo"), @@ -20,3 +49,70 @@ test("OpenRouter keeps imported fallback models as its managed list source", () const fallbackModels = [{ id: "openai/gpt-5" }, { id: "anthropic/claude-sonnet-4-6" }]; assert.deepEqual(getCompatibleFallbackModels("openrouter", fallbackModels), fallbackModels); }); + +test("hidden managed models do not create aliases during sync", async () => { + modelsDb.mergeModelCompatOverride("openrouter", "hidden/model", { isHidden: true }); + + const result = await syncManagedAvailableModelAliases("openrouter", ["hidden/model"]); + const aliases = await localDb.getModelAliases(); + + assert.deepEqual(result.assignedAliases, []); + assert.deepEqual(aliases, {}); +}); + +test("deleteManagedAvailableModelAliases removes only aliases matching target full models", async () => { + await localDb.setModelAlias("target", "openrouter/xiaomi/mimo-v2.5-pro"); + await localDb.setModelAlias("same-provider-other-model", "openrouter/xiaomi/mimo-v2.5-max"); + await localDb.setModelAlias("other-provider", "openai/xiaomi/mimo-v2.5-pro"); + + const removed = await deleteManagedAvailableModelAliases("openrouter", ["xiaomi/mimo-v2.5-pro"]); + const aliases = await localDb.getModelAliases(); + + assert.deepEqual(removed, ["target"]); + assert.equal(aliases.target, undefined); + assert.equal(aliases["same-provider-other-model"], "openrouter/xiaomi/mimo-v2.5-max"); + assert.equal(aliases["other-provider"], "openai/xiaomi/mimo-v2.5-pro"); +}); + +test("deleteManagedAvailableModelAliasesForProvider removes provider-scoped aliases only", async () => { + await localDb.setModelAlias("router-a", "openrouter/provider/model-a"); + await localDb.setModelAlias("router-b", "openrouter/provider/model-b"); + await localDb.setModelAlias("openai-a", "openai/provider/model-a"); + await localDb.setModelAlias("bare", "model-a"); + + const removed = await deleteManagedAvailableModelAliasesForProvider("openrouter"); + const aliases = await localDb.getModelAliases(); + + assert.deepEqual(new Set(removed), new Set(["router-a", "router-b"])); + assert.equal(aliases["router-a"], undefined); + assert.equal(aliases["router-b"], undefined); + assert.equal(aliases["openai-a"], "openai/provider/model-a"); + assert.equal(aliases.bare, "model-a"); +}); + +test("unhidden managed models receive a fresh alias when sync reruns", async () => { + modelsDb.mergeModelCompatOverride("openrouter", "vendor/model", { isHidden: true }); + await syncManagedAvailableModelAliases("openrouter", ["vendor/model"]); + + modelsDb.mergeModelCompatOverride("openrouter", "vendor/model", { isHidden: false }); + const result = await syncManagedAvailableModelAliases("openrouter", ["vendor/model"]); + const aliases = await localDb.getModelAliases(); + + assert.deepEqual(result.assignedAliases, ["model"]); + assert.equal(aliases.model, "openrouter/vendor/model"); +}); + +test("managed alias helpers are no-ops for non-managed providers", async () => { + await localDb.setModelAlias("manual", "claude/custom-model"); + + const removedOne = await deleteManagedAvailableModelAliases("claude", ["custom-model"]); + const removedAll = await deleteManagedAvailableModelAliasesForProvider("claude"); + const syncResult = await syncManagedAvailableModelAliases("claude", ["custom-model"]); + const aliases = await localDb.getModelAliases(); + + assert.deepEqual(removedOne, []); + assert.deepEqual(removedAll, []); + assert.deepEqual(syncResult.assignedAliases, []); + assert.deepEqual(syncResult.removedAliases, []); + assert.equal(aliases.manual, "claude/custom-model"); +}); diff --git a/tests/unit/managed-model-import.test.ts b/tests/unit/managed-model-import.test.ts new file mode 100644 index 0000000000..84be9ff37c --- /dev/null +++ b/tests/unit/managed-model-import.test.ts @@ -0,0 +1,66 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-managed-model-import-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDb = await import("../../src/lib/db/models.ts"); +const localDb = await import("../../src/lib/localDb.ts"); +const { importManagedModels } = await import("../../src/lib/providerModels/managedModelImport.ts"); + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("sync mode builds aliases from provider-level synced available models", async () => { + await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", "conn-a", [ + { id: "shared/model-a", name: "Model A", source: "imported" }, + ]); + + await importManagedModels({ + providerId: "openrouter", + connectionId: "conn-b", + mode: "sync", + fetchedModels: [{ id: "shared/model-b", name: "Model B" }], + }); + + const aliases = await localDb.getModelAliases(); + + assert.equal(aliases["model-a"], "openrouter/shared/model-a"); + assert.equal(aliases["model-b"], "openrouter/shared/model-b"); +}); + +test("merge mode builds aliases from discovered models without pruning missing provider aliases", async () => { + await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", "conn-a", [ + { id: "shared/model-a", name: "Model A", source: "imported" }, + ]); + await localDb.setModelAlias("existing", "openrouter/shared/existing"); + + await importManagedModels({ + providerId: "openrouter", + connectionId: "conn-b", + mode: "merge", + fetchedModels: [{ id: "shared/model-b", name: "Model B" }], + }); + + const aliases = await localDb.getModelAliases(); + + assert.equal(aliases.existing, "openrouter/shared/existing"); + assert.equal(aliases["model-a"], undefined); + assert.equal(aliases["model-b"], "openrouter/shared/model-b"); +});