"use client"; import { useState, useEffect, useCallback, useRef } from "react"; 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, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard, ConfirmModal } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { translate } from "@/i18n/runtime"; import { fetchSuggestedModels } from "@/shared/utils/providerModelsFetcher"; import ModelRow from "./ModelRow"; import PassthroughModelsSection from "./PassthroughModelsSection"; import CompatibleModelsSection from "./CompatibleModelsSection"; import ConnectionRow from "./ConnectionRow"; import AddApiKeyModal from "./AddApiKeyModal"; import EditCompatibleNodeModal from "./EditCompatibleNodeModal"; import AddCustomModelModal from "./AddCustomModelModal"; import BulkImportCodexModal from "./BulkImportCodexModal"; const ONE_BY_ONE_DELAY_MS = 1000; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } 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 [proxyPools, setProxyPools] = useState([]); const [showOAuthModal, setShowOAuthModal] = useState(false); const [showIFlowCookieModal, setShowIFlowCookieModal] = useState(false); const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); const [addConnectionError, setAddConnectionError] = useState(""); const [showBulkImportCodex, setShowBulkImportCodex] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showEditNodeModal, setShowEditNodeModal] = useState(false); const [showBulkProxyModal, setShowBulkProxyModal] = useState(false); const [selectedConnection, setSelectedConnection] = useState(null); const [modelAliases, setModelAliases] = useState({}); const [headerImgError, setHeaderImgError] = useState(false); const [modelTestResults, setModelTestResults] = useState({}); const [modelsTestError, setModelsTestError] = useState(""); const [testingModelId, setTestingModelId] = useState(null); const [showAddCustomModel, setShowAddCustomModel] = useState(false); const [selectedConnectionIds, setSelectedConnectionIds] = useState([]); const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__"); const [bulkUpdatingProxy, setBulkUpdatingProxy] = useState(false); const [providerStrategy, setProviderStrategy] = useState(null); const [providerStickyLimit, setProviderStickyLimit] = useState(""); const [thinkingMode, setThinkingMode] = useState("auto"); const [suggestedModels, setSuggestedModels] = useState([]); const [kiloFreeModels, setKiloFreeModels] = useState([]); const [disabledModelIds, setDisabledModelIds] = useState([]); const [confirmState, setConfirmState] = useState(null); const [showAgRiskModal, setShowAgRiskModal] = useState(false); const [oneByOneRunning, setOneByOneRunning] = useState(false); const [oneByOneStopping, setOneByOneStopping] = useState(false); const [oneByOneCurrentConnectionId, setOneByOneCurrentConnectionId] = useState(null); const [oneByOneResults, setOneByOneResults] = useState({}); const [oneByOneSummary, setOneByOneSummary] = useState(null); const stopOneByOneRef = useRef(false); const [importingQoderModels, setImportingQoderModels] = useState(false); const { copied, copy } = useCopyToClipboard(); const AG_RISK_STORAGE_KEY = "ag_risk_confirmed"; const openOAuthConnection = () => { setShowOAuthModal(true); }; const triggerOAuthConnection = () => { if (providerId === "antigravity" && typeof window !== "undefined") { const confirmed = window.localStorage.getItem(AG_RISK_STORAGE_KEY) === "true"; if (!confirmed) { setShowAgRiskModal(true); return; } } if (isOAuth) { openOAuthConnection(); return; } setAddConnectionError(""); setShowAddApiKeyModal(true); }; const triggerApiKeyConnection = () => { setAddConnectionError(""); setShowAddApiKeyModal(true); }; const triggerAddConnection = () => { if (isOAuth) { triggerOAuthConnection(); return; } triggerApiKeyConnection(); }; const handleAgRiskConfirm = () => { if (typeof window !== "undefined") { window.localStorage.setItem(AG_RISK_STORAGE_KEY, "true"); } setShowAgRiskModal(false); if (isOAuth) { openOAuthConnection(); return; } triggerApiKeyConnection(); }; 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] || FREE_TIER_PROVIDERS[providerId] || WEB_COOKIE_PROVIDERS[providerId]); const authModes = providerInfo?.authModes || []; const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId] || authModes.includes("oauth"); const supportsApiKeyAuth = !!APIKEY_PROVIDERS[providerId] || authModes.includes("apikey"); const isFreeNoAuth = !!FREE_PROVIDERS[providerId]?.noAuth; const models = getModelsByProviderId(providerId); const providerAlias = getProviderAlias(providerId); const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible; const hasDualAuthModes = !isCompatible && isOAuth && supportsApiKeyAuth; const oauthConnectionLabel = providerId === "xai" ? "Grok Build OAuth" : "OAuth"; const apiKeyConnectionLabel = providerId === "xai" ? "xAI API Key" : "API Key"; const thinkingConfig = AI_PROVIDERS[providerId]?.thinkingConfig || THINKING_CONFIG.extended; const providerStorageAlias = isCompatible ? providerId : providerAlias; const providerDisplayAlias = isCompatible ? (providerNode?.prefix || providerId) : providerAlias; const fetchDisabledModels = useCallback(async () => { try { const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}`, { cache: "no-store" }); const data = await res.json(); if (res.ok) setDisabledModelIds(data.ids || []); } catch (error) { console.log("Error fetching disabled models:", error); } }, [providerStorageAlias]); const handleDisableModel = async (modelId) => { try { const res = await fetch("/api/models/disabled", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerAlias: providerStorageAlias, ids: [modelId] }), }); if (res.ok) await fetchDisabledModels(); } catch (error) { console.log("Error disabling model:", error); } }; const handleEnableModel = async (modelId) => { try { const res = await fetch(`/api/models/disabled?providerAlias=${encodeURIComponent(providerStorageAlias)}&id=${encodeURIComponent(modelId)}`, { method: "DELETE" }); if (res.ok) await fetchDisabledModels(); } catch (error) { console.log("Error enabling model:", error); } }; 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 { 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); } }, []); // Fetch free models from Kilo API for kilocode provider useEffect(() => { if (providerId !== "kilocode") return; fetch("/api/providers/kilo/free-models") .then((res) => res.json()) .then((data) => { if (data.models?.length) setKiloFreeModels(data.models); }) .catch(() => {}); }, [providerId]); const fetchConnections = useCallback(async () => { try { const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([ fetch("/api/providers", { cache: "no-store" }), fetch("/api/provider-nodes", { cache: "no-store" }), fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }), fetch("/api/settings", { cache: "no-store" }), ]); const connectionsData = await connectionsRes.json(); const nodesData = await nodesRes.json(); const proxyPoolsData = await proxyPoolsRes.json(); const settingsData = settingsRes.ok ? await settingsRes.json() : {}; if (connectionsRes.ok) { const filtered = (connectionsData.connections || []).filter(c => c.provider === providerId); setConnections(filtered); } if (proxyPoolsRes.ok) { setProxyPools(proxyPoolsData.proxyPools || []); } // Load per-provider strategy override const override = (settingsData.providerStrategies || {})[providerId] || {}; setProviderStrategy(override.fallbackStrategy || null); setProviderStickyLimit(override.stickyRoundRobinLimit != null ? String(override.stickyRoundRobinLimit) : "1"); // Load per-provider thinking config const thinkingCfg = (settingsData.providerThinking || {})[providerId] || {}; setThinkingMode(thinkingCfg.mode || "auto"); 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 saveProviderStrategy = async (strategy, stickyLimit) => { try { const settingsRes = await fetch("/api/settings", { cache: "no-store" }); const settingsData = settingsRes.ok ? await settingsRes.json() : {}; const current = settingsData.providerStrategies || {}; // Build override: null strategy means remove override, use global const override = {}; if (strategy) override.fallbackStrategy = strategy; if (strategy === "round-robin" && stickyLimit !== "") { override.stickyRoundRobinLimit = Number(stickyLimit) || 3; } const updated = { ...current }; if (Object.keys(override).length === 0) { delete updated[providerId]; } else { updated[providerId] = override; } await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerStrategies: updated }), }); } catch (error) { console.log("Error saving provider strategy:", error); } }; const handleRoundRobinToggle = (enabled) => { const strategy = enabled ? "round-robin" : null; const sticky = enabled ? (providerStickyLimit || "1") : providerStickyLimit; if (enabled && !providerStickyLimit) setProviderStickyLimit("1"); setProviderStrategy(strategy); saveProviderStrategy(strategy, sticky); }; const handleStickyLimitChange = (value) => { setProviderStickyLimit(value); saveProviderStrategy("round-robin", value); }; const saveThinkingConfig = async (mode) => { try { const settingsRes = await fetch("/api/settings", { cache: "no-store" }); const settingsData = settingsRes.ok ? await settingsRes.json() : {}; const current = settingsData.providerThinking || {}; const updated = { ...current }; if (!mode || mode === "auto") { delete updated[providerId]; } else { updated[providerId] = { mode }; } await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providerThinking: updated }), }); } catch (error) { console.log("Error saving thinking config:", error); } }; const handleThinkingModeChange = (mode) => { setThinkingMode(mode); saveThinkingConfig(mode); }; useEffect(() => { fetchConnections(); fetchAliases(); fetchDisabledModels(); }, [fetchConnections, fetchAliases, fetchDisabledModels]); // Fetch suggested models from provider's public API (if configured) useEffect(() => { const fetcher = (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId] || FREE_PROVIDERS[providerId] || FREE_TIER_PROVIDERS[providerId])?.modelsFetcher; if (!fetcher) return; fetchSuggestedModels(fetcher).then(setSuggestedModels); }, [providerId]); 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); } }; // Fetch Qoder model list and automatically add to available models const handleImportQoderModels = async () => { if (importingQoderModels) return; const activeConnection = connections.find((conn) => conn.isActive !== false); if (!activeConnection) { alert(translate("Please add an active Qoder connection first")); return; } setImportingQoderModels(true); try { const res = await fetch(`/api/providers/${activeConnection.id}/models`); const data = await res.json(); if (!res.ok) { alert(data.error || translate("Failed to fetch models")); return; } const models = data.models || []; if (models.length === 0) { alert(translate("No models returned")); return; } let importedCount = 0; for (const model of models) { const modelId = model.id || model.name; if (!modelId) continue; // Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix const cleanModelId = modelId.replace(/^qoder\//, ""); const fullModel = `${providerStorageAlias}/${cleanModelId}`; // Check if already exists if (Object.values(modelAliases).includes(fullModel)) { continue; } // Use model ID as alias const alias = cleanModelId; if (modelAliases[alias]) { continue; } await handleSetAlias(cleanModelId, alias, providerStorageAlias); importedCount += 1; } if (importedCount === 0) { alert(translate("All models already exist, no new models added")); } else { alert(translate("Successfully added") + ` ${importedCount} ` + translate("models")); } } catch (error) { console.log("Error importing Qoder models:", error); alert(translate("Error fetching models") + ": " + error.message); } finally { setImportingQoderModels(false); } }; const handleRunOneByOneTest = async () => { if (oneByOneRunning || connections.length === 0) return; const queuedState = Object.fromEntries( connections.map((connection) => [connection.id, { state: "queued", error: null }]), ); stopOneByOneRef.current = false; setOneByOneRunning(true); setOneByOneStopping(false); setOneByOneCurrentConnectionId(null); setOneByOneResults(queuedState); setOneByOneSummary({ total: connections.length, completed: 0, passed: 0, failed: 0, stopped: false }); let passed = 0; let failed = 0; try { for (let index = 0; index < connections.length; index += 1) { if (stopOneByOneRef.current) { setOneByOneSummary({ total: connections.length, completed: index, passed, failed, stopped: true, }); break; } const connection = connections[index]; setOneByOneCurrentConnectionId(connection.id); setOneByOneResults((prev) => ({ ...prev, [connection.id]: { state: "testing", error: null }, })); try { const res = await fetch(`/api/providers/${connection.id}/test`, { method: "POST" }); const data = await res.json(); const valid = !!data.valid; if (valid) { passed += 1; } else { failed += 1; } setOneByOneResults((prev) => ({ ...prev, [connection.id]: { state: valid ? "success" : "failed", error: valid ? null : (data.error || null), }, })); } catch (error) { failed += 1; setOneByOneResults((prev) => ({ ...prev, [connection.id]: { state: "failed", error: error.message || "Test failed", }, })); } setOneByOneSummary({ total: connections.length, completed: index + 1, passed, failed, stopped: false, }); if (index < connections.length - 1) { await sleep(ONE_BY_ONE_DELAY_MS); } } } finally { setOneByOneCurrentConnectionId(null); setOneByOneRunning(false); setOneByOneStopping(false); stopOneByOneRef.current = false; } }; const handleStopOneByOneTest = () => { if (!oneByOneRunning) return; stopOneByOneRef.current = true; setOneByOneStopping(true); }; const handleDelete = async (id) => { setConfirmState({ title: "Delete Connection", message: "Delete this connection?", onConfirm: async () => { setConfirmState(null); 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 handleIFlowCookieSuccess = () => { fetchConnections(); setShowIFlowCookieModal(false); }; const handleSaveApiKey = async (formData) => { setAddConnectionError(""); try { const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: providerId, ...formData }), }); let data = null; try { data = await res.json(); } catch { data = null; } if (res.ok) { await fetchConnections(); setShowAddApiKeyModal(false); return; } setAddConnectionError(data?.error || "Failed to save connection"); } catch (error) { console.log("Error saving connection:", error); setAddConnectionError("Failed to save connection"); } }; 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 (index1, index2) => { // Optimistic update state const newConnections = [...connections]; [newConnections[index1], newConnections[index2]] = [newConnections[index2], newConnections[index1]]; setConnections(newConnections); try { await Promise.all([ fetch(`/api/providers/${newConnections[index1].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: index1 }), }), fetch(`/api/providers/${newConnections[index2].id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ priority: index2 }), }), ]); } catch (error) { console.log("Error swapping priority:", error); await fetchConnections(); } }; const selectedConnections = connections.filter((conn) => selectedConnectionIds.includes(conn.id)); const allSelected = connections.length > 0 && selectedConnectionIds.length === connections.length; const toggleSelectConnection = (connectionId) => { setSelectedConnectionIds((prev) => ( prev.includes(connectionId) ? prev.filter((id) => id !== connectionId) : [...prev, connectionId] )); }; const toggleSelectAllConnections = () => { if (allSelected) { setSelectedConnectionIds([]); return; } setSelectedConnectionIds(connections.map((conn) => conn.id)); }; const clearSelection = () => { setSelectedConnectionIds([]); setBulkProxyPoolId("__none__"); }; useEffect(() => { setSelectedConnectionIds((prev) => prev.filter((id) => connections.some((conn) => conn.id === id))); }, [connections]); const selectedProxySummary = (() => { if (selectedConnections.length === 0) return ""; const poolIds = new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__")); if (poolIds.size === 1) { const onlyId = [...poolIds][0]; if (onlyId === "__none__") return "All selected currently unbound"; const pool = proxyPools.find((p) => p.id === onlyId); return `All selected currently bound to ${pool?.name || onlyId}`; } return "Selected connections have mixed proxy bindings"; })(); const openBulkProxyModal = () => { if (selectedConnections.length === 0) return; const uniquePoolIds = [...new Set(selectedConnections.map((conn) => conn.providerSpecificData?.proxyPoolId || "__none__"))]; setBulkProxyPoolId(uniquePoolIds.length === 1 ? uniquePoolIds[0] : "__none__"); setShowBulkProxyModal(true); }; const closeBulkProxyModal = () => { if (bulkUpdatingProxy) return; setShowBulkProxyModal(false); }; const applyProxyAssignments = async (assignments) => { setBulkUpdatingProxy(true); try { let failed = 0; for (const { connectionId, proxyPoolId } of assignments) { try { const res = await fetch(`/api/providers/${connectionId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyPoolId }), }); if (!res.ok) failed += 1; } catch (e) { console.log("Error applying proxy for", connectionId, e); failed += 1; } } if (failed > 0) alert(`Updated with ${failed} failed request(s).`); await fetchConnections(); setShowBulkProxyModal(false); } finally { setBulkUpdatingProxy(false); } }; const handleApplySinglePool = (proxyPoolId) => { const targets = connections.map((c) => ({ connectionId: c.id, proxyPoolId })); return applyProxyAssignments(targets); }; const handleApplyOneToOne = () => { const activePools = proxyPools.filter((p) => p.isActive === true); if (activePools.length === 0) { alert("No active proxy pools available."); return; } const targets = connections.map((c, i) => ({ connectionId: c.id, proxyPoolId: activePools[i % activePools.length].id, })); return applyProxyAssignments(targets); }; const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId); const connectionsList = (
{connections .map((conn, index) => (
handleSwapPriority(index, index - 1)} onMoveDown={() => handleSwapPriority(index, index + 1)} onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)} onUpdateProxy={async (proxyPoolId) => { try { const res = await fetch(`/api/providers/${conn.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyPoolId: proxyPoolId || null }), }); if (res.ok) { setConnections(prev => prev.map(c => c.id === conn.id ? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } } : c )); } } catch (error) { console.log("Error updating proxy:", error); } }} onEdit={() => { setSelectedConnection(conn); setShowEditModal(true); }} onDelete={() => handleDelete(conn.id)} oneByOneStatus={oneByOneResults[conn.id] || null} />
))}
); const activePools = proxyPools.filter((p) => p.isActive === true); const bulkActionModal = (
{proxyPools.map((pool) => ( ))}
{bulkUpdatingProxy &&

Applying...

}
); const handleTestModel = async (modelId) => { if (testingModelId) return; setTestingModelId(modelId); try { const res = await fetch("/api/models/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: `${providerStorageAlias}/${modelId}` }), }); const data = await res.json(); setModelTestResults((prev) => ({ ...prev, [modelId]: data.ok ? "ok" : "error" })); setModelsTestError(data.ok ? "" : (data.error || "Model not reachable")); } catch { setModelTestResults((prev) => ({ ...prev, [modelId]: "error" })); setModelsTestError("Network error"); } finally { setTestingModelId(null); } }; const renderModelsSection = () => { if (isCompatible) { return ( ); } // Combine hardcoded models with Kilo free models (deduplicated) // Exclude non-llm models (embedding, tts, etc.) — they have dedicated pages under media-providers const allModels = [ ...models, ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)), ].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; }); const disabledSet = new Set(disabledModelIds); const displayModels = allModels.filter((m) => !disabledSet.has(m.id)); const disabledDisplayModels = allModels.filter((m) => disabledSet.has(m.id)); // Custom models added by user (stored as aliases: modelId → providerAlias/modelId) const customModels = Object.entries(modelAliases) .filter(([alias, fullModel]) => { const prefix = `${providerStorageAlias}/`; if (!fullModel.startsWith(prefix)) return false; const modelId = fullModel.slice(prefix.length); // Only show if not already in hardcoded list // For passthroughModels, include all aliases (model IDs may contain slashes like "anthropic/claude-3") if (providerInfo.passthroughModels) return !models.some((m) => m.id === modelId); return !models.some((m) => m.id === modelId) && alias === modelId; }) .map(([alias, fullModel]) => ({ id: fullModel.slice(`${providerStorageAlias}/`.length), alias, fullModel, })); return (
{/* Custom models first */} {customModels.map((model) => ( {}} onDeleteAlias={() => handleDeleteAlias(model.alias)} testStatus={modelTestResults[model.id]} onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isCustom isFree={false} /> ))} {displayModels.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 ( handleSetAlias(model.id, alias, providerStorageAlias)} onDeleteAlias={() => handleDeleteAlias(existingAlias)} testStatus={modelTestResults[model.id]} onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined} isTesting={testingModelId === model.id} isFree={model.isFree} onDisable={() => handleDisableModel(model.id)} /> ); })} {/* Add model button — inline, same style as model chips */} {/* Import Qoder models button — only show for qoder provider */} {providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && ( )} {/* Suggested models from provider API — show only models not yet added */} {suggestedModels.length > 0 && (() => { const addedFullModels = new Set(Object.values(modelAliases)); const hardcodedIds = new Set(models.map((m) => m.id)); const notAdded = suggestedModels.filter( (m) => !addedFullModels.has(`${providerStorageAlias}/${m.id}`) && !hardcodedIds.has(m.id) ); if (notAdded.length === 0) return null; return (

Suggested free models (≥200k context):

{notAdded.map((m) => ( ))}
); })()} {/* Disabled models — restorable */} {disabledDisplayModels.length > 0 && (

Disabled models ({disabledDisplayModels.length}):

{disabledDisplayModels.map((m) => ( ))}
)}
); }; 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()} ) : ( {providerInfo.name} setHeaderImgError(true)} /> )}

{providerInfo.name}

{(providerInfo.notice?.apiKeyUrl || providerInfo.notice?.signupUrl || providerInfo.website) && ( open_in_new {providerInfo.notice?.apiKeyUrl ? "Get API Key" : "Sign up / Learn more"} )}

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

{providerInfo.deprecated && (
warning

{providerInfo.deprecationNotice}

)} {providerInfo.notice?.text && !providerInfo.deprecated && (
info

{providerInfo.notice.text}

{providerInfo.notice.apiKeyUrl && ( Get API Key → )}
)} {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")}

)} {/* Connections */} {isFreeNoAuth ? ( ) : (

Connections

{connections.length > 0 && proxyPools.length > 0 && ( )} {connections.length > 0 && ( <> {oneByOneRunning && ( )} )} {/* Thinking config */} {/* {thinkingConfig && (
Thinking
)} */} {/* Round Robin toggle */}
Round Robin {providerStrategy === "round-robin" && (
Sticky: handleStickyLimitChange(e.target.value)} placeholder="1" className="w-14 px-2 py-1 text-xs border border-border rounded-md bg-background focus:outline-none focus:border-primary" />
)}
{connections.length === 0 ? (
{isOAuth ? "lock" : "key"}

No connections yet

{hasDualAuthModes && (

Choose {oauthConnectionLabel} or {apiKeyConnectionLabel}.

)}
{hasDualAuthModes ? ( <> ) : ( <> {!isCompatible && providerId === "iflow" && ( )} {providerId === "codex" && ( )} )}
) : ( <> {oneByOneSummary && (
Total: {oneByOneSummary.total} Completed: {oneByOneSummary.completed} Passed: {oneByOneSummary.passed} Failed: {oneByOneSummary.failed} {oneByOneSummary.stopped && ( Stopped )} {oneByOneRunning && oneByOneCurrentConnectionId && ( Running: {connections.find((conn) => conn.id === oneByOneCurrentConnectionId)?.name || oneByOneCurrentConnectionId} )}
)} {connectionsList} {!isCompatible && (
{providerId === "iflow" && ( )} {providerId === "codex" && ( )} {hasDualAuthModes ? ( <> ) : ( )}
)} )}
)} {/* Models */}

{"Available Models"}

{!isCompatible && (() => { const allIds = [ ...models, ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)), ].filter((m) => { const k = m.kind || m.type; return !k || k === "llm"; }).map((m) => m.id); const activeIds = allIds.filter((id) => !disabledModelIds.includes(id)); return (
{disabledModelIds.length > 0 && ( )} {activeIds.length > 0 && ( )}
); })()}
{!!modelsTestError && (

{modelsTestError}

)} {renderModelsSection()}
{bulkActionModal} {/* Modals */} {providerId === "kiro" ? ( setShowOAuthModal(false)} /> ) : providerId === "cursor" ? ( setShowOAuthModal(false)} /> ) : providerId === "gitlab" ? ( setShowOAuthModal(false)} /> ) : ( setShowOAuthModal(false)} /> )} {providerId === "iflow" && ( setShowIFlowCookieModal(false)} /> )} { setAddConnectionError(""); setShowAddApiKeyModal(false); }} /> setShowEditModal(false)} /> {isCompatible && ( setShowEditNodeModal(false)} isAnthropic={isAnthropicCompatible} /> )} {!isCompatible && ( { // For passthrough providers (OpenRouter), use last segment as alias to avoid slash conflicts const alias = providerInfo?.passthroughModels ? modelId.split("/").pop() : modelId; await handleSetAlias(modelId, alias, providerStorageAlias); setShowAddCustomModel(false); }} onClose={() => setShowAddCustomModel(false)} /> )} {providerId === "codex" && ( setShowBulkImportCodex(false)} onSuccess={fetchConnections} /> )} {/* AG Risk Confirmation Modal */} setShowAgRiskModal(false)} onConfirm={handleAgRiskConfirm} title="Risk Notice" message={providerInfo?.deprecationNotice} confirmText="I Understand, Continue" cancelText="Cancel" variant="danger" /> {/* Confirm Modal */} setConfirmState(null)} onConfirm={confirmState?.onConfirm} title={confirmState?.title || "Confirm"} message={confirmState?.message} variant="danger" />
); }