diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.new.js b/src/app/(dashboard)/dashboard/providers/[id]/page.new.js
new file mode 100644
index 00000000..114e1a7e
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.new.js
@@ -0,0 +1,1716 @@
+"use client";
+
+import { useState, useEffect, useCallback, useMemo } from "react";
+import PropTypes from "prop-types";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import Image from "next/image";
+import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, Toggle, Select } from "@/shared/components";
+import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
+import { getModelsByProviderId } from "@/shared/constants/models";
+import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+
+export default function ProviderDetailPage() {
+ const params = useParams();
+ const router = useRouter();
+ const providerId = params.id;
+ const [connections, setConnections] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [providerNode, setProviderNode] = useState(null);
+ const [showOAuthModal, setShowOAuthModal] = useState(false);
+ const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [showEditNodeModal, setShowEditNodeModal] = useState(false);
+ const [selectedConnection, setSelectedConnection] = useState(null);
+ const [modelAliases, setModelAliases] = useState({});
+ const [remoteModels, setRemoteModels] = useState([]);
+ const [loadingRemoteModels, setLoadingRemoteModels] = useState(false);
+ const [selectedModelIds, setSelectedModelIds] = useState([]);
+ const [savingSelectedModels, setSavingSelectedModels] = useState(false);
+ const [modelSearchQuery, setModelSearchQuery] = useState("");
+ const [showSelectedOnly, setShowSelectedOnly] = useState(false);
+ const [headerImgError, setHeaderImgError] = useState(false);
+ const { copied, copy } = useCopyToClipboard();
+
+ const providerInfo = providerNode
+ ? {
+ id: providerNode.id,
+ name: providerNode.name || (providerNode.type === "anthropic-compatible" ? "Anthropic Compatible" : "OpenAI Compatible"),
+ color: providerNode.type === "anthropic-compatible" ? "#D97757" : "#10A37F",
+ textIcon: providerNode.type === "anthropic-compatible" ? "AC" : "OC",
+ apiType: providerNode.apiType,
+ baseUrl: providerNode.baseUrl,
+ type: providerNode.type,
+ }
+ : (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]);
+ const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId];
+ const models = useMemo(() => getModelsByProviderId(providerId), [providerId]);
+ const providerAlias = getProviderAlias(providerId);
+
+ const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
+ const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
+ const isCompatible = isOpenAICompatible || isAnthropicCompatible;
+
+ const providerStorageAlias = isCompatible ? providerId : providerAlias;
+ const providerDisplayAlias = isCompatible
+ ? (providerNode?.prefix || providerId)
+ : providerAlias;
+ const activeConnection = connections.find((conn) => conn.isActive !== false) || null;
+ const allProviderModels = models.length > 0 ? models : remoteModels;
+ const allProviderModelIds = useMemo(
+ () => allProviderModels.map((model) => model.id),
+ [allProviderModels]
+ );
+ const savedEnabledModels = useMemo(() => {
+ const enabled = activeConnection?.providerSpecificData?.enabledModels;
+ return Array.isArray(enabled)
+ ? enabled.filter((modelId) => allProviderModelIds.includes(modelId))
+ : [];
+ }, [activeConnection?.providerSpecificData?.enabledModels, allProviderModelIds]);
+ const savedEnabledModelsKey = useMemo(
+ () => savedEnabledModels.join("|"),
+ [savedEnabledModels]
+ );
+
+ // Define callbacks BEFORE the useEffect that uses them
+ const fetchAliases = useCallback(async () => {
+ try {
+ const res = await fetch("/api/models/alias");
+ const data = await res.json();
+ if (res.ok) {
+ setModelAliases(data.aliases || {});
+ }
+ } catch (error) {
+ console.log("Error fetching aliases:", error);
+ }
+ }, []);
+
+ const fetchConnections = useCallback(async () => {
+ try {
+ const [connectionsRes, nodesRes] = await Promise.all([
+ fetch("/api/providers", { cache: "no-store" }),
+ fetch("/api/provider-nodes", { cache: "no-store" }),
+ ]);
+ const connectionsData = await connectionsRes.json();
+ const nodesData = await nodesRes.json();
+ if (connectionsRes.ok) {
+ const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId);
+ setConnections(filtered);
+ }
+ if (nodesRes.ok) {
+ let node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
+
+ // Newly created compatible nodes can be briefly unavailable on one worker.
+ // Retry a few times before showing "Provider not found".
+ if (!node && isCompatible) {
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ await new Promise((resolve) => setTimeout(resolve, 150));
+ const retryRes = await fetch("/api/provider-nodes", { cache: "no-store" });
+ if (!retryRes.ok) continue;
+ const retryData = await retryRes.json();
+ node = (retryData.nodes || []).find((entry) => entry.id === providerId) || null;
+ if (node) break;
+ }
+ }
+
+ setProviderNode(node);
+ }
+ } catch (error) {
+ console.log("Error fetching connections:", error);
+ } finally {
+ setLoading(false);
+ }
+ }, [providerId, isCompatible]);
+
+ const handleUpdateNode = async (formData) => {
+ try {
+ const res = await fetch(`/api/provider-nodes/${providerId}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(formData),
+ });
+ const data = await res.json();
+ if (res.ok) {
+ setProviderNode(data.node);
+ await fetchConnections();
+ setShowEditNodeModal(false);
+ }
+ } catch (error) {
+ console.log("Error updating provider node:", error);
+ }
+ };
+
+ const handleToggleModelSelected = (modelId) => {
+ setSelectedModelIds((prev) => (
+ prev.includes(modelId)
+ ? prev.filter((id) => id !== modelId)
+ : [...prev, modelId]
+ ));
+ };
+
+ const handleSaveSelectedModels = async () => {
+ if (!activeConnection || savingSelectedModels) return;
+ setSavingSelectedModels(true);
+ try {
+ const res = await fetch(`/api/providers/${activeConnection.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ providerSpecificData: {
+ enabledModels: selectedModelIds,
+ },
+ }),
+ });
+
+ if (!res.ok) {
+ const data = await res.json();
+ alert(data.error || "Failed to save selected models");
+ return;
+ }
+
+ await fetchConnections();
+ } catch (error) {
+ console.log("Error saving selected models:", error);
+ alert("Failed to save selected models");
+ } finally {
+ setSavingSelectedModels(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchConnections();
+ fetchAliases();
+ }, [fetchConnections, fetchAliases]);
+
+ useEffect(() => {
+ const nextSelectedModelIds = (isCompatible || providerInfo?.passthroughModels)
+ ? []
+ : savedEnabledModels;
+
+ setSelectedModelIds((prev) => {
+ if (
+ prev.length === nextSelectedModelIds.length
+ && prev.every((modelId, index) => modelId === nextSelectedModelIds[index])
+ ) {
+ return prev;
+ }
+ return nextSelectedModelIds;
+ });
+ }, [
+ isCompatible,
+ providerInfo?.passthroughModels,
+ activeConnection?.id,
+ savedEnabledModels
+ ]);
+
+ const fetchRemoteModels = useCallback(async () => {
+ if (isCompatible || providerInfo?.passthroughModels || models.length > 0) {
+ setRemoteModels([]);
+ return;
+ }
+
+ if (!activeConnection) {
+ setRemoteModels([]);
+ return;
+ }
+
+ setLoadingRemoteModels(true);
+ try {
+ const res = await fetch(`/api/providers/${activeConnection.id}/models`);
+ const data = await res.json();
+ if (!res.ok) {
+ setRemoteModels([]);
+ return;
+ }
+
+ const parsed = (data.models || [])
+ .map((item) => {
+ if (typeof item === "string") return { id: item, name: item };
+ const modelId = item?.id || item?.name || item?.model;
+ if (!modelId) return null;
+ return { id: modelId, name: item?.name || modelId };
+ })
+ .filter(Boolean);
+
+ const deduped = Array.from(
+ new Map(parsed.map((item) => [item.id, item])).values()
+ );
+
+ setRemoteModels(deduped);
+ } catch (error) {
+ console.log("Error fetching remote models:", error);
+ setRemoteModels([]);
+ } finally {
+ setLoadingRemoteModels(false);
+ }
+ }, [activeConnection, isCompatible, models.length, providerInfo?.passthroughModels]);
+
+ useEffect(() => {
+ fetchRemoteModels();
+ }, [fetchRemoteModels]);
+
+ const handleSetAlias = async (modelId, alias, providerAliasOverride = providerAlias) => {
+ const fullModel = `${providerAliasOverride}/${modelId}`;
+ try {
+ const res = await fetch("/api/models/alias", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model: fullModel, alias }),
+ });
+ if (res.ok) {
+ await fetchAliases();
+ } else {
+ const data = await res.json();
+ alert(data.error || "Failed to set alias");
+ }
+ } catch (error) {
+ console.log("Error setting alias:", error);
+ }
+ };
+
+ const handleDeleteAlias = async (alias) => {
+ try {
+ const res = await fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, {
+ method: "DELETE",
+ });
+ if (res.ok) {
+ await fetchAliases();
+ }
+ } catch (error) {
+ console.log("Error deleting alias:", error);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!confirm("Delete this connection?")) return;
+ try {
+ const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
+ if (res.ok) {
+ setConnections(connections.filter(c => c.id !== id));
+ }
+ } catch (error) {
+ console.log("Error deleting connection:", error);
+ }
+ };
+
+ const handleOAuthSuccess = () => {
+ fetchConnections();
+ setShowOAuthModal(false);
+ };
+
+ const handleSaveApiKey = async (formData) => {
+ try {
+ const res = await fetch("/api/providers", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider: providerId, ...formData }),
+ });
+ if (res.ok) {
+ await fetchConnections();
+ setShowAddApiKeyModal(false);
+ }
+ } catch (error) {
+ console.log("Error saving connection:", error);
+ }
+ };
+
+ const handleUpdateConnection = async (formData) => {
+ try {
+ const res = await fetch(`/api/providers/${selectedConnection.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(formData),
+ });
+ if (res.ok) {
+ await fetchConnections();
+ setShowEditModal(false);
+ }
+ } catch (error) {
+ console.log("Error updating connection:", error);
+ }
+ };
+
+ const handleUpdateConnectionStatus = async (id, isActive) => {
+ try {
+ const res = await fetch(`/api/providers/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ isActive }),
+ });
+ if (res.ok) {
+ setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c));
+ }
+ } catch (error) {
+ console.log("Error updating connection status:", error);
+ }
+ };
+
+ const handleSwapPriority = async (conn1, conn2) => {
+ if (!conn1 || !conn2) return;
+ try {
+ // If they have the same priority, we need to ensure the one moving up
+ // gets a lower value than the one moving down.
+ // We use a small offset which the backend re-indexing will fix.
+ let p1 = conn2.priority;
+ let p2 = conn1.priority;
+
+ if (p1 === p2) {
+ // If moving conn1 "up" (index decreases)
+ const isConn1MovingUp = connections.indexOf(conn1) > connections.indexOf(conn2);
+ if (isConn1MovingUp) {
+ p1 = conn2.priority - 0.5;
+ } else {
+ p1 = conn2.priority + 0.5;
+ }
+ }
+
+ await Promise.all([
+ fetch(`/api/providers/${conn1.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ priority: p1 }),
+ }),
+ fetch(`/api/providers/${conn2.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ priority: p2 }),
+ }),
+ ]);
+ await fetchConnections();
+ } catch (error) {
+ console.log("Error swapping priority:", error);
+ }
+ };
+
+ const renderModelsSection = () => {
+ if (isCompatible) {
+ return (
+
+ );
+ }
+ if (providerInfo.passthroughModels) {
+ return (
+
+ );
+ }
+
+ const availableModels = allProviderModels;
+ if (availableModels.length === 0) {
+ if (loadingRemoteModels) {
+ return
Loading models from provider...
;
+ }
+ return No models configured
;
+ }
+
+ const selectedSet = new Set(selectedModelIds);
+ const filteredBySelection = showSelectedOnly
+ ? availableModels.filter((model) => selectedSet.has(model.id))
+ : availableModels;
+ const query = modelSearchQuery.trim().toLowerCase();
+ const visibleModels = query
+ ? filteredBySelection.filter((model) =>
+ model.id.toLowerCase().includes(query) ||
+ (model.name || "").toLowerCase().includes(query)
+ )
+ : filteredBySelection;
+ const hasSelectionChanges =
+ savedEnabledModels.length !== selectedModelIds.length ||
+ savedEnabledModels.some((modelId) => !selectedSet.has(modelId));
+
+ return (
+
+
+
+ setModelSearchQuery(e.target.value)}
+ placeholder="Search model id"
+ className="pr-8"
+ />
+ {modelSearchQuery && (
+ setModelSearchQuery("")}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-text-muted hover:text-primary"
+ title="Clear search"
+ >
+ close
+
+ )}
+
+
setSelectedModelIds(allProviderModelIds)}
+ disabled={allProviderModels.length === 0}
+ >
+ Select all
+
+
setSelectedModelIds([])}
+ >
+ Unselect all
+
+
+ {savingSelectedModels ? "Saving..." : "Save selection"}
+
+
+ Selected only
+
+
+
+
+
+ {selectedModelIds.length > 0
+ ? `${selectedModelIds.length} selected`
+ : "All models enabled"}
+
+
+ {visibleModels.length === 0 ? (
+
No models match your filter.
+ ) : (
+
+ {visibleModels.map((model) => {
+ const fullModel = `${providerStorageAlias}/${model.id}`;
+ const oldFormatModel = `${providerId}/${model.id}`;
+ const existingAlias = Object.entries(modelAliases).find(
+ ([, m]) => m === fullModel || m === oldFormatModel
+ )?.[0];
+ return (
+ handleToggleModelSelected(model.id)}
+ onSetAlias={(alias) => handleSetAlias(model.id, alias, providerStorageAlias)}
+ onDeleteAlias={() => handleDeleteAlias(existingAlias)}
+ />
+ );
+ })}
+
+ )}
+
+ );
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!providerInfo) {
+ return (
+
+
Provider not found
+
+ Back to Providers
+
+
+ );
+ }
+
+ // Determine icon path: OpenAI Compatible providers use specialized icons
+ const getHeaderIconPath = () => {
+ if (isOpenAICompatible && providerInfo.apiType) {
+ return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png";
+ }
+ if (isAnthropicCompatible) {
+ return "/providers/anthropic-m.png";
+ }
+ return `/providers/${providerInfo.id}.png`;
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
arrow_back
+ Back to Providers
+
+
+
+ {headerImgError ? (
+
+ {providerInfo.textIcon || providerInfo.id.slice(0, 2).toUpperCase()}
+
+ ) : (
+ setHeaderImgError(true)}
+ />
+ )}
+
+
+
{providerInfo.name}
+
+ {connections.length} connection{connections.length === 1 ? "" : "s"}
+
+
+
+
+
+ {isCompatible && providerNode && (
+
+
+
+
{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}
+
+ {isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
+ {isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
+
+
+
+ setShowAddApiKeyModal(true)}
+ disabled={connections.length > 0}
+ >
+ Add
+
+ setShowEditNodeModal(true)}
+ >
+ Edit
+
+ {
+ if (!confirm(`Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`)) return;
+ try {
+ const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" });
+ if (res.ok) {
+ router.push("/dashboard/providers");
+ }
+ } catch (error) {
+ console.log("Error deleting provider node:", error);
+ }
+ }}
+ >
+ Delete
+
+
+
+ {connections.length > 0 && (
+
+ Only one connection is allowed per compatible node. Add another node if you need more connections.
+
+ )}
+
+ )}
+
+ {/* Connections */}
+
+
+
Connections
+ {!isCompatible && (
+ isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}
+ >
+ Add
+
+ )}
+
+
+ {connections.length === 0 ? (
+
+
+ {isOAuth ? "lock" : "key"}
+
+
No connections yet
+
Add your first connection to get started
+ {!isCompatible && (
+
isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
+ Add Connection
+
+ )}
+
+ ) : (
+
+ {connections
+ .sort((a, b) => (a.priority || 0) - (b.priority || 0))
+ .map((conn, index) => (
+ handleSwapPriority(conn, connections[index - 1])}
+ onMoveDown={() => handleSwapPriority(conn, connections[index + 1])}
+ onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
+ onEdit={() => {
+ setSelectedConnection(conn);
+ setShowEditModal(true);
+ }}
+ onDelete={() => handleDelete(conn.id)}
+ />
+ ))}
+
+ )}
+
+
+ {/* Models */}
+
+
+ {providerInfo.passthroughModels ? "Model Aliases" : "Available Models"}
+
+ {renderModelsSection()}
+
+
+
+ {/* Modals */}
+ {providerId === "kiro" ? (
+
setShowOAuthModal(false)}
+ />
+ ) : providerId === "cursor" ? (
+ setShowOAuthModal(false)}
+ />
+ ) : (
+ setShowOAuthModal(false)}
+ />
+ )}
+ setShowAddApiKeyModal(false)}
+ />
+ setShowEditModal(false)}
+ />
+ {isCompatible && (
+ setShowEditNodeModal(false)}
+ isAnthropic={isAnthropicCompatible}
+ />
+ )}
+
+ );
+}
+
+function ModelRow({ model, fullModel, alias, selected, onToggleSelect, copied, onCopy }) {
+ return (
+
+
+
+ {selected ? "check_box" : "check_box_outline_blank"}
+
+
+ smart_toy
+ {fullModel}
+ onCopy(fullModel, `model-${model.id}`)}
+ className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
+ title="Copy model"
+ >
+
+ {copied === `model-${model.id}` ? "check" : "content_copy"}
+
+
+
+ );
+}
+
+ModelRow.propTypes = {
+ model: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ }).isRequired,
+ fullModel: PropTypes.string.isRequired,
+ alias: PropTypes.string,
+ selected: PropTypes.bool,
+ onToggleSelect: PropTypes.func,
+ copied: PropTypes.string,
+ onCopy: PropTypes.func.isRequired,
+};
+
+function PassthroughModelsSection({ providerAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias }) {
+ const [newModel, setNewModel] = useState("");
+ const [adding, setAdding] = useState(false);
+
+ // Filter aliases for this provider - models are persisted via alias
+ const providerAliases = Object.entries(modelAliases).filter(
+ ([, model]) => model.startsWith(`${providerAlias}/`)
+ );
+
+ const allModels = providerAliases.map(([alias, fullModel]) => ({
+ modelId: fullModel.replace(`${providerAlias}/`, ""),
+ fullModel,
+ alias,
+ }));
+
+ // Generate default alias from modelId (last part after /)
+ const generateDefaultAlias = (modelId) => {
+ const parts = modelId.split("/");
+ return parts[parts.length - 1];
+ };
+
+ const handleAdd = async () => {
+ if (!newModel.trim() || adding) return;
+ const modelId = newModel.trim();
+ const defaultAlias = generateDefaultAlias(modelId);
+
+ // Check if alias already exists
+ if (modelAliases[defaultAlias]) {
+ alert(`Alias "${defaultAlias}" already exists. Please use a different model or edit existing alias.`);
+ return;
+ }
+
+ setAdding(true);
+ try {
+ await onSetAlias(modelId, defaultAlias);
+ setNewModel("");
+ } catch (error) {
+ console.log("Error adding model:", error);
+ } finally {
+ setAdding(false);
+ }
+ };
+
+ return (
+
+
+ OpenRouter supports any model. Add models and create aliases for quick access.
+
+
+ {/* Add new model */}
+
+
+ Model ID (from OpenRouter)
+ setNewModel(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleAdd()}
+ placeholder="anthropic/claude-3-opus"
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
+ />
+
+
+ {adding ? "Adding..." : "Add"}
+
+
+
+ {/* Models list */}
+ {allModels.length > 0 && (
+
+ {allModels.map(({ modelId, fullModel, alias }) => (
+
onDeleteAlias(alias)}
+ />
+ ))}
+
+ )}
+
+ );
+}
+
+PassthroughModelsSection.propTypes = {
+ providerAlias: PropTypes.string.isRequired,
+ modelAliases: PropTypes.object.isRequired,
+ copied: PropTypes.string,
+ onCopy: PropTypes.func.isRequired,
+ onSetAlias: PropTypes.func.isRequired,
+ onDeleteAlias: PropTypes.func.isRequired,
+};
+
+function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) {
+ return (
+
+
smart_toy
+
+
+
{modelId}
+
+
+ {fullModel}
+ onCopy(fullModel, `model-${modelId}`)}
+ className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
+ title="Copy model"
+ >
+
+ {copied === `model-${modelId}` ? "check" : "content_copy"}
+
+
+
+
+
+ {/* Delete button */}
+
+ delete
+
+
+ );
+}
+
+PassthroughModelRow.propTypes = {
+ modelId: PropTypes.string.isRequired,
+ fullModel: PropTypes.string.isRequired,
+ copied: PropTypes.string,
+ onCopy: PropTypes.func.isRequired,
+ onDeleteAlias: PropTypes.func.isRequired,
+};
+
+function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) {
+ const [newModel, setNewModel] = useState("");
+ const [adding, setAdding] = useState(false);
+ const [importing, setImporting] = useState(false);
+
+ const providerAliases = Object.entries(modelAliases).filter(
+ ([, model]) => model.startsWith(`${providerStorageAlias}/`)
+ );
+
+ const allModels = providerAliases.map(([alias, fullModel]) => ({
+ modelId: fullModel.replace(`${providerStorageAlias}/`, ""),
+ fullModel,
+ alias,
+ }));
+
+ const generateDefaultAlias = (modelId) => {
+ const parts = modelId.split("/");
+ return parts[parts.length - 1];
+ };
+
+ const resolveAlias = (modelId) => {
+ const baseAlias = generateDefaultAlias(modelId);
+ if (!modelAliases[baseAlias]) return baseAlias;
+ const prefixedAlias = `${providerDisplayAlias}-${baseAlias}`;
+ if (!modelAliases[prefixedAlias]) return prefixedAlias;
+ return null;
+ };
+
+ const handleAdd = async () => {
+ if (!newModel.trim() || adding) return;
+ const modelId = newModel.trim();
+ const resolvedAlias = resolveAlias(modelId);
+ if (!resolvedAlias) {
+ alert("All suggested aliases already exist. Please choose a different model or remove conflicting aliases.");
+ return;
+ }
+
+ setAdding(true);
+ try {
+ await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
+ setNewModel("");
+ } catch (error) {
+ console.log("Error adding model:", error);
+ } finally {
+ setAdding(false);
+ }
+ };
+
+ const handleImport = async () => {
+ if (importing) return;
+ const activeConnection = connections.find((conn) => conn.isActive !== false);
+ if (!activeConnection) return;
+
+ setImporting(true);
+ try {
+ const res = await fetch(`/api/providers/${activeConnection.id}/models`);
+ const data = await res.json();
+ if (!res.ok) {
+ alert(data.error || "Failed to import models");
+ return;
+ }
+ const models = data.models || [];
+ if (models.length === 0) {
+ alert("No models returned from /models.");
+ return;
+ }
+ let importedCount = 0;
+ for (const model of models) {
+ const modelId = model.id || model.name || model.model;
+ if (!modelId) continue;
+ const resolvedAlias = resolveAlias(modelId);
+ if (!resolvedAlias) continue;
+ await onSetAlias(modelId, resolvedAlias, providerStorageAlias);
+ importedCount += 1;
+ }
+ if (importedCount === 0) {
+ alert("No new models were added.");
+ }
+ } catch (error) {
+ console.log("Error importing models:", error);
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ const canImport = connections.some((conn) => conn.isActive !== false);
+
+ return (
+
+
+ Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint.
+
+
+
+
+ Model ID
+ setNewModel(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleAdd()}
+ placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"}
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
+ />
+
+
+ {adding ? "Adding..." : "Add"}
+
+
+ {importing ? "Importing..." : "Import from /models"}
+
+
+
+ {!canImport && (
+
+ Add a connection to enable importing models.
+
+ )}
+
+ {allModels.length > 0 && (
+
+ {allModels.map(({ modelId, fullModel, alias }) => (
+
onDeleteAlias(alias)}
+ />
+ ))}
+
+ )}
+
+ );
+}
+
+CompatibleModelsSection.propTypes = {
+ providerStorageAlias: PropTypes.string.isRequired,
+ providerDisplayAlias: PropTypes.string.isRequired,
+ modelAliases: PropTypes.object.isRequired,
+ copied: PropTypes.string,
+ onCopy: PropTypes.func.isRequired,
+ onSetAlias: PropTypes.func.isRequired,
+ onDeleteAlias: PropTypes.func.isRequired,
+ connections: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string,
+ isActive: PropTypes.bool,
+ })).isRequired,
+ isAnthropic: PropTypes.bool,
+};
+
+function CooldownTimer({ until }) {
+ const [remaining, setRemaining] = useState("");
+
+ useEffect(() => {
+ const updateRemaining = () => {
+ const diff = new Date(until).getTime() - Date.now();
+ if (diff <= 0) {
+ setRemaining("");
+ return;
+ }
+ const secs = Math.floor(diff / 1000);
+ if (secs < 60) {
+ setRemaining(`${secs}s`);
+ } else if (secs < 3600) {
+ setRemaining(`${Math.floor(secs / 60)}m ${secs % 60}s`);
+ } else {
+ const hrs = Math.floor(secs / 3600);
+ const mins = Math.floor((secs % 3600) / 60);
+ setRemaining(`${hrs}h ${mins}m`);
+ }
+ };
+
+ updateRemaining();
+ const interval = setInterval(updateRemaining, 1000);
+ return () => clearInterval(interval);
+ }, [until]);
+
+ if (!remaining) return null;
+
+ return (
+
+ ⏱ {remaining}
+
+ );
+}
+
+CooldownTimer.propTypes = {
+ until: PropTypes.string.isRequired,
+};
+
+function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onEdit, onDelete }) {
+ const displayName = isOAuth
+ ? connection.name || connection.email || connection.displayName || "OAuth Account"
+ : connection.name;
+
+ // Use useState + useEffect for impure Date.now() to avoid calling during render
+ const [isCooldown, setIsCooldown] = useState(false);
+
+ useEffect(() => {
+ const checkCooldown = () => {
+ const cooldown = connection.rateLimitedUntil &&
+ new Date(connection.rateLimitedUntil).getTime() > Date.now();
+ setIsCooldown(cooldown);
+ };
+
+ checkCooldown();
+ // Update every second while in cooldown
+ const interval = connection.rateLimitedUntil ? setInterval(checkCooldown, 1000) : null;
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }, [connection.rateLimitedUntil]);
+
+ // Determine effective status (override unavailable if cooldown expired)
+ const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
+ ? "active" // Cooldown expired → treat as active
+ : connection.testStatus;
+
+ const getStatusVariant = () => {
+ if (connection.isActive === false) return "default";
+ if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
+ if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
+ return "default";
+ };
+
+ return (
+
+
+ {/* Priority arrows */}
+
+
+ keyboard_arrow_up
+
+
+ keyboard_arrow_down
+
+
+
+ {isOAuth ? "lock" : "key"}
+
+
+
{displayName}
+
+
+ {connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
+
+ {isCooldown && connection.isActive !== false && }
+ {connection.lastError && connection.isActive !== false && (
+
+ {connection.lastError}
+
+ )}
+ #{connection.priority}
+ {connection.globalPriority && (
+ Auto: {connection.globalPriority}
+ )}
+
+
+
+
+
+
+
+ edit
+
+
+ delete
+
+
+
+
+ );
+}
+
+ConnectionRow.propTypes = {
+ connection: PropTypes.shape({
+ id: PropTypes.string,
+ name: PropTypes.string,
+ email: PropTypes.string,
+ displayName: PropTypes.string,
+ rateLimitedUntil: PropTypes.string,
+ testStatus: PropTypes.string,
+ isActive: PropTypes.bool,
+ lastError: PropTypes.string,
+ priority: PropTypes.number,
+ globalPriority: PropTypes.number,
+ }).isRequired,
+ isOAuth: PropTypes.bool.isRequired,
+ isFirst: PropTypes.bool.isRequired,
+ isLast: PropTypes.bool.isRequired,
+ onMoveUp: PropTypes.func.isRequired,
+ onMoveDown: PropTypes.func.isRequired,
+ onToggleActive: PropTypes.func.isRequired,
+ onEdit: PropTypes.func.isRequired,
+ onDelete: PropTypes.func.isRequired,
+};
+
+function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, onSave, onClose }) {
+ const [formData, setFormData] = useState({
+ name: "",
+ apiKey: "",
+ priority: 1,
+ });
+ const [validating, setValidating] = useState(false);
+ const [validationResult, setValidationResult] = useState(null);
+ const [saving, setSaving] = useState(false);
+
+ const handleValidate = async () => {
+ setValidating(true);
+ try {
+ const res = await fetch("/api/providers/validate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider, apiKey: formData.apiKey }),
+ });
+ const data = await res.json();
+ setValidationResult(data.valid ? "success" : "failed");
+ } catch {
+ setValidationResult("failed");
+ } finally {
+ setValidating(false);
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (!provider || !formData.apiKey) return;
+
+ setSaving(true);
+ try {
+ let isValid = false;
+ try {
+ setValidating(true);
+ setValidationResult(null);
+ const res = await fetch("/api/providers/validate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider, apiKey: formData.apiKey }),
+ });
+ const data = await res.json();
+ isValid = !!data.valid;
+ setValidationResult(isValid ? "success" : "failed");
+ } catch {
+ setValidationResult("failed");
+ } finally {
+ setValidating(false);
+ }
+
+ await onSave({
+ name: formData.name,
+ apiKey: formData.apiKey,
+ priority: formData.priority,
+ testStatus: isValid ? "active" : "unknown",
+ });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (!provider) return null;
+
+ return (
+
+
+
setFormData({ ...formData, name: e.target.value })}
+ placeholder="Production Key"
+ />
+
+
setFormData({ ...formData, apiKey: e.target.value })}
+ className="flex-1"
+ />
+
+
+ {validating ? "Checking..." : "Check"}
+
+
+
+ {validationResult && (
+
+ {validationResult === "success" ? "Valid" : "Invalid"}
+
+ )}
+ {isCompatible && (
+
+ {isAnthropic
+ ? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.`
+ : `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.`
+ }
+
+ )}
+
setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
+ />
+
+
+ {saving ? "Saving..." : "Save"}
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+AddApiKeyModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ provider: PropTypes.string,
+ providerName: PropTypes.string,
+ isCompatible: PropTypes.bool,
+ isAnthropic: PropTypes.bool,
+ onSave: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+function EditConnectionModal({ isOpen, connection, onSave, onClose }) {
+ const [formData, setFormData] = useState({
+ name: "",
+ priority: 1,
+ apiKey: "",
+ });
+ const [testing, setTesting] = useState(false);
+ const [testResult, setTestResult] = useState(null);
+ const [validating, setValidating] = useState(false);
+ const [validationResult, setValidationResult] = useState(null);
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ if (connection) {
+ setFormData({
+ name: connection.name || "",
+ priority: connection.priority || 1,
+ apiKey: "",
+ });
+ setTestResult(null);
+ setValidationResult(null);
+ }
+ }, [connection]);
+
+ const handleTest = async () => {
+ if (!connection?.provider) return;
+ setTesting(true);
+ setTestResult(null);
+ try {
+ const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" });
+ const data = await res.json();
+ setTestResult(data.valid ? "success" : "failed");
+ } catch {
+ setTestResult("failed");
+ } finally {
+ setTesting(false);
+ }
+ };
+
+ const handleValidate = async () => {
+ if (!connection?.provider || !formData.apiKey) return;
+ setValidating(true);
+ setValidationResult(null);
+ try {
+ const res = await fetch("/api/providers/validate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }),
+ });
+ const data = await res.json();
+ setValidationResult(data.valid ? "success" : "failed");
+ } catch {
+ setValidationResult("failed");
+ } finally {
+ setValidating(false);
+ }
+ };
+
+ const handleSubmit = async () => {
+ setSaving(true);
+ try {
+ const updates = { name: formData.name, priority: formData.priority };
+ if (!isOAuth && formData.apiKey) {
+ updates.apiKey = formData.apiKey;
+ let isValid = validationResult === "success";
+ if (!isValid) {
+ try {
+ setValidating(true);
+ setValidationResult(null);
+ const res = await fetch("/api/providers/validate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider: connection.provider, apiKey: formData.apiKey }),
+ });
+ const data = await res.json();
+ isValid = !!data.valid;
+ setValidationResult(isValid ? "success" : "failed");
+ } catch {
+ setValidationResult("failed");
+ } finally {
+ setValidating(false);
+ }
+ }
+ if (isValid) {
+ updates.testStatus = "active";
+ updates.lastError = null;
+ updates.lastErrorAt = null;
+ }
+ }
+ await onSave(updates);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (!connection) return null;
+
+ const isOAuth = connection.authType === "oauth";
+ const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider);
+
+ return (
+
+
+
setFormData({ ...formData, name: e.target.value })}
+ placeholder={isOAuth ? "Account name" : "Production Key"}
+ />
+ {isOAuth && connection.email && (
+
+
Email
+
{connection.email}
+
+ )}
+
setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
+ />
+ {!isOAuth && (
+ <>
+
+
setFormData({ ...formData, apiKey: e.target.value })}
+ placeholder="Enter new API key"
+ hint="Leave blank to keep the current API key."
+ className="flex-1"
+ />
+
+
+ {validating ? "Checking..." : "Check"}
+
+
+
+ {validationResult && (
+
+ {validationResult === "success" ? "Valid" : "Invalid"}
+
+ )}
+ >
+ )}
+
+ {/* Test Connection */}
+ {!isCompatible && (
+
+
+ {testing ? "Testing..." : "Test Connection"}
+
+ {testResult && (
+
+ {testResult === "success" ? "Valid" : "Failed"}
+
+ )}
+
+ )}
+
+
+ {saving ? "Saving..." : "Save"}
+ Cancel
+
+
+
+ );
+}
+
+EditConnectionModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ connection: PropTypes.shape({
+ id: PropTypes.string,
+ name: PropTypes.string,
+ email: PropTypes.string,
+ priority: PropTypes.number,
+ authType: PropTypes.string,
+ provider: PropTypes.string,
+ }),
+ onSave: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) {
+ const [formData, setFormData] = useState({
+ name: "",
+ prefix: "",
+ apiType: "chat",
+ baseUrl: "https://api.openai.com/v1",
+ });
+ const [saving, setSaving] = useState(false);
+ const [checkKey, setCheckKey] = useState("");
+ const [validating, setValidating] = useState(false);
+ const [validationResult, setValidationResult] = useState(null);
+
+ useEffect(() => {
+ if (node) {
+ setFormData({
+ name: node.name || "",
+ prefix: node.prefix || "",
+ apiType: node.apiType || "chat",
+ baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
+ });
+ }
+ }, [node, isAnthropic]);
+
+ const apiTypeOptions = [
+ { value: "chat", label: "Chat Completions" },
+ { value: "responses", label: "Responses API" },
+ ];
+
+ const handleSubmit = async () => {
+ if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
+ setSaving(true);
+ try {
+ const payload = {
+ name: formData.name,
+ prefix: formData.prefix,
+ baseUrl: formData.baseUrl,
+ };
+ if (!isAnthropic) {
+ payload.apiType = formData.apiType;
+ }
+ await onSave(payload);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleValidate = async () => {
+ setValidating(true);
+ try {
+ const res = await fetch("/api/provider-nodes/validate", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ baseUrl: formData.baseUrl,
+ apiKey: checkKey,
+ type: isAnthropic ? "anthropic-compatible" : "openai-compatible"
+ }),
+ });
+ const data = await res.json();
+ setValidationResult(data.valid ? "success" : "failed");
+ } catch {
+ setValidationResult("failed");
+ } finally {
+ setValidating(false);
+ }
+ };
+
+ if (!node) return null;
+
+ return (
+
+
+
+ );
+}
+
+EditCompatibleNodeModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ node: PropTypes.shape({
+ id: PropTypes.string,
+ name: PropTypes.string,
+ prefix: PropTypes.string,
+ apiType: PropTypes.string,
+ baseUrl: PropTypes.string,
+ }),
+ onSave: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ isAnthropic: PropTypes.bool,
+};