diff --git a/.github/instructions/project-structure.instructions.md b/.github/instructions/project-structure.instructions.md
index b12d68cf..995f8402 100644
--- a/.github/instructions/project-structure.instructions.md
+++ b/.github/instructions/project-structure.instructions.md
@@ -59,7 +59,6 @@ applyTo: '**/*'
│ │ │ └── helpers/ # JSON column helpers, backups
│ │ ├── localDb.js # Backward-compat shim → re-exports @/lib/db/index.js
│ │ ├── usageDb.js # Usage + log persistence (~/.9router/usage.json, log.txt)
-│ │ ├── disabledModelsDb.js # Disabled models DB
│ │ ├── requestDetailsDb.js # Request detail logging DB
│ │ ├── oauth/ # OAuth flow helpers
│ │ ├── headroom/ # Headroom token compression
diff --git a/public/providers/ag.png b/public/providers/ag.png
new file mode 100644
index 00000000..ce40c20f
Binary files /dev/null and b/public/providers/ag.png differ
diff --git a/public/providers/cf.png b/public/providers/cf.png
new file mode 100644
index 00000000..2ec56991
Binary files /dev/null and b/public/providers/cf.png differ
diff --git a/public/providers/cx.png b/public/providers/cx.png
new file mode 100644
index 00000000..12df8bb1
Binary files /dev/null and b/public/providers/cx.png differ
diff --git a/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js b/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js
index b18a3048..47636dd5 100644
--- a/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js
+++ b/src/app/(dashboard)/dashboard/basic-chat/BasicChatPageClient.js
@@ -165,7 +165,7 @@ export default function BasicChatPageClient() {
const providerMap = new Map();
for (const model of modelsData.models || []) {
- if (model.disabled || !model.fullModel) continue;
+ if (!model.fullModel) continue;
const providerId = model.provider?.id || model.providerAlias;
const providerName = model.provider?.name || humanize(providerId);
diff --git a/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js b/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js
index cbbe8b54..fc13b01a 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/[toolId]/ToolDetailClient.js
@@ -45,7 +45,7 @@ export default function ToolDetailClient({ toolId, machineId }) {
}
if (modelsRes.ok) {
const data = await modelsRes.json();
- setAvailableModels((data.models || []).filter((model) => !model.disabled));
+ setAvailableModels(data.models || []);
}
if (configRes.ok) {
const data = await configRes.json();
diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js
index bd88a6e8..b5c03d7f 100644
--- a/src/app/(dashboard)/dashboard/combos/page.js
+++ b/src/app/(dashboard)/dashboard/combos/page.js
@@ -54,10 +54,8 @@ export default function CombosPage() {
}
}
- // The Models page is the source of truth for eligible models. Administrators
- // can still see disabled rows there to manage them, but disabled models must
- // not be added to new or edited combos because they cannot serve requests.
- const selectableModels = connectedModels.filter((model) => !model.disabled);
+ // The Models page is the source of truth for eligible models.
+ const selectableModels = connectedModels;
const handleCreate = async (data) => {
try {
diff --git a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js
index e3f5afb4..ca54992d 100644
--- a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js
+++ b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js
@@ -35,7 +35,7 @@ export default function MitmPageClient() {
setConnections(connectionsData.connections || []);
setApiKeys(keysData.keys || []);
setModelAliases(aliasesData.aliases || {});
- setAvailableModels((modelsData.models || []).filter((model) => !model.disabled));
+ setAvailableModels(modelsData.models || []);
setCloudEnabled(settingsData.cloudEnabled || false);
}).catch(() => {});
diff --git a/src/app/(dashboard)/dashboard/models/page.js b/src/app/(dashboard)/dashboard/models/page.js
index 8fefb0c7..a4a96b06 100644
--- a/src/app/(dashboard)/dashboard/models/page.js
+++ b/src/app/(dashboard)/dashboard/models/page.js
@@ -3,16 +3,12 @@
import { useEffect, useMemo, useState } from "react";
import {
Badge,
- Button,
Card,
CardSkeleton,
CapacityBadges,
- Toggle,
} from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
-import useUserStore from "@/store/userStore";
-import { useNotificationStore } from "@/store/notificationStore";
function groupModelsByProvider(models) {
return models.reduce((groups, model) => {
@@ -28,17 +24,9 @@ function groupModelsByProvider(models) {
}, {});
}
-function ProviderModelsCard({ group, canManage, onSetModelDisabled, onSetModelsDisabled, pendingIds }) {
+function ProviderModelsCard({ group }) {
const [expanded, setExpanded] = useState(true);
const { copied, copy } = useCopyToClipboard();
- const isUpdatingGroup = group.models.some((model) => pendingIds.has(model.fullModel));
- const enabledCount = group.models.filter((model) => !model.disabled).length;
- const disabledCount = group.models.length - enabledCount;
-
- const setAllModelsDisabled = (disabled) => {
- const modelsToUpdate = group.models.filter((model) => model.disabled !== disabled);
- if (modelsToUpdate.length > 0) onSetModelsDisabled(group.provider.alias, modelsToUpdate, disabled);
- };
return (
@@ -72,38 +60,14 @@ function ProviderModelsCard({ group, canManage, onSetModelDisabled, onSetModelsD
expand_more
- {canManage ? (
-
- setAllModelsDisabled(false)}
- >
- Enable all
-
- setAllModelsDisabled(true)}
- >
- Disable all
-
-
- ) : null}
{expanded ? (
- {group.models.map((model) => {
- const isPending = pendingIds.has(model.fullModel);
- return (
+ {group.models.map((model) => (
@@ -130,19 +94,9 @@ function ProviderModelsCard({ group, canManage, onSetModelDisabled, onSetModelsD
{model.alias !== model.model ? {model.alias} : null}
- {canManage ? (
- onSetModelDisabled(group.provider.alias, model, !enabled)}
- className="shrink-0"
- />
- ) : null}
- );
- })}
+ ))}
) : null}
@@ -150,18 +104,10 @@ function ProviderModelsCard({ group, canManage, onSetModelDisabled, onSetModelsD
}
export default function ModelsPage() {
- const user = useUserStore((state) => state.user);
- const fetchCurrentUser = useUserStore((state) => state.fetchCurrentUser);
- const notify = useNotificationStore();
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
- const [pendingIds, setPendingIds] = useState(new Set());
-
- useEffect(() => {
- if (!user) fetchCurrentUser();
- }, [fetchCurrentUser, user]);
useEffect(() => {
const loadModels = async () => {
@@ -195,43 +141,6 @@ export default function ModelsPage() {
.sort((a, b) => a.provider.name.localeCompare(b.provider.name));
}, [models, search]);
- const setModelDisabled = async (providerAlias, model, disabled) => {
- await setModelsDisabled(providerAlias, [model], disabled);
- };
-
- const setModelsDisabled = async (providerAlias, modelsToUpdate, disabled) => {
- const ids = modelsToUpdate.map((model) => model.fullModel);
- const modelIds = modelsToUpdate.map((model) => model.model);
- if (ids.length === 0) return;
-
- setPendingIds((current) => new Set([...current, ...ids]));
- setModels((current) => current.map((item) => (
- ids.includes(item.fullModel) ? { ...item, disabled } : item
- )));
-
- try {
- const response = await fetch("/api/models/connected", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ providerAlias, modelIds, disabled }),
- });
- const data = await response.json();
- if (!response.ok) throw new Error(data.error || "Failed to update model");
- notify.success(`${modelIds.length} model${modelIds.length === 1 ? "" : "s"} ${disabled ? "disabled" : "enabled"}.`);
- } catch (updateError) {
- setModels((current) => current.map((item) => (
- ids.includes(item.fullModel) ? { ...item, disabled: !disabled } : item
- )));
- notify.error(updateError.message);
- } finally {
- setPendingIds((current) => {
- const next = new Set(current);
- ids.forEach((id) => next.delete(id));
- return next;
- });
- }
- };
-
if (loading) {
return (
@@ -242,7 +151,6 @@ export default function ModelsPage() {
}
const providerCount = new Set(models.map((model) => model.providerAlias)).size;
- const canManage = user?.role === "admin";
return (
@@ -253,9 +161,7 @@ export default function ModelsPage() {
{models.length}
- {canManage
- ? "Manage models available from connected providers."
- : "Browse models currently available through connected providers."}
+ Browse models currently available through connected providers.
@@ -306,10 +212,6 @@ export default function ModelsPage() {
))}
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js b/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js
index 63c64568..896df832 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/ModelRow.js
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { CapacityBadges } from "@/shared/components";
import ActionMenu from "@/shared/components/ActionMenu";
-export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onRemove, onTest, isTesting, onDisable, caps, thinkingSuffix }) {
+export default function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onRemove, onTest, isTesting, onDeleteModel, caps, thinkingSuffix }) {
const displayModel = thinkingSuffix ? `${fullModel}(${thinkingSuffix})` : fullModel;
const borderColor = testStatus === "ok"
? "border-green-500/40"
@@ -15,7 +15,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
: testStatus === "error"
? "#ef4444"
: undefined;
- const deleteModel = onRemove || (isCustom ? onDeleteAlias : onDisable);
+ const deleteModel = onRemove || (isCustom ? onDeleteAlias : onDeleteModel);
const deletesPermanently = !!deleteModel;
const actionItems = [
...(onTest ? [{
@@ -81,7 +81,7 @@ ModelRow.propTypes = {
onRemove: PropTypes.func,
onTest: PropTypes.func,
isTesting: PropTypes.bool,
- onDisable: PropTypes.func,
+ onDeleteModel: PropTypes.func,
caps: PropTypes.object,
thinkingSuffix: PropTypes.string,
};
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js
index 4c18895b..e73aa531 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js
@@ -67,7 +67,6 @@ export default function ProviderDetailPage() {
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
const [suggestedModels, setSuggestedModels] = useState([]);
const [kiloFreeModels, setKiloFreeModels] = useState([]);
- const [disabledModelIds, setDisabledModelIds] = useState([]);
const [deletedModelIds, setDeletedModelIds] = useState([]);
const [confirmState, setConfirmState] = useState(null);
const [showAgRiskModal, setShowAgRiskModal] = useState(false);
@@ -185,17 +184,13 @@ export default function ProviderDetailPage() {
? (providerNode?.prefix || providerId)
: providerAlias;
- const fetchDisabledModels = useCallback(async () => {
+ const fetchDeletedModels = useCallback(async () => {
try {
- const [disabledRes, deletedRes] = await Promise.all([
- fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { cache: "no-store" }),
- fetch(`/api/models/delete?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { cache: "no-store" }),
- ]);
- const [disabledData, deletedData] = await Promise.all([disabledRes.json(), deletedRes.json()]);
- if (disabledRes.ok) setDisabledModelIds(disabledData.ids || []);
+ const deletedRes = await fetch(`/api/models/delete?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { cache: "no-store" });
+ const deletedData = await deletedRes.json();
if (deletedRes.ok) setDeletedModelIds(deletedData.ids || []);
} catch (error) {
- console.log("Error fetching disabled models:", error);
+ console.log("Error fetching deleted models:", error);
}
}, [providerStorageAlias]);
@@ -220,7 +215,7 @@ export default function ProviderDetailPage() {
return;
}
- await Promise.all([fetchAliases(), fetchCustomModels(), fetchDisabledModels()]);
+ await Promise.all([fetchAliases(), fetchCustomModels(), fetchDeletedModels()]);
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("customModelChanged"));
} catch (error) {
console.log("Error permanently deleting model:", error);
@@ -230,36 +225,6 @@ export default function ProviderDetailPage() {
});
};
- const handleDisableAll = async (ids) => {
- if (!ids.length) return;
- setConfirmState({
- title: "Disable All Models",
- message: `Disable all ${ids.length} model(s)?`,
- onConfirm: async () => {
- setConfirmState(null);
- try {
- const res = await fetch("/api/models/disabled", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ providerAlias: providerStorageAlias, ids }),
- });
- if (res.ok) await fetchDisabledModels();
- } catch (error) {
- console.log("Error disabling all models:", error);
- }
- }
- });
- };
-
- const handleEnableAll = async () => {
- try {
- const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { method: "DELETE" });
- if (res.ok) await fetchDisabledModels();
- } catch (error) {
- console.log("Error enabling all models:", error);
- }
- };
-
// Define callbacks BEFORE the useEffect that uses them
const fetchAliases = useCallback(async () => {
try {
@@ -459,8 +424,8 @@ export default function ProviderDetailPage() {
fetchConnections();
fetchAliases();
fetchCustomModels();
- fetchDisabledModels();
- }, [fetchConnections, fetchAliases, fetchCustomModels, fetchDisabledModels]);
+ fetchDeletedModels();
+ }, [fetchConnections, fetchAliases, fetchCustomModels, fetchDeletedModels]);
// Fetch suggested models from provider's public API (if configured)
useEffect(() => {
@@ -1069,7 +1034,6 @@ export default function ProviderDetailPage() {
...models,
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; });
- const disabledSet = new Set(disabledModelIds);
const addedModelIds = new Set(
customModels
.filter((entry) => (
@@ -1080,8 +1044,7 @@ export default function ProviderDetailPage() {
.map((entry) => entry.id),
);
const deletedSet = new Set(deletedModelIds);
- const displayModels = allModels.filter((model) => !disabledSet.has(model.id) && !deletedSet.has(model.id));
- const disabledDisplayModels = allModels.filter((model) => disabledSet.has(model.id) && !deletedSet.has(model.id));
+ const displayModels = allModels.filter((model) => !deletedSet.has(model.id));
const customModelRows = getProviderCustomModelRows({
customModels,
modelAliases,
@@ -1138,7 +1101,7 @@ export default function ProviderDetailPage() {
onRemove={addedModelIds.has(model.id)
? () => handlePermanentlyDeleteModel(model.id, providerStorageAlias)
: undefined}
- onDisable={() => handlePermanentlyDeleteModel(model.id, providerStorageAlias)}
+ onDeleteModel={() => handlePermanentlyDeleteModel(model.id, providerStorageAlias)}
caps={getCaps(`${providerId}/${model.id}`)}
thinkingSuffix={resolveThinkingSuffix(model.id)}
/>
@@ -1201,33 +1164,6 @@ export default function ProviderDetailPage() {
);
})()}
- {/* Disabled models stay restorable; permanently deleted models do not. */}
- {disabledDisplayModels.length > 0 && (
-
-
Disabled models ({disabledDisplayModels.length}):
-
- {disabledDisplayModels.map((model) => (
- {
- try {
- const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}&id=${encodeURIComponent(model.id)}`, { method: "DELETE" });
- if (res.ok) await fetchDisabledModels();
- } catch (error) {
- console.log("Error enabling model:", error);
- }
- }}
- className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg border border-dashed border-black/10 dark:border-white/10 text-xs text-text-muted hover:text-primary hover:border-primary/40 hover:bg-primary/5 transition-colors"
- title="Restore model"
- >
- add
- {model.id}
-
- ))}
-
-
- )}
-
);
};
@@ -1641,27 +1577,6 @@ export default function ProviderDetailPage() {
)}
- {!isCompatible && (() => {
- const allIds = [
- ...models,
- ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
- ].filter((m) => { const k = getModelKind(m); return !k || k === "llm"; }).map((m) => m.id);
- const activeIds = allIds.filter((id) => !disabledModelIds.includes(id));
- return (
-
- {disabledModelIds.length > 0 && (
-
- Active All
-
- )}
- {activeIds.length > 0 && (
- handleDisableAll(activeIds)}>
- Disable All
-
- )}
-
- );
- })()}
{!!modelsTestError && (
{modelsTestError}
diff --git a/src/app/api/models/connected/route.js b/src/app/api/models/connected/route.js
index 99b930b9..54aaec16 100644
--- a/src/app/api/models/connected/route.js
+++ b/src/app/api/models/connected/route.js
@@ -6,9 +6,8 @@ import {
getProviderNodes,
} from "@/models";
import { getUsers } from "@/lib/db";
-import { disableModels, enableModels, getDisabledModels } from "@/lib/disabledModelsDb";
import { getDeletedModels } from "@/lib/db";
-import { requireAdminUser, requireUsageDashboardUser } from "@/lib/auth/currentUser";
+import { requireUsageDashboardUser } from "@/lib/auth/currentUser";
import {
AI_PROVIDERS,
getProviderAlias,
@@ -73,15 +72,6 @@ function getCompatibleProviderLabel(providerId, node, connection) {
};
}
-function getModelDisabledState(disabledModels, storageAlias, providerEntry, modelId) {
- const disabled = new Set([
- ...(disabledModels[storageAlias] || []),
- ...(disabledModels[providerEntry.providerId] || []),
- ...(disabledModels[providerEntry.providerAlias] || []),
- ]);
- return disabled.has(modelId);
-}
-
function isDeletedModelId(deletedModels, storageAlias, providerEntry, modelId) {
const deleted = new Set([
...(deletedModels[storageAlias] || []),
@@ -95,7 +85,6 @@ function isDeletedModelId(deletedModels, storageAlias, providerEntry, modelId) {
}
function createConnectedModel({
- disabledModels,
deletedModels,
fullModel,
isCustom,
@@ -115,7 +104,6 @@ function createConnectedModel({
name: name || modelId,
fullModel,
alias: modelAliases.get(fullModel) || modelId,
- disabled: getModelDisabledState(disabledModels, storageAlias, providerEntry, modelId),
isCustom,
caps: {
vision: caps.vision,
@@ -140,12 +128,11 @@ function getForbiddenResponse(error) {
// customModels records extend the catalog and can provide administrator names.
export async function GET() {
try {
- const user = await requireUsageDashboardUser();
+ await requireUsageDashboardUser();
- const [connections, customModels, disabledModels, deletedModels, modelAliases, providerNodes, users] = await Promise.all([
+ const [connections, customModels, deletedModels, modelAliases, providerNodes, users] = await Promise.all([
getProviderConnections(),
getCustomModels(),
- getDisabledModels(),
getDeletedModels(),
getModelAliases(),
getProviderNodes(),
@@ -221,7 +208,6 @@ export async function GET() {
if (seenFullModels.has(fullModel)) return;
seenFullModels.add(fullModel);
const connectedModel = createConnectedModel({
- disabledModels,
deletedModels,
fullModel,
isCustom,
@@ -273,7 +259,6 @@ export async function GET() {
}
const visibleModels = models
- .filter((model) => user.role === "admin" || !model.disabled)
.sort((a, b) => (
a.provider.name.localeCompare(b.provider.name)
|| a.name.localeCompare(b.name)
@@ -289,38 +274,3 @@ export async function GET() {
return NextResponse.json({ error: "Failed to fetch connected models" }, { status: 500 });
}
}
-
-// PUT /api/models/connected - Enable or disable one or more models for a provider.
-export async function PUT(request) {
- try {
- await requireAdminUser();
-
- const { providerAlias, modelId, modelIds, disabled } = await request.json();
- const ids = Array.isArray(modelIds)
- ? modelIds
- : modelId
- ? [modelId]
- : [];
-
- if (!providerAlias || typeof disabled !== "boolean" || ids.length === 0 || ids.some((id) => typeof id !== "string" || !id)) {
- return NextResponse.json(
- { error: "providerAlias, disabled, and modelId or modelIds[] are required" },
- { status: 400 },
- );
- }
-
- if (disabled) {
- await disableModels(providerAlias, ids);
- } else {
- await enableModels(providerAlias, ids);
- }
-
- return NextResponse.json({ success: true, providerAlias, ids, disabled });
- } catch (error) {
- const accessError = getForbiddenResponse(error);
- if (accessError) return accessError;
-
- console.log("Error updating connected models:", error);
- return NextResponse.json({ error: "Failed to update connected models" }, { status: 500 });
- }
-}
\ No newline at end of file
diff --git a/src/app/api/models/disabled/route.js b/src/app/api/models/disabled/route.js
deleted file mode 100644
index 1e511489..00000000
--- a/src/app/api/models/disabled/route.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { NextResponse } from "next/server";
-import { getDisabledModels, disableModels, enableModels } from "@/lib/disabledModelsDb";
-import { requireAdminUser } from "@/lib/auth/currentUser";
-
-export const dynamic = "force-dynamic";
-
-function getAccessErrorResponse(error) {
- if (error.message === "Unauthorized") {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
- if (error.message === "Forbidden") {
- return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
- }
- return null;
-}
-
-// GET /api/models/disabled?providerAlias=xxx
-export async function GET(request) {
- try {
- const { searchParams } = new URL(request.url);
- const providerAlias = searchParams.get("providerAlias");
- const all = await getDisabledModels();
- if (providerAlias) return NextResponse.json({ ids: all[providerAlias] || [] });
- return NextResponse.json({ disabled: all });
- } catch (error) {
- console.log("Error fetching disabled models:", error);
- return NextResponse.json({ error: "Failed to fetch disabled models" }, { status: 500 });
- }
-}
-
-// POST /api/models/disabled body: { providerAlias, ids: [...] }
-export async function POST(request) {
- try {
- await requireAdminUser();
-
- const { providerAlias, ids } = await request.json();
- if (!providerAlias || !Array.isArray(ids)) {
- return NextResponse.json({ error: "providerAlias and ids[] required" }, { status: 400 });
- }
- await disableModels(providerAlias, ids);
- return NextResponse.json({ success: true });
- } catch (error) {
- const accessError = getAccessErrorResponse(error);
- if (accessError) return accessError;
-
- console.log("Error disabling models:", error);
- return NextResponse.json({ error: "Failed to disable models" }, { status: 500 });
- }
-}
-
-// DELETE /api/models/disabled?providerAlias=xxx[&id=yyy]
-export async function DELETE(request) {
- try {
- await requireAdminUser();
-
- const { searchParams } = new URL(request.url);
- const providerAlias = searchParams.get("providerAlias");
- const id = searchParams.get("id");
- if (!providerAlias) {
- return NextResponse.json({ error: "providerAlias required" }, { status: 400 });
- }
- await enableModels(providerAlias, id ? [id] : []);
- return NextResponse.json({ success: true });
- } catch (error) {
- const accessError = getAccessErrorResponse(error);
- if (accessError) return accessError;
-
- console.log("Error enabling models:", error);
- return NextResponse.json({ error: "Failed to enable models" }, { status: 500 });
- }
-}
diff --git a/src/app/api/models/route.js b/src/app/api/models/route.js
index 9607fd2e..6b61e381 100644
--- a/src/app/api/models/route.js
+++ b/src/app/api/models/route.js
@@ -1,6 +1,5 @@
import { NextResponse } from "next/server";
import { getModelAliases, setModelAlias } from "@/models";
-import { getDisabledModels } from "@/lib/disabledModelsDb";
import { getDeletedModels, isDeletedModelReference } from "@/lib/db";
import { AI_MODELS } from "@/shared/constants/config";
import { getProviderAlias } from "@/shared/constants/providers";
@@ -10,14 +9,13 @@ import { getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
export async function GET() {
try {
const modelAliases = await getModelAliases();
- const [disabled, deleted] = await Promise.all([getDisabledModels(), getDeletedModels()]);
+ const deleted = await getDeletedModels();
const models = AI_MODELS
.filter((m) => {
const alias = getProviderAlias(m.provider) || m.provider;
- const list = disabled[alias] || disabled[m.provider] || [];
const deletedIds = [...(deleted[alias] || []), ...(deleted[m.provider] || [])];
- return !list.includes(m.model) && !deletedIds.some((id) => (
+ return !deletedIds.some((id) => (
m.model === id || (m.model.startsWith(`${id}(`) && m.model.endsWith(")"))
));
})
diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js
index db8db2b3..1f37fb24 100644
--- a/src/app/api/v1/models/route.js
+++ b/src/app/api/v1/models/route.js
@@ -6,7 +6,6 @@ import {
isOpenAICompatibleProvider,
} from "@/shared/constants/providers";
import { getApiKeyByKey, getProviderConnections, getCombos, getCustomModels, getModelAliases } from "@/lib/localDb";
-import { getDisabledModels } from "@/lib/disabledModelsDb";
import { getDeletedModels } from "@/lib/db";
import { resolveKiroModels } from "open-sse/services/kiroModels.js";
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
@@ -256,14 +255,6 @@ export async function buildModelsList(kindFilter, ownerOrOptions = {}) {
console.log("Could not fetch model aliases");
}
- let disabledByAlias = {};
- try {
- disabledByAlias = await getDisabledModels();
- } catch (e) {
- console.log("Could not fetch disabled models");
- }
- const isDisabled = (alias, modelId) => Array.isArray(disabledByAlias[alias]) && disabledByAlias[alias].includes(modelId);
-
let deletedByAlias = {};
try {
deletedByAlias = await getDeletedModels();
@@ -311,7 +302,7 @@ export async function buildModelsList(kindFilter, ownerOrOptions = {}) {
if (!providerMatchesKinds(providerId, kindFilter)) continue;
for (const model of providerModels) {
if (!kindFilter.includes(modelKind(model))) continue;
- if (isDisabled(alias, model.id) || isDeleted(model.id, alias, providerId)) continue;
+ if (isDeleted(model.id, alias, providerId)) continue;
models.push({
id: `${alias}/${model.id}`,
object: "model",
@@ -467,9 +458,7 @@ export async function buildModelsList(kindFilter, ownerOrOptions = {}) {
const allowAsLlm = kind === "imageToText" && kindFilter.includes(LLM_KIND);
if (!kindFilter.includes(kind) && !allowAsLlm) continue;
if (
- isDisabled(outputAlias, modelId)
- || isDisabled(staticAlias, modelId)
- || isDeleted(modelId, outputAlias, staticAlias, providerId)
+ isDeleted(modelId, outputAlias, staticAlias, providerId)
) continue;
const model = {
diff --git a/src/lib/db/index.js b/src/lib/db/index.js
index b2cba293..13965bc5 100644
--- a/src/lib/db/index.js
+++ b/src/lib/db/index.js
@@ -78,11 +78,6 @@ export {
getPricing, getPricingForModel, updatePricing, resetPricing, resetAllPricing,
} from "./repos/pricingRepo.js";
-// Disabled models
-export {
- getDisabledModels, getDisabledByProvider, disableModels, enableModels,
-} from "./repos/disabledModelsRepo.js";
-
// Permanently deleted models
export {
getDeletedModels, isDeletedModel, isDeletedModelReference, deleteModelPermanently,
@@ -120,7 +115,6 @@ export async function exportDb() {
customModels: [],
mitmAlias: {},
pricing: {},
- disabledModels: {},
deletedModels: {},
};
@@ -128,7 +122,6 @@ export async function exportDb() {
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'customModels'`)) out.customModels.push(parseJson(r.value));
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'mitmAlias'`)) out.mitmAlias[r.key] = parseJson(r.value);
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'pricing'`)) out.pricing[r.key] = parseJson(r.value);
- for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'disabledModels'`)) out.disabledModels[r.key] = parseJson(r.value, []);
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'deletedModels'`)) out.deletedModels[r.key] = parseJson(r.value, []);
return out;
@@ -162,7 +155,7 @@ export async function importDb(payload) {
db.run(`DELETE FROM proxyPools`);
db.run(`DELETE FROM apiKeys`);
db.run(`DELETE FROM combos`);
- db.run(`DELETE FROM kv WHERE scope IN ('modelAliases', 'customModels', 'mitmAlias', 'pricing', 'disabledModels', 'deletedModels')`);
+ db.run(`DELETE FROM kv WHERE scope IN ('modelAliases', 'customModels', 'mitmAlias', 'pricing', 'deletedModels')`);
// Settings
if (payload.settings) {
@@ -261,14 +254,6 @@ export async function importDb(payload) {
for (const [provider, models] of Object.entries(payload.pricing || {})) {
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('pricing', ?, ?)`, [provider, stringifyJson(models || {})]);
}
- for (const [providerAlias, modelIds] of Object.entries(payload.disabledModels || {})) {
- const validModelIds = Array.isArray(modelIds)
- ? modelIds.filter((modelId) => typeof modelId === "string" && modelId)
- : [];
- if (providerAlias && validModelIds.length > 0) {
- db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('disabledModels', ?, ?)`, [providerAlias, stringifyJson([...new Set(validModelIds)])]);
- }
- }
for (const [providerAlias, modelIds] of Object.entries(payload.deletedModels || {})) {
const validModelIds = Array.isArray(modelIds)
? modelIds.filter((modelId) => typeof modelId === "string" && modelId)
diff --git a/src/lib/db/migrate.js b/src/lib/db/migrate.js
index 170b05b7..96dee452 100644
--- a/src/lib/db/migrate.js
+++ b/src/lib/db/migrate.js
@@ -206,13 +206,6 @@ function importLegacyUsage(adapter, data) {
}
}
-function importLegacyDisabled(adapter, data) {
- if (!data || typeof data.disabled !== "object") return;
- for (const [provider, ids] of Object.entries(data.disabled)) {
- adapter.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('disabledModels', ?, ?)`, [provider, stringifyJson(ids || [])]);
- }
-}
-
function importLegacyDetails(adapter, data) {
if (!data || !Array.isArray(data.records)) return;
for (const r of data.records) {
@@ -267,9 +260,8 @@ export async function runMigrationOnce(adapter) {
const alreadyImported = fs.existsSync(MIGRATED_MARKER);
const legacyMain = readJsonSafe(LEGACY_FILES.main);
const legacyUsage = readJsonSafe(LEGACY_FILES.usage);
- const legacyDisabled = readJsonSafe(LEGACY_FILES.disabled);
const legacyDetails = readJsonSafe(LEGACY_FILES.details);
- const hasLegacy = !!(legacyMain || legacyUsage || legacyDisabled || legacyDetails);
+ const hasLegacy = !!(legacyMain || legacyUsage || legacyDetails);
if (fresh && hasLegacy && !alreadyImported) {
const t0 = Date.now();
@@ -280,7 +272,6 @@ export async function runMigrationOnce(adapter) {
adapter.transaction(() => {
importLegacyMain(adapter, legacyMain);
importLegacyUsage(adapter, legacyUsage);
- importLegacyDisabled(adapter, legacyDisabled);
importLegacyDetails(adapter, legacyDetails);
setMetaSync(adapter, "appVersion", getAppVersion());
setMetaSync(adapter, "backupSchemaVersion", SCHEMA_VERSION);
diff --git a/src/lib/db/paths.js b/src/lib/db/paths.js
index 5757f7b4..ab98c817 100644
--- a/src/lib/db/paths.js
+++ b/src/lib/db/paths.js
@@ -8,7 +8,6 @@ export const BACKUPS_DIR = path.join(DB_DIR, "backups");
export const LEGACY_FILES = {
main: path.join(DATA_DIR, "db.json"),
usage: path.join(DATA_DIR, "usage.json"),
- disabled: path.join(DATA_DIR, "disabledModels.json"),
details: path.join(DATA_DIR, "request-details.json"),
};
export function ensureDirs() {
diff --git a/src/lib/db/repos/deletedModelsRepo.js b/src/lib/db/repos/deletedModelsRepo.js
index 4f96ace2..d83d8282 100644
--- a/src/lib/db/repos/deletedModelsRepo.js
+++ b/src/lib/db/repos/deletedModelsRepo.js
@@ -44,7 +44,7 @@ function modelWhereClause(column, modelId) {
function removeModelFromKvScopesSync(db, providerAliases, modelId) {
const aliases = new Set(providerAliases);
- const result = { aliases: 0, customModels: 0, pricing: 0, disabledModels: 0 };
+ const result = { aliases: 0, customModels: 0, pricing: 0 };
const modelAliasRows = db.all(`SELECT key, value FROM kv WHERE scope = 'modelAliases'`);
for (const row of modelAliasRows) {
@@ -76,20 +76,6 @@ function removeModelFromKvScopesSync(db, providerAliases, modelId) {
}
}
- const disabledRows = db.all(`SELECT key, value FROM kv WHERE scope = 'disabledModels'`);
- for (const row of disabledRows) {
- if (!aliases.has(row.key)) continue;
- const current = normalizeIds(parseJson(row.value, []));
- const next = current.filter((storedModelId) => !matchesDeletedModelId(storedModelId, modelId));
- result.disabledModels += current.length - next.length;
- if (next.length === current.length) continue;
- if (next.length === 0) {
- db.run(`DELETE FROM kv WHERE scope = 'disabledModels' AND key = ?`, [row.key]);
- } else {
- db.run(`UPDATE kv SET value = ? WHERE scope = 'disabledModels' AND key = ?`, [stringifyJson(next), row.key]);
- }
- }
-
return result;
}
@@ -337,7 +323,6 @@ export async function deleteModelPermanently(providerAlias, modelId) {
removedAliases: kv.aliases,
removedCustomModels: kv.customModels,
removedPricingEntries: kv.pricing,
- removedDisabledModels: kv.disabledModels,
updatedComboIds: combos.updatedComboIds,
deletedComboIds: combos.deletedComboIds,
updatedCliToolConfigs,
diff --git a/src/lib/db/repos/disabledModelsRepo.js b/src/lib/db/repos/disabledModelsRepo.js
deleted file mode 100644
index 9f1675fc..00000000
--- a/src/lib/db/repos/disabledModelsRepo.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { getAdapter } from "../driver.js";
-import { parseJson, stringifyJson } from "../helpers/jsonCol.js";
-
-const SCOPE = "disabledModels";
-
-export async function getDisabledModels() {
- const db = await getAdapter();
- const rows = db.all(`SELECT key, value FROM kv WHERE scope = ?`, [SCOPE]);
- const out = {};
- for (const r of rows) out[r.key] = parseJson(r.value, []);
- return out;
-}
-
-export async function getDisabledByProvider(providerAlias) {
- const db = await getAdapter();
- const row = db.get(`SELECT value FROM kv WHERE scope = ? AND key = ?`, [SCOPE, providerAlias]);
- return row ? (parseJson(row.value, []) || []) : [];
-}
-
-// Atomic read-merge-write inside a transaction (no JS yield mid-transaction).
-export async function disableModels(providerAlias, ids) {
- if (!providerAlias || !Array.isArray(ids)) return;
- const db = await getAdapter();
- db.transaction(() => {
- const row = db.get(`SELECT value FROM kv WHERE scope = ? AND key = ?`, [SCOPE, providerAlias]);
- const current = row ? (parseJson(row.value, []) || []) : [];
- const merged = [...new Set([...current, ...ids])];
- db.run(
- `INSERT INTO kv(scope, key, value) VALUES(?, ?, ?) ON CONFLICT(scope, key) DO UPDATE SET value = excluded.value`,
- [SCOPE, providerAlias, stringifyJson(merged)]
- );
- });
-}
-
-export async function enableModels(providerAlias, ids) {
- if (!providerAlias) return;
- const db = await getAdapter();
- db.transaction(() => {
- if (!Array.isArray(ids) || ids.length === 0) {
- db.run(`DELETE FROM kv WHERE scope = ? AND key = ?`, [SCOPE, providerAlias]);
- return;
- }
- const row = db.get(`SELECT value FROM kv WHERE scope = ? AND key = ?`, [SCOPE, providerAlias]);
- const current = row ? (parseJson(row.value, []) || []) : [];
- const removeSet = new Set(ids);
- const next = current.filter((id) => !removeSet.has(id));
- if (next.length === 0) {
- db.run(`DELETE FROM kv WHERE scope = ? AND key = ?`, [SCOPE, providerAlias]);
- } else {
- db.run(
- `INSERT INTO kv(scope, key, value) VALUES(?, ?, ?) ON CONFLICT(scope, key) DO UPDATE SET value = excluded.value`,
- [SCOPE, providerAlias, stringifyJson(next)]
- );
- }
- });
-}
diff --git a/src/lib/disabledModelsDb.js b/src/lib/disabledModelsDb.js
deleted file mode 100644
index 044e0149..00000000
--- a/src/lib/disabledModelsDb.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Shim → re-export from new SQLite-based DB layer (src/lib/db/)
-export {
- getDisabledModels, getDisabledByProvider, disableModels, enableModels,
-} from "@/lib/db/index.js";
diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js
index d3331109..d076ced1 100644
--- a/src/shared/components/ModelSelectModal.js
+++ b/src/shared/components/ModelSelectModal.js
@@ -48,7 +48,6 @@ export default function ModelSelectModal({
const [combos, setCombos] = useState([]);
const [providerNodes, setProviderNodes] = useState([]);
const [customModels, setCustomModels] = useState([]);
- const [disabledModels, setDisabledModels] = useState({});
const [deletedModels, setDeletedModels] = useState({});
const fetchCombos = async () => {
@@ -102,28 +101,20 @@ export default function ModelSelectModal({
if (isOpen) fetchCustomModels();
}, [isOpen]);
- const fetchDisabledModels = async () => {
+ const fetchDeletedModels = async () => {
try {
- const [disabledRes, deletedRes] = await Promise.all([
- fetch("/api/models/disabled"),
- fetch("/api/models/delete"),
- ]);
- if (!disabledRes.ok) throw new Error(`Failed to fetch disabled models: ${disabledRes.status}`);
- const [disabledData, deletedData] = await Promise.all([
- disabledRes.json(),
- deletedRes.ok ? deletedRes.json() : Promise.resolve({ deleted: {} }),
- ]);
- setDisabledModels(disabledData.disabled || {});
+ const deletedRes = await fetch("/api/models/delete");
+ if (!deletedRes.ok) throw new Error(`Failed to fetch deleted models: ${deletedRes.status}`);
+ const deletedData = await deletedRes.json();
setDeletedModels(deletedData.deleted || {});
} catch (error) {
- console.error("Error fetching disabled models:", error);
- setDisabledModels({});
+ console.error("Error fetching deleted models:", error);
setDeletedModels({});
}
};
useEffect(() => {
- if (isOpen) fetchDisabledModels();
+ if (isOpen) fetchDeletedModels();
}, [isOpen]);
const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...APIKEY_PROVIDERS }), []);
@@ -379,21 +370,15 @@ export default function ModelSelectModal({
}
});
- // Filter out disabled models per provider (disabled keyed by storage alias OR providerId)
+ // Filter out permanently deleted models per provider (keyed by storage alias OR provider ID).
Object.entries(groups).forEach(([providerId, group]) => {
const aliasKey = getProviderAlias(providerId);
- const disabledIds = new Set([
- ...(disabledModels[aliasKey] || []),
- ...(disabledModels[providerId] || []),
- ]);
const deletedIds = new Set([
...(deletedModels[aliasKey] || []),
...(deletedModels[providerId] || []),
]);
- if (disabledIds.size === 0 && deletedIds.size === 0) return;
- group.models = group.models.filter((model) => !disabledIds.has(model.id) && ![
- ...deletedIds,
- ].some((deletedModelId) => (
+ if (deletedIds.size === 0) return;
+ group.models = group.models.filter((model) => ![...deletedIds].some((deletedModelId) => (
model.id === deletedModelId
|| (model.id.startsWith(`${deletedModelId}(`) && model.id.endsWith(")"))
)));
@@ -401,7 +386,7 @@ export default function ModelSelectModal({
});
return groups;
- }, [availableModels, filteredActiveProviders, modelAliases, allProviders, providerNodes, customModels, disabledModels, deletedModels, kindFilter, activeProviders]);
+ }, [availableModels, filteredActiveProviders, modelAliases, allProviders, providerNodes, customModels, deletedModels, kindFilter, activeProviders]);
// Filter combos by search query (and hide combos when kindFilter is set — combos are LLM-only by design)
const filteredCombos = useMemo(() => {
diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js
index 63445e2b..de8d7f47 100644
--- a/src/sse/handlers/chat.js
+++ b/src/sse/handlers/chat.js
@@ -25,7 +25,7 @@ import { detectFormatByEndpoint } from "open-sse/translator/formats.js";
import * as log from "../utils/logger.js";
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
import { getProjectIdForConnection } from "open-sse/services/projectId.js";
-import { getDisabledModelResponse } from "../services/disabledModels.js";
+import { getDeletedModelResponse } from "../services/deletedModels.js";
import { checkUserTokenLimit } from "@/lib/tokenLimitEnforcer.js";
/**
@@ -201,8 +201,8 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
const { provider, model } = modelInfo;
- const disabledModelResponse = await getDisabledModelResponse(provider, model);
- if (disabledModelResponse) return disabledModelResponse;
+ const deletedModelResponse = await getDeletedModelResponse(provider, model);
+ if (deletedModelResponse) return deletedModelResponse;
const tokenLimitResult = await checkUserTokenLimit(ownerId, provider);
if (tokenLimitResult) {
diff --git a/src/sse/handlers/embeddings.js b/src/sse/handlers/embeddings.js
index 66fd030c..16ca5da7 100644
--- a/src/sse/handlers/embeddings.js
+++ b/src/sse/handlers/embeddings.js
@@ -13,7 +13,7 @@ import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import * as log from "../utils/logger.js";
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
-import { getDisabledModelResponse } from "../services/disabledModels.js";
+import { getDeletedModelResponse } from "../services/deletedModels.js";
/**
* Handle embeddings request for the SSE/Next.js server.
@@ -76,8 +76,8 @@ export async function handleEmbeddings(request) {
const { provider, model } = modelInfo;
- const disabledModelResponse = await getDisabledModelResponse(provider, model);
- if (disabledModelResponse) return disabledModelResponse;
+ const deletedModelResponse = await getDeletedModelResponse(provider, model);
+ if (deletedModelResponse) return deletedModelResponse;
if (modelStr !== `${provider}/${model}`) {
log.info("ROUTING", `${modelStr} → ${provider}/${model}`);
diff --git a/src/sse/handlers/imageGeneration.js b/src/sse/handlers/imageGeneration.js
index 5dc4bd95..76b6f81d 100644
--- a/src/sse/handlers/imageGeneration.js
+++ b/src/sse/handlers/imageGeneration.js
@@ -14,7 +14,7 @@ import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
import { handleComboChat } from "open-sse/services/combo.js";
import * as log from "../utils/logger.js";
-import { getDisabledModelResponse } from "../services/disabledModels.js";
+import { getDeletedModelResponse } from "../services/deletedModels.js";
// Providers that don't require credentials (noAuth)
const NO_AUTH_PROVIDERS = new Set(["sdwebui", "comfyui"]);
@@ -78,8 +78,8 @@ async function handleSingleModelImage(body, modelStr, { wantsStream, binaryOutpu
const { provider, model } = modelInfo;
- const disabledModelResponse = await getDisabledModelResponse(provider, model);
- if (disabledModelResponse) return disabledModelResponse;
+ const deletedModelResponse = await getDeletedModelResponse(provider, model);
+ if (deletedModelResponse) return deletedModelResponse;
// noAuth providers — no credential needed
if (NO_AUTH_PROVIDERS.has(provider)) {
diff --git a/src/sse/handlers/stt.js b/src/sse/handlers/stt.js
index 9add0015..56dbdc00 100644
--- a/src/sse/handlers/stt.js
+++ b/src/sse/handlers/stt.js
@@ -9,7 +9,7 @@ import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import * as log from "../utils/logger.js";
-import { getDisabledModelResponse } from "../services/disabledModels.js";
+import { getDeletedModelResponse } from "../services/deletedModels.js";
// Providers requiring credentials for STT
const CREDENTIALED_PROVIDERS = new Set(
@@ -46,8 +46,8 @@ export async function handleStt(request) {
const { provider, model } = modelInfo;
- const disabledModelResponse = await getDisabledModelResponse(provider, model);
- if (disabledModelResponse) return disabledModelResponse;
+ const deletedModelResponse = await getDeletedModelResponse(provider, model);
+ if (deletedModelResponse) return deletedModelResponse;
log.info("ROUTING", `Provider: ${provider}, Model: ${model}`);
diff --git a/src/sse/handlers/tts.js b/src/sse/handlers/tts.js
index 9a8a6c6b..b377d0c3 100644
--- a/src/sse/handlers/tts.js
+++ b/src/sse/handlers/tts.js
@@ -11,7 +11,7 @@ import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { handleComboChat } from "open-sse/services/combo.js";
import * as log from "../utils/logger.js";
-import { getDisabledModelResponse } from "../services/disabledModels.js";
+import { getDeletedModelResponse } from "../services/deletedModels.js";
// Derived from providers.js: any TTS provider not noAuth requires stored credentials
const CREDENTIALED_PROVIDERS = new Set(
@@ -75,8 +75,8 @@ async function handleSingleModelTts(body, modelStr, responseFormat, language, ow
const { provider, model } = modelInfo;
- const disabledModelResponse = await getDisabledModelResponse(provider, model);
- if (disabledModelResponse) return disabledModelResponse;
+ const deletedModelResponse = await getDeletedModelResponse(provider, model);
+ if (deletedModelResponse) return deletedModelResponse;
log.info("ROUTING", `Provider: ${provider}, Voice: ${model}`);
diff --git a/src/sse/services/deletedModels.js b/src/sse/services/deletedModels.js
new file mode 100644
index 00000000..c043ca47
--- /dev/null
+++ b/src/sse/services/deletedModels.js
@@ -0,0 +1,24 @@
+import { isDeletedModel } from "@/lib/db";
+import { errorResponse } from "open-sse/utils/error.js";
+import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
+
+/**
+ * Return a not-found response when a resolved provider/model pair has been
+ * permanently deleted by an administrator.
+ */
+export async function getDeletedModelResponse(provider, model) {
+ try {
+ if (!await isDeletedModel(provider, model)) return null;
+
+ return errorResponse(
+ HTTP_STATUS.NOT_FOUND,
+ `Model ${provider}/${model} has been deleted by an administrator`,
+ );
+ } catch (error) {
+ console.log("Error checking deleted model status:", error);
+ return errorResponse(
+ HTTP_STATUS.SERVER_ERROR,
+ "Unable to verify whether the requested model has been deleted",
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/sse/services/disabledModels.js b/src/sse/services/disabledModels.js
deleted file mode 100644
index d3e01e20..00000000
--- a/src/sse/services/disabledModels.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { getDisabledModels } from "@/lib/disabledModelsDb";
-import { getDeletedModels } from "@/lib/db";
-import { getProviderAlias } from "@/shared/constants/providers";
-import { errorResponse } from "open-sse/utils/error.js";
-import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
-import { stripThinkingSuffix } from "open-sse/translator/concerns/thinkingUnified.js";
-
-/**
- * Return an error response when a resolved provider/model pair has been
- * disabled by an administrator. The check uses both the provider's persisted
- * alias and ID to preserve compatibility with existing disabled-model data.
- *
- * A storage read failure blocks execution rather than risking an accidental
- * bypass of an administrator's disabled-model policy.
- */
-export async function getDisabledModelResponse(provider, model) {
- try {
- const [disabledModels, deletedModels] = await Promise.all([getDisabledModels(), getDeletedModels()]);
- const providerAlias = getProviderAlias(provider) || provider;
- // Thinking variants use a client-facing suffix, e.g. `gpt-5.6-sol(high)`,
- // but dispatch to the base upstream model. Evaluate the disabled policy
- // against that base ID as well so a suffix cannot bypass an admin disable.
- const baseModel = stripThinkingSuffix(model);
- const disabledIds = new Set([
- ...(disabledModels[providerAlias] || []),
- ...(disabledModels[provider] || []),
- ]);
- const deletedIds = new Set([
- ...(deletedModels[providerAlias] || []),
- ...(deletedModels[provider] || []),
- ]);
-
- const matchesDeletedModel = [...deletedIds].some((modelId) => (
- model === modelId
- || baseModel === modelId
- || (model.startsWith(`${modelId}(`) && model.endsWith(")"))
- ));
- if (!matchesDeletedModel && !disabledIds.has(model) && !disabledIds.has(baseModel)) return null;
-
- return errorResponse(
- HTTP_STATUS.NOT_FOUND,
- matchesDeletedModel
- ? `Model ${provider}/${model} has been deleted by an administrator`
- : `Model ${provider}/${model} is disabled by an administrator`,
- );
- } catch (error) {
- console.log("Error checking disabled model status:", error);
- return errorResponse(
- HTTP_STATUS.SERVER_ERROR,
- "Unable to verify whether the requested model is enabled",
- );
- }
-}
\ No newline at end of file
diff --git a/tests/unit/connected-models-route.test.js b/tests/unit/connected-models-route.test.js
index 9bd4687b..fec98a62 100644
--- a/tests/unit/connected-models-route.test.js
+++ b/tests/unit/connected-models-route.test.js
@@ -6,7 +6,6 @@ const getCustomModels = vi.fn();
const getProviderNodes = vi.fn();
const getUsers = vi.fn();
const getDeletedModels = vi.fn();
-const getDisabledModels = vi.fn();
const requireUsageDashboardUser = vi.fn();
const getCapabilitiesForModel = vi.fn();
@@ -19,7 +18,6 @@ vi.mock("@/models", () => ({
vi.mock("@/lib/db", () => ({ getUsers, getDeletedModels }));
-vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels }));
vi.mock("@/lib/auth/currentUser", () => ({
requireUsageDashboardUser,
}));
@@ -27,7 +25,7 @@ vi.mock("open-sse/config/providerModels.js", () => ({
getModelsByProviderId: (providerId) => ({
alpha: [
{ id: "enabled", name: "Enabled model" },
- { id: "disabled", name: "Disabled model" },
+ { id: "alternative", name: "Alternative model" },
],
beta: [{ id: "inactive", name: "Inactive provider model" }],
"orbit-provider": [
@@ -63,16 +61,14 @@ describe("GET /api/models/connected", () => {
getProviderNodes.mockReset();
getUsers.mockReset();
getDeletedModels.mockReset();
- getDisabledModels.mockReset();
requireUsageDashboardUser.mockReset();
getCapabilitiesForModel.mockReset();
getModelAliases.mockResolvedValue({ "preferred-alpha": "alpha-alias/enabled" });
- getDisabledModels.mockResolvedValue({ "alpha-alias": ["disabled"] });
getDeletedModels.mockResolvedValue({});
getCustomModels.mockResolvedValue([
{ providerAlias: "alpha-alias", id: "enabled", name: "Enabled model", type: "llm" },
- { providerAlias: "alpha-alias", id: "disabled", name: "Disabled model", type: "llm" },
+ { providerAlias: "alpha-alias", id: "alternative", name: "Alternative model", type: "llm" },
{ providerAlias: "alpha-alias", id: "embedding", name: "Embedding model", type: "embedding" },
{ providerAlias: "beta-alias", id: "inactive", name: "Inactive provider model", type: "llm" },
]);
@@ -85,7 +81,7 @@ describe("GET /api/models/connected", () => {
]);
});
- it("returns added models for a connected provider to an administrator, including disabled rows", async () => {
+ it("returns added models for a connected provider", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
const response = await GET();
@@ -94,14 +90,12 @@ describe("GET /api/models/connected", () => {
expect(response.status).toBe(200);
expect(body.models).toEqual([
expect.objectContaining({
- fullModel: "alpha-alias/disabled",
+ fullModel: "alpha-alias/alternative",
providerAlias: "alpha-alias",
- disabled: true,
}),
expect.objectContaining({
fullModel: "alpha-alias/enabled",
alias: "preferred-alpha",
- disabled: false,
caps: { vision: false, search: true, reasoning: true },
}),
]);
@@ -126,8 +120,7 @@ describe("GET /api/models/connected", () => {
isCustom: false,
}),
expect.objectContaining({
- fullModel: "alpha-alias/disabled",
- disabled: true,
+ fullModel: "alpha-alias/alternative",
isCustom: false,
}),
]));
@@ -171,16 +164,17 @@ describe("GET /api/models/connected", () => {
expect(body.models.filter((model) => model.fullModel === "orbit/claude-opus-4-8")).toHaveLength(1);
});
- it("excludes disabled models for non-administrators", async () => {
+ it("returns the same catalog to non-administrators", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
const response = await GET();
const body = await response.json();
expect(response.status).toBe(200);
- expect(body.models).toEqual([
- expect.objectContaining({ fullModel: "alpha-alias/enabled", disabled: false }),
- ]);
+ expect(body.models).toEqual(expect.arrayContaining([
+ expect.objectContaining({ fullModel: "alpha-alias/enabled" }),
+ expect.objectContaining({ fullModel: "alpha-alias/alternative" }),
+ ]));
});
it("includes administrator-managed compatible-provider models for non-administrators", async () => {
@@ -224,39 +218,16 @@ describe("GET /api/models/connected", () => {
]));
});
- it("does not expose disabled compatible-provider models to non-administrators", async () => {
- const providerId = "anthropic-compatible-test-node";
- requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
- getProviderConnections.mockResolvedValue([
- { provider: providerId, isActive: true, apiKey: "admin-secret", ownerId: "admin" },
- ]);
- getProviderNodes.mockResolvedValue([
- { id: providerId, type: "anthropic-compatible", name: "Company Anthropic" },
- ]);
- getCustomModels.mockResolvedValue([
- { providerAlias: providerId, id: "claude-company", type: "llm" },
- ]);
- getDisabledModels.mockResolvedValue({ [providerId]: ["claude-company"] });
-
- const response = await GET();
- const body = await response.json();
-
- expect(response.status).toBe(200);
- expect(body.models).not.toEqual(expect.arrayContaining([
- expect.objectContaining({ fullModel: `${providerId}/claude-company` }),
- ]));
- });
-
it("does not expose permanently deleted models to administrators", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
- getDeletedModels.mockResolvedValue({ "alpha-alias": ["disabled", "enabled"] });
+ getDeletedModels.mockResolvedValue({ "alpha-alias": ["alternative", "enabled"] });
const response = await GET();
const body = await response.json();
expect(response.status).toBe(200);
expect(body.models).not.toEqual(expect.arrayContaining([
- expect.objectContaining({ fullModel: "alpha-alias/disabled" }),
+ expect.objectContaining({ fullModel: "alpha-alias/alternative" }),
expect.objectContaining({ fullModel: "alpha-alias/enabled" }),
]));
});
diff --git a/tests/unit/db-concurrent.test.js b/tests/unit/db-concurrent.test.js
index 0b2ebac4..c00b77b9 100644
--- a/tests/unit/db-concurrent.test.js
+++ b/tests/unit/db-concurrent.test.js
@@ -66,7 +66,7 @@ describe("DB Concurrency — atomic safety", () => {
expect(list.pagination.totalItems).toBeGreaterThanOrEqual(N);
}, 15000);
- it("mixed concurrent: usage + details + connections + aliases", async () => {
+ it("mixed concurrent: usage, aliases, and connections", async () => {
const ops = [];
for (let i = 0; i < 50; i++) {
ops.push(db.saveRequestUsage({
@@ -74,16 +74,12 @@ describe("DB Concurrency — atomic safety", () => {
tokens: { prompt_tokens: 20 }, status: "ok",
}));
ops.push(db.setModelAlias(`a-${i}`, `target-${i}`));
- ops.push(db.disableModels("openai", [`d-${i}`]));
}
await Promise.all(ops);
const aliases = await db.getModelAliases();
expect(Object.keys(aliases).filter((k) => k.startsWith("a-")).length).toBe(50);
- const disabled = await db.getDisabledByProvider("openai");
- expect(disabled.length).toBeGreaterThanOrEqual(50);
-
const stats = await db.getUsageStats("24h");
expect(stats.byProvider.anthropic.requests).toBe(50);
}, 30000);
diff --git a/tests/unit/db-sqlite-vs-lowdb.test.js b/tests/unit/db-sqlite-vs-lowdb.test.js
index 9153fe83..c7326ad9 100644
--- a/tests/unit/db-sqlite-vs-lowdb.test.js
+++ b/tests/unit/db-sqlite-vs-lowdb.test.js
@@ -275,15 +275,6 @@ describe("DB SQLite layer — public API parity", () => {
expect(all.cursor).toEqual({ "gpt-5": "claude-3" });
});
- it("disabledModels: add/remove per provider", async () => {
- await sqliteDb.disableModels("openai", ["gpt-3", "gpt-4"]);
- expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(expect.arrayContaining(["gpt-3", "gpt-4"]));
- await sqliteDb.enableModels("openai", ["gpt-3"]);
- expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(["gpt-4"]);
- await sqliteDb.enableModels("openai", []);
- expect(await sqliteDb.getDisabledByProvider("openai")).toEqual([]);
- });
-
it("usage: saveRequestUsage + getUsageHistory + getUsageStats", async () => {
await sqliteDb.saveRequestUsage({
provider: "openai", model: "gpt-4", connectionId: "c1",
@@ -341,6 +332,7 @@ describe("DB SQLite layer — public API parity", () => {
expect(exported.settings).toBeDefined();
expect(Array.isArray(exported.providerConnections)).toBe(true);
expect(typeof exported.modelAliases).toBe("object");
+ expect(exported).not.toHaveProperty("disabledModels");
// Add marker, export, import a different payload, verify reset
await sqliteDb.setModelAlias("marker", "before");
@@ -353,20 +345,6 @@ describe("DB SQLite layer — public API parity", () => {
expect((await sqliteDb.getModelAliases()).marker).toBe("before");
});
- it("exportDb / importDb preserves disabled model settings", async () => {
- await sqliteDb.disableModels("backup-provider", ["backup-model"]);
- const snapshot = await sqliteDb.exportDb();
-
- expect(snapshot.disabledModels).toMatchObject({
- "backup-provider": ["backup-model"],
- });
-
- await sqliteDb.enableModels("backup-provider", []);
- await sqliteDb.importDb(snapshot);
-
- expect(await sqliteDb.getDisabledByProvider("backup-provider")).toEqual(["backup-model"]);
- });
-
it("pricing: user pricing merged with constants", async () => {
await sqliteDb.updatePricing({ openai: { "gpt-test": { input: 1, output: 2 } } });
const p = await sqliteDb.getPricing();
diff --git a/tests/unit/deleted-models-enforcement.test.js b/tests/unit/deleted-models-enforcement.test.js
new file mode 100644
index 00000000..654b9390
--- /dev/null
+++ b/tests/unit/deleted-models-enforcement.test.js
@@ -0,0 +1,51 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const isDeletedModel = vi.fn();
+
+vi.mock("@/lib/db", () => ({ isDeletedModel }));
+
+const { getDeletedModelResponse } = await import("../../src/sse/services/deletedModels.js");
+
+describe("getDeletedModelResponse", () => {
+ beforeEach(() => {
+ isDeletedModel.mockReset();
+ });
+
+ it("allows a model that has not been permanently deleted", async () => {
+ isDeletedModel.mockResolvedValue(false);
+
+ await expect(getDeletedModelResponse("openai", "gpt-available")).resolves.toBeNull();
+ expect(isDeletedModel).toHaveBeenCalledWith("openai", "gpt-available");
+ });
+
+ it("blocks a permanently deleted model", async () => {
+ isDeletedModel.mockResolvedValue(true);
+
+ const response = await getDeletedModelResponse("openai", "gpt-deleted");
+
+ expect(response.status).toBe(404);
+ await expect(response.json()).resolves.toMatchObject({
+ error: {
+ code: "model_not_found",
+ message: "Model openai/gpt-deleted has been deleted by an administrator",
+ },
+ });
+ });
+
+ it("blocks permanently deleted thinking variants", async () => {
+ isDeletedModel.mockResolvedValue(true);
+
+ const response = await getDeletedModelResponse("codex", "gpt-deleted(high)");
+
+ expect(response.status).toBe(404);
+ expect(isDeletedModel).toHaveBeenCalledWith("codex", "gpt-deleted(high)");
+ });
+
+ it("fails closed when deleted-model storage cannot be read", async () => {
+ isDeletedModel.mockRejectedValue(new Error("database unavailable"));
+
+ const response = await getDeletedModelResponse("openai", "gpt-available");
+
+ expect(response.status).toBe(500);
+ });
+});
\ No newline at end of file
diff --git a/tests/unit/disabled-models-enforcement.test.js b/tests/unit/disabled-models-enforcement.test.js
deleted file mode 100644
index 884a0ec2..00000000
--- a/tests/unit/disabled-models-enforcement.test.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-
-const getDisabledModels = vi.fn();
-const getDeletedModels = vi.fn();
-
-vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels }));
-vi.mock("@/lib/db", () => ({ getDeletedModels }));
-vi.mock("@/shared/constants/providers", () => ({
- getProviderAlias: (provider) => ({ openai: "oa", claude: "claude" })[provider] || provider,
-}));
-
-const { getDisabledModelResponse } = await import("../../src/sse/services/disabledModels.js");
-
-describe("getDisabledModelResponse", () => {
- beforeEach(() => {
- getDisabledModels.mockReset();
- getDeletedModels.mockReset();
- getDeletedModels.mockResolvedValue({});
- });
-
- it("allows an enabled model", async () => {
- getDisabledModels.mockResolvedValue({ oa: ["gpt-disabled"] });
-
- await expect(getDisabledModelResponse("openai", "gpt-enabled")).resolves.toBeNull();
- });
-
- it("blocks a model disabled under the provider alias", async () => {
- getDisabledModels.mockResolvedValue({ oa: ["gpt-disabled"] });
-
- const response = await getDisabledModelResponse("openai", "gpt-disabled");
-
- expect(response.status).toBe(404);
- await expect(response.json()).resolves.toMatchObject({
- error: {
- code: "model_not_found",
- message: "Model openai/gpt-disabled is disabled by an administrator",
- },
- });
- });
-
- it("blocks a model disabled under the provider ID", async () => {
- getDisabledModels.mockResolvedValue({ openai: ["gpt-disabled"] });
-
- const response = await getDisabledModelResponse("openai", "gpt-disabled");
-
- expect(response.status).toBe(404);
- });
-
- it("blocks thinking variants when their base model is disabled", async () => {
- getDisabledModels.mockResolvedValue({ codex: ["gpt-5.6-sol"] });
-
- const response = await getDisabledModelResponse("codex", "gpt-5.6-sol(high)");
-
- expect(response.status).toBe(404);
- await expect(response.json()).resolves.toMatchObject({
- error: {
- code: "model_not_found",
- message: "Model codex/gpt-5.6-sol(high) is disabled by an administrator",
- },
- });
- });
-
- it("blocks a permanently deleted model", async () => {
- getDisabledModels.mockResolvedValue({});
- getDeletedModels.mockResolvedValue({ oa: ["gpt-deleted"] });
-
- const response = await getDisabledModelResponse("openai", "gpt-deleted");
-
- expect(response.status).toBe(404);
- await expect(response.json()).resolves.toMatchObject({
- error: {
- code: "model_not_found",
- message: "Model openai/gpt-deleted has been deleted by an administrator",
- },
- });
- });
-
- it("fails closed when disabled-model storage cannot be read", async () => {
- getDisabledModels.mockRejectedValue(new Error("database unavailable"));
-
- const response = await getDisabledModelResponse("openai", "gpt-enabled");
-
- expect(response.status).toBe(500);
- });
-});
diff --git a/tests/unit/disabled-models-route.test.js b/tests/unit/disabled-models-route.test.js
deleted file mode 100644
index 83f6022b..00000000
--- a/tests/unit/disabled-models-route.test.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-
-const getDisabledModels = vi.fn();
-const disableModels = vi.fn();
-const enableModels = vi.fn();
-const requireAdminUser = vi.fn();
-
-vi.mock("@/lib/disabledModelsDb", () => ({
- getDisabledModels,
- disableModels,
- enableModels,
-}));
-vi.mock("@/lib/auth/currentUser", () => ({ requireAdminUser }));
-
-const { GET, POST, DELETE } = await import("../../src/app/api/models/disabled/route.js");
-
-describe("/api/models/disabled", () => {
- beforeEach(() => {
- getDisabledModels.mockReset();
- disableModels.mockReset();
- enableModels.mockReset();
- requireAdminUser.mockReset();
- });
-
- it("keeps disabled model reads available for model selectors", async () => {
- getDisabledModels.mockResolvedValue({ claude: ["claude-disabled"] });
-
- const response = await GET(new Request("http://localhost/api/models/disabled?providerAlias=claude"));
-
- expect(response.status).toBe(200);
- await expect(response.json()).resolves.toEqual({ ids: ["claude-disabled"] });
- expect(requireAdminUser).not.toHaveBeenCalled();
- });
-
- it("rejects a non-admin disable request", async () => {
- requireAdminUser.mockRejectedValue(new Error("Forbidden"));
-
- const response = await POST(new Request("http://localhost/api/models/disabled", {
- method: "POST",
- body: JSON.stringify({ providerAlias: "claude", ids: ["claude-disabled"] }),
- }));
-
- expect(response.status).toBe(403);
- expect(disableModels).not.toHaveBeenCalled();
- });
-
- it("allows an admin to disable models", async () => {
- requireAdminUser.mockResolvedValue({ role: "admin" });
-
- const response = await POST(new Request("http://localhost/api/models/disabled", {
- method: "POST",
- body: JSON.stringify({ providerAlias: "claude", ids: ["claude-disabled"] }),
- }));
-
- expect(response.status).toBe(200);
- expect(disableModels).toHaveBeenCalledWith("claude", ["claude-disabled"]);
- });
-
- it("rejects a non-admin enable request", async () => {
- requireAdminUser.mockRejectedValue(new Error("Forbidden"));
-
- const response = await DELETE(new Request("http://localhost/api/models/disabled?providerAlias=claude&id=claude-disabled", {
- method: "DELETE",
- }));
-
- expect(response.status).toBe(403);
- expect(enableModels).not.toHaveBeenCalled();
- });
-});
diff --git a/tests/unit/permanent-model-delete.test.js b/tests/unit/permanent-model-delete.test.js
index ce28b8ac..7be327db 100644
--- a/tests/unit/permanent-model-delete.test.js
+++ b/tests/unit/permanent-model-delete.test.js
@@ -42,7 +42,6 @@ describe("permanent model deletion", () => {
await db.setModelAlias("keep-alias", `${providerPrefix}/gpt-keep`);
await db.addCustomModel({ providerAlias: providerPrefix, id: modelId, type: "llm" });
await db.addCustomModel({ providerAlias: providerPrefix, id: "gpt-keep", type: "llm" });
- await db.disableModels(providerPrefix, [modelId]);
await db.updatePricing({
[providerPrefix]: {
[modelId]: { prompt: 1, completion: 2 },
@@ -131,7 +130,6 @@ describe("permanent model deletion", () => {
removedAliases: 1,
removedCustomModels: 1,
removedPricingEntries: 1,
- removedDisabledModels: 1,
removedRequestDetails: 1,
updatedCliToolConfigs: 2,
updatedComboIds: [mixedCombo.id],
@@ -146,7 +144,6 @@ describe("permanent model deletion", () => {
expect(await db.getCustomModels()).toEqual([
expect.objectContaining({ providerAlias: providerPrefix, id: "gpt-keep" }),
]);
- expect(await db.getDisabledByProvider(providerPrefix)).toEqual([]);
expect((await db.getPricing())[providerPrefix]).toEqual({
"gpt-keep": { prompt: 3, completion: 4 },
});