"use client"; import { useState, useEffect, useCallback } 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 } 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 { 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"; 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 [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); // null = use global, "round-robin" = override const [providerStickyLimit, setProviderStickyLimit] = useState(""); const [thinkingMode, setThinkingMode] = useState("auto"); const [suggestedModels, setSuggestedModels] = useState([]); const [kiloFreeModels, setKiloFreeModels] = useState([]); const [disabledModelIds, setDisabledModelIds] = useState([]); 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] || FREE_TIER_PROVIDERS[providerId] || WEB_COOKIE_PROVIDERS[providerId]); const isOAuth = !!OAUTH_PROVIDERS[providerId] || !!FREE_PROVIDERS[providerId]; 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 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; if (!confirm(`Disable all ${ids.length} model(s)?`)) return; 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); } }; 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 handleIFlowCookieSuccess = () => { fetchConnections(); setShowIFlowCookieModal(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 (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 { const results = await Promise.all(assignments.map(async ({ connectionId, proxyPoolId }) => { try { const res = await fetch(`/api/providers/${connectionId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyPoolId }), }); return res.ok; } catch (e) { console.log("Error applying proxy for", connectionId, e); return false; } })); const failed = results.filter((ok) => !ok).length; 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)} />
))}
); const activePools = proxyPools.filter((p) => p.isActive === true); const bulkActionModal = (

Apply single pool to all

{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) => !m.type || m.type === "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 */} {/* 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.length > 0 && (

Only one connection is allowed per compatible node. Add another node if you need more connections.

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

Connections

{connections.length > 0 && proxyPools.length > 0 && ( )} {/* 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

{!isCompatible && (
{providerId === "iflow" && ( )}
)}
) : ( <> {connectionsList} {!isCompatible && (
{providerId === "iflow" && ( )}
)} )}
)} {/* Models */}

{"Available Models"}

{!isCompatible && (() => { const allIds = [ ...models, ...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)), ].filter((m) => !m.type || m.type === "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)} /> )} 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)} /> )}
); }