mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
fix: update the logic code for combos pages
This commit is contained in:
@@ -7,7 +7,6 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers";
|
||||
import { Card, Button, Modal, Input, CardSkeleton, ModelSelectModal, ConfirmModal, CapacityBadges, Select } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
|
||||
@@ -17,7 +16,7 @@ export default function CombosPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingCombo, setEditingCombo] = useState(null);
|
||||
const [activeProviders, setActiveProviders] = useState([]);
|
||||
const [connectedModels, setConnectedModels] = useState([]);
|
||||
const [comboStrategies, setComboStrategies] = useState({});
|
||||
const [modelCaps, setModelCaps] = useState({});
|
||||
const [confirmState, setConfirmState] = useState(null);
|
||||
@@ -29,23 +28,19 @@ export default function CombosPage() {
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [combosRes, providersRes, settingsRes, modelsRes] = await Promise.all([
|
||||
const [combosRes, settingsRes, modelsRes] = await Promise.all([
|
||||
fetch("/api/combos"),
|
||||
fetch("/api/providers"),
|
||||
fetch("/api/settings"),
|
||||
fetch("/api/models"),
|
||||
fetch("/api/models/connected", { cache: "no-store" }),
|
||||
]);
|
||||
const combosData = await combosRes.json();
|
||||
const providersData = await providersRes.json();
|
||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||
|
||||
// Only LLM combos here - webSearch/webFetch combos belong to media-providers/web
|
||||
if (combosRes.ok) setCombos((combosData.combos || []).filter(c => !c.kind || c.kind === "llm"));
|
||||
if (providersRes.ok) {
|
||||
setActiveProviders(providersData.connections || []);
|
||||
}
|
||||
if (modelsRes.ok) {
|
||||
const md = await modelsRes.json();
|
||||
setConnectedModels(md.models || []);
|
||||
// Build fullModel -> caps map for badge lookup
|
||||
const map = {};
|
||||
for (const m of md.models || []) if (m.caps) map[m.fullModel] = m.caps;
|
||||
@@ -59,6 +54,11 @@ 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);
|
||||
|
||||
const handleCreate = async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/combos", {
|
||||
@@ -190,7 +190,7 @@ export default function CombosPage() {
|
||||
key={combo.id}
|
||||
combo={combo}
|
||||
modelCaps={modelCaps}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onEdit={() => setEditingCombo(combo)}
|
||||
@@ -208,7 +208,7 @@ export default function CombosPage() {
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
onSave={handleCreate}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
/>
|
||||
|
||||
{/* Edit Modal - Use key to force remount and reset state */}
|
||||
@@ -218,7 +218,7 @@ export default function CombosPage() {
|
||||
combo={editingCombo}
|
||||
onClose={() => setEditingCombo(null)}
|
||||
onSave={(data) => handleUpdate(editingCombo.id, data)}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={selectableModels}
|
||||
/>
|
||||
|
||||
{/* Confirm Delete Modal */}
|
||||
@@ -240,7 +240,7 @@ const STRATEGY_OPTIONS = [
|
||||
{ value: "fusion", label: "Fusion — panel + judge" },
|
||||
];
|
||||
|
||||
function ComboCard({ combo, modelCaps = {}, activeProviders = [], copied, onCopy, onEdit, onDelete, strategy = {}, onSetStrategy }) {
|
||||
function ComboCard({ combo, modelCaps = {}, availableModels = [], copied, onCopy, onEdit, onDelete, strategy = {}, onSetStrategy }) {
|
||||
const [showJudgeSelect, setShowJudgeSelect] = useState(false);
|
||||
const current = strategy.fallbackStrategy || "fallback";
|
||||
const judge = strategy.judgeModel || "";
|
||||
@@ -344,7 +344,7 @@ function ComboCard({ combo, modelCaps = {}, activeProviders = [], copied, onCopy
|
||||
isOpen={showJudgeSelect}
|
||||
onClose={() => setShowJudgeSelect(false)}
|
||||
onSelect={(m) => { onSetStrategy({ judgeModel: m?.value || "" }); setShowJudgeSelect(false); }}
|
||||
activeProviders={activeProviders}
|
||||
availableModels={availableModels}
|
||||
title="Select Judge Model"
|
||||
addedModelValues={judge ? [judge] : []}
|
||||
closeOnSelect={true}
|
||||
@@ -451,14 +451,13 @@ function ModelItem({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMove
|
||||
);
|
||||
}
|
||||
|
||||
function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindFilter = null }) {
|
||||
function ComboFormModal({ isOpen, combo, onClose, onSave, availableModels = [], kindFilter = null }) {
|
||||
// Initialize state with combo values - key prop on parent handles reset on remount
|
||||
const [name, setName] = useState(combo?.name || "");
|
||||
const [models, setModels] = useState(combo?.models || []);
|
||||
const [showModelSelect, setShowModelSelect] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [nameError, setNameError] = useState("");
|
||||
const [modelAliases, setModelAliases] = useState({});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
@@ -479,21 +478,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModalData = async () => {
|
||||
try {
|
||||
const aliasesRes = await fetch("/api/models/alias");
|
||||
if (!aliasesRes.ok) return;
|
||||
const aliasesData = await aliasesRes.json();
|
||||
setModelAliases(aliasesData.aliases || {});
|
||||
} catch (error) {
|
||||
console.error("Error fetching modal data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchModalData();
|
||||
}, [isOpen]);
|
||||
|
||||
const validateName = (value) => {
|
||||
if (!value.trim()) {
|
||||
setNameError("Name is required");
|
||||
@@ -642,8 +626,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
onClose={() => setShowModelSelect(false)}
|
||||
onSelect={handleAddModel}
|
||||
onDeselect={handleDeselectModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
availableModels={availableModels}
|
||||
title="Add Model to Combo"
|
||||
kindFilter={kindFilter}
|
||||
addedModelValues={models}
|
||||
|
||||
@@ -110,6 +110,7 @@ export default function ProvidersPage() {
|
||||
const searchQuery = useHeaderSearchStore((s) => s.query);
|
||||
const registerSearch = useHeaderSearchStore((s) => s.register);
|
||||
const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
|
||||
const isAdmin = user?.role === "admin";
|
||||
|
||||
useEffect(() => {
|
||||
registerSearch("Search providers...");
|
||||
@@ -335,8 +336,10 @@ export default function ProvidersPage() {
|
||||
freeEntries.length > 0 ||
|
||||
freeTierEntries.length > 0 ||
|
||||
apikeyEntries.length > 0 ||
|
||||
compatibleProviders.length > 0 ||
|
||||
anthropicCompatibleProviders.length > 0;
|
||||
(isAdmin && (
|
||||
compatibleProviders.length > 0 ||
|
||||
anthropicCompatibleProviders.length > 0
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
@@ -349,7 +352,8 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Providers (OpenAI/Anthropic Compatible) — dynamic */}
|
||||
{/* Custom provider configuration is administered centrally. */}
|
||||
{isAdmin && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
@@ -400,6 +404,7 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Providers */}
|
||||
{oauthEntries.length > 0 && (
|
||||
@@ -577,7 +582,7 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<AddCompatibleModal
|
||||
{isAdmin && <AddCompatibleModal
|
||||
variant="openai"
|
||||
isOpen={showAddCompatibleModal}
|
||||
onClose={() => setShowAddCompatibleModal(false)}
|
||||
@@ -585,8 +590,8 @@ export default function ProvidersPage() {
|
||||
setProviderNodes((prev) => [...prev, node]);
|
||||
setShowAddCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
<AddCompatibleModal
|
||||
/>}
|
||||
{isAdmin && <AddCompatibleModal
|
||||
variant="anthropic"
|
||||
isOpen={showAddAnthropicCompatibleModal}
|
||||
onClose={() => setShowAddAnthropicCompatibleModal(false)}
|
||||
@@ -594,7 +599,7 @@ export default function ProvidersPage() {
|
||||
setProviderNodes((prev) => [...prev, node]);
|
||||
setShowAddAnthropicCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
/>}
|
||||
|
||||
{/* Test Results Modal */}
|
||||
{testResults && (
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelAliases, getProviderConnections } from "@/models";
|
||||
import {
|
||||
getCustomModels,
|
||||
getModelAliases,
|
||||
getProviderConnections,
|
||||
getProviderNodes,
|
||||
} from "@/models";
|
||||
import { getUsers } from "@/lib/db";
|
||||
import { disableModels, enableModels, getDisabledModels } from "@/lib/disabledModelsDb";
|
||||
import { requireAdminUser, requireUsageDashboardUser } from "@/lib/auth/currentUser";
|
||||
import { AI_MODELS } from "@/shared/constants/models";
|
||||
import { AI_PROVIDERS, getProviderAlias, getProviderByAlias } from "@/shared/constants/providers";
|
||||
import {
|
||||
AI_PROVIDERS,
|
||||
getProviderAlias,
|
||||
getProviderByAlias,
|
||||
isAnthropicCompatibleProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
import { getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -34,6 +46,20 @@ function getProviderLabel(providerAlias) {
|
||||
};
|
||||
}
|
||||
|
||||
function getCompatibleProviderLabel(providerId, node, connection) {
|
||||
const isAnthropic = node?.type === "anthropic-compatible"
|
||||
|| isAnthropicCompatibleProvider(providerId);
|
||||
|
||||
return {
|
||||
id: providerId,
|
||||
alias: providerId,
|
||||
name: node?.name || connection?.providerSpecificData?.nodeName
|
||||
|| (isAnthropic ? "Anthropic Compatible" : "OpenAI Compatible"),
|
||||
color: isAnthropic ? "#D97757" : "#10A37F",
|
||||
textIcon: isAnthropic ? "AC" : "OC",
|
||||
};
|
||||
}
|
||||
|
||||
function getForbiddenResponse(error) {
|
||||
if (error.message === "Unauthorized") {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
@@ -49,10 +75,13 @@ export async function GET() {
|
||||
try {
|
||||
const user = await requireUsageDashboardUser();
|
||||
|
||||
const [connections, disabledModels, modelAliases] = await Promise.all([
|
||||
const [connections, customModels, disabledModels, modelAliases, providerNodes, users] = await Promise.all([
|
||||
getProviderConnections(),
|
||||
getCustomModels(),
|
||||
getDisabledModels(),
|
||||
getModelAliases(),
|
||||
getProviderNodes(),
|
||||
getUsers(),
|
||||
]);
|
||||
|
||||
const connectionCountByAlias = new Map();
|
||||
@@ -64,7 +93,7 @@ export async function GET() {
|
||||
}
|
||||
}
|
||||
|
||||
const models = AI_MODELS
|
||||
const staticModels = AI_MODELS
|
||||
.filter((model) => connectionCountByAlias.has(model.provider))
|
||||
.map((model) => {
|
||||
const providerAlias = getProviderAlias(model.provider) || model.provider;
|
||||
@@ -84,7 +113,66 @@ export async function GET() {
|
||||
reasoning: caps.reasoning,
|
||||
},
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
// Compatible providers are dynamic and therefore absent from AI_MODELS.
|
||||
// Their catalog is the explicit list maintained by an administrator on the
|
||||
// provider detail page. The provider-node ID is retained as the alias so
|
||||
// combo model values route directly to the correct credential pool.
|
||||
const adminOwnerIds = new Set(
|
||||
users
|
||||
.filter((entry) => entry.role === "admin" && entry.isActive !== false)
|
||||
.map((entry) => entry.id),
|
||||
);
|
||||
const nodeById = new Map(providerNodes.map((node) => [node.id, node]));
|
||||
const viableCompatibleConnections = new Map();
|
||||
for (const connection of connections) {
|
||||
if (!isViableConnection(connection)) continue;
|
||||
if (!adminOwnerIds.has(connection.ownerId)) continue;
|
||||
if (
|
||||
!isOpenAICompatibleProvider(connection.provider)
|
||||
&& !isAnthropicCompatibleProvider(connection.provider)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!viableCompatibleConnections.has(connection.provider)) {
|
||||
viableCompatibleConnections.set(connection.provider, connection);
|
||||
}
|
||||
}
|
||||
|
||||
const compatibleModels = [];
|
||||
for (const [providerId, connection] of viableCompatibleConnections) {
|
||||
const provider = getCompatibleProviderLabel(providerId, nodeById.get(providerId), connection);
|
||||
const disabled = disabledModels[providerId] || [];
|
||||
|
||||
for (const customModel of customModels) {
|
||||
const kind = customModel.kind || customModel.type || "llm";
|
||||
if (customModel.providerAlias !== providerId || kind !== "llm" || !customModel.id) continue;
|
||||
|
||||
const modelId = String(customModel.id).trim();
|
||||
if (!modelId) continue;
|
||||
|
||||
const fullModel = `${providerId}/${modelId}`;
|
||||
const caps = getCapabilitiesForModel(providerId, modelId);
|
||||
compatibleModels.push({
|
||||
provider,
|
||||
providerAlias: providerId,
|
||||
model: modelId,
|
||||
name: customModel.name || modelId,
|
||||
fullModel,
|
||||
alias: modelAliases[fullModel] || modelId,
|
||||
disabled: disabled.includes(modelId),
|
||||
isCustom: true,
|
||||
caps: {
|
||||
vision: caps.vision,
|
||||
search: caps.search,
|
||||
reasoning: caps.reasoning,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const models = [...staticModels, ...compatibleModels]
|
||||
.filter((model) => user.role === "admin" || !model.disabled)
|
||||
.sort((a, b) => (
|
||||
a.provider.name.localeCompare(b.provider.name)
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCustomModels, addCustomModel, deleteCustomModel } from "@/models";
|
||||
import {
|
||||
getCustomModels,
|
||||
addCustomModel,
|
||||
deleteCustomModel,
|
||||
} from "@/models";
|
||||
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;
|
||||
}
|
||||
|
||||
async function requireCustomModelCatalogAdmin() {
|
||||
// Custom-model records are a shared catalog, not connection-owned data.
|
||||
// Restrict mutations to administrators for every provider so one dashboard
|
||||
// user cannot alter models available to other users.
|
||||
await requireAdminUser();
|
||||
}
|
||||
|
||||
// GET /api/models/custom - List all custom models
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -21,9 +43,13 @@ export async function POST(request) {
|
||||
if (!providerAlias || !id) {
|
||||
return NextResponse.json({ error: "providerAlias and id required" }, { status: 400 });
|
||||
}
|
||||
await requireCustomModelCatalogAdmin();
|
||||
const added = await addCustomModel({ providerAlias, id, type: type || "llm", name });
|
||||
return NextResponse.json({ success: true, added });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error adding custom model:", error);
|
||||
return NextResponse.json({ error: "Failed to add custom model" }, { status: 500 });
|
||||
}
|
||||
@@ -39,9 +65,13 @@ export async function DELETE(request) {
|
||||
if (!providerAlias || !id) {
|
||||
return NextResponse.json({ error: "providerAlias and id required" }, { status: 400 });
|
||||
}
|
||||
await requireCustomModelCatalogAdmin();
|
||||
await deleteCustomModel({ providerAlias, id, type });
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error deleting custom model:", error);
|
||||
return NextResponse.json({ error: "Failed to delete custom model" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { deleteProviderConnectionsByProvider, deleteProviderNode, getProviderConnections, getProviderNodeById, updateProviderConnection, updateProviderNode } from "@/models";
|
||||
import { requireAdminUser } from "@/lib/auth/currentUser";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// PUT /api/provider-nodes/[id] - Update provider node
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
await requireAdminUser();
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, prefix, apiType, baseUrl } = body;
|
||||
@@ -75,6 +87,9 @@ export async function PUT(request, { params }) {
|
||||
|
||||
return NextResponse.json({ node: updated });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error updating provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to update provider node" }, { status: 500 });
|
||||
}
|
||||
@@ -83,6 +98,7 @@ export async function PUT(request, { params }) {
|
||||
// DELETE /api/provider-nodes/[id] - Delete provider node and its connections
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
await requireAdminUser();
|
||||
const { id } = await params;
|
||||
const node = await getProviderNodeById(id);
|
||||
|
||||
@@ -95,6 +111,9 @@ export async function DELETE(request, { params }) {
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error deleting provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to delete provider node" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createProviderNode, getProviderNodes } from "@/models";
|
||||
import { requireAdminUser } from "@/lib/auth/currentUser";
|
||||
import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX, CUSTOM_EMBEDDING_PREFIX } from "@/shared/constants/providers";
|
||||
import { generateId } from "@/shared/utils";
|
||||
|
||||
@@ -17,12 +18,26 @@ const CUSTOM_EMBEDDING_DEFAULTS = {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
};
|
||||
|
||||
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/provider-nodes - List all provider nodes
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAdminUser();
|
||||
const nodes = await getProviderNodes();
|
||||
return NextResponse.json({ nodes });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error fetching provider nodes:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch provider nodes" }, { status: 500 });
|
||||
}
|
||||
@@ -31,6 +46,7 @@ export async function GET() {
|
||||
// POST /api/provider-nodes - Create provider node
|
||||
export async function POST(request) {
|
||||
try {
|
||||
await requireAdminUser();
|
||||
const body = await request.json();
|
||||
const { name, prefix, apiType, baseUrl, type } = body;
|
||||
|
||||
@@ -98,6 +114,9 @@ export async function POST(request) {
|
||||
|
||||
return NextResponse.json({ error: "Invalid provider node type" }, { status: 400 });
|
||||
} catch (error) {
|
||||
const accessError = getAccessErrorResponse(error);
|
||||
if (accessError) return accessError;
|
||||
|
||||
console.log("Error creating provider node:", error);
|
||||
return NextResponse.json({ error: "Failed to create provider node" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,21 @@ import {
|
||||
deleteProviderConnection,
|
||||
} from "@/models";
|
||||
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
|
||||
import {
|
||||
isAnthropicCompatibleProvider,
|
||||
isCustomEmbeddingProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
} from "@/shared/constants/providers";
|
||||
|
||||
function isAdministratorManagedProvider(provider) {
|
||||
return isOpenAICompatibleProvider(provider)
|
||||
|| isAnthropicCompatibleProvider(provider)
|
||||
|| isCustomEmbeddingProvider(provider);
|
||||
}
|
||||
|
||||
function canMutateConnection(user, connection) {
|
||||
return !isAdministratorManagedProvider(connection.provider) || user.role === "admin";
|
||||
}
|
||||
|
||||
function normalizeProxyConfig(body = {}) {
|
||||
const hasAnyProxyField =
|
||||
@@ -92,7 +107,7 @@ export async function GET(request, { params }) {
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { ownerId } = await getProviderConnectionAccess();
|
||||
const { user, ownerId } = await getProviderConnectionAccess();
|
||||
const body = await request.json();
|
||||
const {
|
||||
name,
|
||||
@@ -111,6 +126,9 @@ export async function PUT(request, { params }) {
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
if (!canMutateConnection(user, existing)) {
|
||||
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
|
||||
}
|
||||
|
||||
const proxyConfig = normalizeProxyConfig(body);
|
||||
if (proxyConfig.error) {
|
||||
@@ -184,12 +202,15 @@ export async function PUT(request, { params }) {
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { ownerId } = await getProviderConnectionAccess();
|
||||
const { user, ownerId } = await getProviderConnectionAccess();
|
||||
|
||||
const existing = await getProviderConnectionById(id, ownerId);
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
if (!canMutateConnection(user, existing)) {
|
||||
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
|
||||
}
|
||||
|
||||
const deleted = await deleteProviderConnection(id);
|
||||
if (!deleted) {
|
||||
|
||||
@@ -111,17 +111,21 @@ export async function POST(request) {
|
||||
// Dual-auth providers (e.g. codebuddy-cn, xai) live under category "oauth" but also
|
||||
// accept an API key via authModes — they aren't in APIKEY_PROVIDERS, so allow them here.
|
||||
const supportsApiKeyMode = !!AI_PROVIDERS[provider]?.authModes?.includes("apikey");
|
||||
const isCompatibleProvider = isOpenAICompatibleProvider(provider)
|
||||
|| isAnthropicCompatibleProvider(provider)
|
||||
|| isCustomEmbeddingProvider(provider);
|
||||
const isValidProvider = APIKEY_PROVIDERS[provider] ||
|
||||
FREE_TIER_PROVIDERS[provider] ||
|
||||
supportsApiKeyMode ||
|
||||
isWebCookieProvider ||
|
||||
isOpenAICompatibleProvider(provider) ||
|
||||
isAnthropicCompatibleProvider(provider) ||
|
||||
isCustomEmbeddingProvider(provider);
|
||||
isCompatibleProvider;
|
||||
|
||||
if (!provider || !isValidProvider) {
|
||||
return NextResponse.json({ error: "Invalid provider" }, { status: 400 });
|
||||
}
|
||||
if (isCompatibleProvider && user.role !== "admin") {
|
||||
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
|
||||
}
|
||||
if (!apiKey && provider !== "ollama-local") {
|
||||
return NextResponse.json({ error: `${isWebCookieProvider ? "Cookie value" : "API Key"} is required` }, { status: 400 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user