diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 50b9ca84b1..284d319e44 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -2735,10 +2735,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 { @@ -2764,10 +2761,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 { @@ -4281,7 +4275,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, @@ -4380,7 +4381,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} @@ -5350,7 +5351,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} 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"; 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" }, + ]); +});