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] 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;