"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { useParams } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, Toggle } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { PROVIDER_ENDPOINTS } from "@/shared/constants/config"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; export default function ProviderDetailPage() { const params = useParams(); const providerId = params.id; const [connections, setConnections] = useState([]); const [loading, setLoading] = useState(true); const [showOAuthModal, setShowOAuthModal] = useState(false); const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [selectedConnection, setSelectedConnection] = useState(null); const [modelAliases, setModelAliases] = useState({}); const { copied, copy } = useCopyToClipboard(); const providerInfo = OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId]; const isOAuth = !!OAUTH_PROVIDERS[providerId]; const models = getModelsByProviderId(providerId); const providerAlias = getProviderAlias(providerId); // 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 res = await fetch("/api/providers"); const data = await res.json(); if (res.ok) { const filtered = (data.connections || []).filter(c => c.provider === providerId); setConnections(filtered); } } catch (error) { console.log("Error fetching connections:", error); } finally { setLoading(false); } }, [providerId]); useEffect(() => { fetchConnections(); fetchAliases(); }, [fetchConnections, fetchAliases]); const handleSetAlias = async (modelId, alias) => { const fullModel = `${providerAlias}/${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); } }; if (!providerInfo) { return (

Provider not found

Back to Providers
); } if (loading) { return (
); } return (
{/* Header */}
arrow_back Back to Providers
{providerInfo.name} { e.currentTarget.style.display = "none"; }} />

{providerInfo.name}

{connections.length} connection{connections.length !== 1 ? "s" : ""}

{/* Connections */}

Connections

{connections.length === 0 ? (
{isOAuth ? "lock" : "key"}

No connections yet

) : (
{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"}

{providerInfo.passthroughModels ? ( ) : models.length === 0 ? (

No models configured

) : (
{models.map((model) => { const fullModel = `${providerAlias}/${model.id}`; // Also check for old format (providerId/model) for backward compatibility const oldFormatModel = `${providerId}/${model.id}`; const existingAlias = Object.entries(modelAliases).find( ([, m]) => m === fullModel || m === oldFormatModel )?.[0]; return ( handleSetAlias(model.id, alias)} onDeleteAlias={() => handleDeleteAlias(existingAlias)} /> ); })}
)}
{/* Modals */} setShowOAuthModal(false)} /> setShowAddApiKeyModal(false)} /> setShowEditModal(false)} />
); } function ModelRow({ model, fullModel, alias, copied, onCopy }) { return (
smart_toy {fullModel}
); } 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 */}
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" />
{/* Models list */} {allModels.length > 0 && (
{allModels.map(({ modelId, fullModel, alias }) => ( onDeleteAlias(alias)} /> ))}
)}
); } function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias }) { return (
smart_toy

{modelId}

{fullModel}
{/* Delete button */}
); } 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} ); } 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"; }; const hasError = effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable"; return (
{/* Priority arrows */}
{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} )}
); } function AddApiKeyModal({ isOpen, provider, onSave, onClose }) { const [formData, setFormData] = useState({ name: "", apiKey: "", priority: 1, }); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); 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 = () => { onSave({ name: formData.name, apiKey: formData.apiKey, priority: formData.priority, testStatus: validationResult === "success" ? "active" : "unknown", }); }; if (!provider) return null; return (
setFormData({ ...formData, name: e.target.value })} placeholder="Production Key" />
setFormData({ ...formData, apiKey: e.target.value })} className="flex-1" />
{validationResult && ( {validationResult === "success" ? "Valid" : "Invalid"} )} setFormData({ ...formData, priority: parseInt(e.target.value) || 1 })} />
); } function EditConnectionModal({ isOpen, connection, onSave, onClose }) { const [formData, setFormData] = useState({ name: "", priority: 1, }); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); useEffect(() => { if (connection) { setFormData({ name: connection.name || "", priority: connection.priority || 1, }); setTestResult(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"); if (data.valid) { onSave({ testStatus: "active", lastError: null, lastErrorAt: null }); } else { onSave({ testStatus: "error", lastError: data.error, lastErrorAt: new Date().toISOString() }); } } catch { setTestResult("failed"); } finally { setTesting(false); } }; const handleSubmit = () => { const updates = { name: formData.name, priority: formData.priority }; onSave(updates); }; if (!connection) return null; const isOAuth = connection.authType === "oauth"; return (
setFormData({ ...formData, name: e.target.value })} placeholder={isOAuth ? "Account name" : "Production Key"} /> {isOAuth && connection.email && (

Email

{connection.email}

)} setFormData({ ...formData, priority: parseInt(e.target.value) || 1 })} /> {/* Test Connection */}
{testResult && ( {testResult === "success" ? "Valid" : "Failed"} )}
); }