From 2f794d92a42e9a95126b2503841ab55150a9db87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=9E=97ObsidianGrove?= Date: Fri, 15 May 2026 01:31:08 +0800 Subject: [PATCH 1/3] fix: hide delete action for imported models Prevent synced imported and fallback provider models from showing row-level delete actions, while keeping custom/manual and alias-only deletion behavior intact. Co-Authored-By: Claude Opus 4.7 --- .../dashboard/providers/[id]/page.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 2b6d8bd2e0..eed2cf8600 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -2731,10 +2731,7 @@ export default function ProviderDetailPage() { notify.error(detail || t("failedSaveCustomModel")); return; } - await Promise.all([ - fetchProviderModelMeta().catch(() => {}), - fetchAliases().catch(() => {}), - ]); + await Promise.all([fetchProviderModelMeta().catch(() => {}), fetchAliases().catch(() => {})]); } catch { notify.error(t("failedSaveCustomModel")); } finally { @@ -2760,10 +2757,7 @@ export default function ProviderDetailPage() { notify.error(detail || t("failedSaveCustomModel")); return; } - await Promise.all([ - fetchProviderModelMeta().catch(() => {}), - fetchAliases().catch(() => {}), - ]); + await Promise.all([fetchProviderModelMeta().catch(() => {}), fetchAliases().catch(() => {})]); } catch { notify.error(t("failedSaveCustomModel")); } finally { @@ -4277,7 +4271,14 @@ function PassthroughModelsSection({ } return rows; - }, [availableModels, customModelMap, customModels, isModelHidden, providerAlias, providerAliases]); + }, [ + availableModels, + customModelMap, + customModels, + isModelHidden, + providerAlias, + providerAliases, + ]); const filteredModels = allModels.filter((model) => matchesModelCatalogQuery(modelFilter, { modelId: model.modelId, @@ -4376,7 +4377,7 @@ function PassthroughModelsSection({ isHidden={isHidden} copied={copied} onCopy={onCopy} - onDeleteAlias={alias ? () => onDeleteAlias(alias) : undefined} + onDeleteAlias={source === "alias" && alias ? () => onDeleteAlias(alias) : undefined} t={t} showDeveloperToggle effectiveModelNormalize={effectiveModelNormalize} @@ -5346,7 +5347,13 @@ function CompatibleModelsSection({ isHidden={isHidden} copied={copied} onCopy={onCopy} - onDeleteAlias={() => handleDeleteModel(modelId, alias)} + onDeleteAlias={ + source === "custom" || source === "manual" + ? () => handleDeleteModel(modelId, alias) + : source === "alias" && alias + ? () => onDeleteAlias(alias) + : undefined + } t={t} showDeveloperToggle={!isAnthropic} effectiveModelNormalize={effectiveModelNormalize} From 83f242fa4ddc25d07d155fe179e823675bfdbba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=9E=97ObsidianGrove?= Date: Fri, 15 May 2026 01:39:31 +0800 Subject: [PATCH 2/3] fix: clear synced provider models on delete all Ensure provider-level delete all removes synced available model lists so imported models do not reappear after clearing managed models. Co-Authored-By: Claude Opus 4.7 --- src/app/api/provider-models/route.ts | 4 ++++ src/lib/db/models.ts | 16 ++++++++++++++++ src/lib/localDb.ts | 1 + 3 files changed, 21 insertions(+) diff --git a/src/app/api/provider-models/route.ts b/src/app/api/provider-models/route.ts index 40c81a687b..4956aaf962 100644 --- a/src/app/api/provider-models/route.ts +++ b/src/app/api/provider-models/route.ts @@ -4,6 +4,7 @@ import { addCustomModel, removeCustomModel, replaceCustomModels, + deleteSyncedAvailableModelsForProvider, updateCustomModel, getModelCompatOverrides, mergeModelCompatOverride, @@ -369,9 +370,12 @@ export async function DELETE(request) { const all = searchParams.get("all"); if (all === "true") { await replaceCustomModels(provider, [], { allowEmpty: true }); + const syncedAvailableModelListsRemoved = + await deleteSyncedAvailableModelsForProvider(provider); const removedAliases = await deleteManagedAvailableModelAliasesForProvider(provider); return Response.json({ cleared: true, + syncedAvailableModelListsRemoved, aliasChanges: { removed: removedAliases, assigned: [] }, }); } diff --git a/src/lib/db/models.ts b/src/lib/db/models.ts index f749a4ffc8..2520ada8fa 100644 --- a/src/lib/db/models.ts +++ b/src/lib/db/models.ts @@ -707,6 +707,22 @@ export async function deleteSyncedAvailableModelsForConnection( return getSyncedAvailableModels(providerId); } +/** + * Delete all synced models for every connection belonging to a provider. + * Returns the number of connection-scoped synced model lists removed. + */ +export async function deleteSyncedAvailableModelsForProvider(providerId: string): Promise { + const db = getDbInstance(); + const keyPrefix = `${providerId}:`; + const result = db + .prepare( + "DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND substr(key, 1, ?) = ?" + ) + .run(keyPrefix.length, keyPrefix); + backupDbFile("pre-write"); + return Number(result.changes || 0); +} + export async function updateCustomModel( providerId: string, modelId: string, diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index f3af94bc83..65a5681e08 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -65,6 +65,7 @@ export { getAllSyncedAvailableModels, replaceSyncedAvailableModelsForConnection, deleteSyncedAvailableModelsForConnection, + deleteSyncedAvailableModelsForProvider, } from "./db/models"; export type { ModelCompatPerProtocol, ModelCompatPatch, SyncedAvailableModel } from "./db/models"; From 4a31a795b9cf2738699e8fdfc9df0026c7218760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=9E=97ObsidianGrove?= Date: Fri, 15 May 2026 02:05:32 +0800 Subject: [PATCH 3/3] test: cover provider synced model deletion Add regression coverage for clearing provider-scoped synced available models without affecting other providers. Co-Authored-By: Claude Opus 4.7 --- tests/unit/managed-model-import.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/managed-model-import.test.ts b/tests/unit/managed-model-import.test.ts index 84be9ff37c..0a09fe12ff 100644 --- a/tests/unit/managed-model-import.test.ts +++ b/tests/unit/managed-model-import.test.ts @@ -64,3 +64,23 @@ test("merge mode builds aliases from discovered models without pruning missing p assert.equal(aliases["model-a"], undefined); assert.equal(aliases["model-b"], "openrouter/shared/model-b"); }); + +test("provider-level synced model deletion removes only that provider", async () => { + await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", "conn-a", [ + { id: "shared/model-a", name: "Model A", source: "imported" }, + ]); + await modelsDb.replaceSyncedAvailableModelsForConnection("openrouter", "conn-b", [ + { id: "shared/model-b", name: "Model B", source: "imported" }, + ]); + await modelsDb.replaceSyncedAvailableModelsForConnection("openai", "conn-a", [ + { id: "shared/model-c", name: "Model C", source: "imported" }, + ]); + + const removed = await modelsDb.deleteSyncedAvailableModelsForProvider("openrouter"); + + assert.equal(removed, 2); + assert.deepEqual(await modelsDb.getSyncedAvailableModels("openrouter"), []); + assert.deepEqual(await modelsDb.getSyncedAvailableModels("openai"), [ + { id: "shared/model-c", name: "Model C", source: "imported" }, + ]); +});