"use client";
import { useState, useEffect, useCallback } 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, Toggle, Select } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider } 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 { copied, copy } = useCopyToClipboard();
const providerInfo = providerNode
? {
id: providerNode.id,
name: providerNode.name || "OpenAI Compatible",
color: "#10A37F",
textIcon: "OC",
apiType: providerNode.apiType,
baseUrl: providerNode.baseUrl,
}
: (OAUTH_PROVIDERS[providerId] || APIKEY_PROVIDERS[providerId]);
const isOAuth = !!OAUTH_PROVIDERS[providerId];
const models = getModelsByProviderId(providerId);
const providerAlias = getProviderAlias(providerId);
const isOpenAICompatible = isOpenAICompatibleProvider(providerId);
const providerStorageAlias = isOpenAICompatible ? providerId : providerAlias;
const providerDisplayAlias = isOpenAICompatible
? (providerNode?.prefix || providerId)
: providerAlias;
// 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"),
fetch("/api/provider-nodes"),
]);
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) {
const node = (nodesData.nodes || []).find((entry) => entry.id === providerId) || null;
setProviderNode(node);
}
} catch (error) {
console.log("Error fetching connections:", error);
} finally {
setLoading(false);
}
}, [providerId]);
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);
}
};
useEffect(() => {
fetchConnections();
fetchAliases();
}, [fetchConnections, fetchAliases]);
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 (isOpenAICompatible) {
return (
);
}
if (providerInfo.passthroughModels) {
return (
);
}
if (models.length === 0) {
return
No models configured
;
}
return (
{models.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)}
/>
);
})}
);
};
if (loading) {
return (
);
}
if (!providerInfo) {
return (
Provider not found
Back to Providers
);
}
return (
{/* Header */}
arrow_back
Back to Providers
{providerInfo.textIcon ? (
{providerInfo.textIcon}
) : (
{ e.currentTarget.style.display = "none"; }}
/>
)}
{providerInfo.name}
{connections.length} connection{connections.length === 1 ? "" : "s"}
{isOpenAICompatible && providerNode && (
OpenAI Compatible Details
{providerNode.apiType === "responses" ? "Responses API" : "Chat Completions"} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
{providerNode.apiType === "responses" ? "responses" : "chat/completions"}
setShowAddApiKeyModal(true)}
disabled={connections.length > 0}
>
Add
setShowEditNodeModal(true)}
>
Edit
{
if (!confirm("Delete this 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 OpenAI Compatible node. Add another node if you need more connections.
)}
)}
{/* Connections */}
Connections
{!isOpenAICompatible && (
isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}
>
Add
)}
{connections.length === 0 ? (
{isOAuth ? "lock" : "key"}
No connections yet
Add your first connection to get started
{!isOpenAICompatible && (
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)}
/>
) : (
setShowOAuthModal(false)}
/>
)}
setShowAddApiKeyModal(false)}
/>
setShowEditModal(false)}
/>
setShowEditNodeModal(false)}
/>
);
}
function ModelRow({ model, fullModel, alias, copied, onCopy }) {
return (
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,
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 */}
{/* 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 OpenAICompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections }) {
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 OpenAI-compatible models manually or import them from the /models endpoint.
{!canImport && (
Add a connection to enable importing models.
)}
{allModels.length > 0 && (
{allModels.map(({ modelId, fullModel, alias }) => (
onDeleteAlias(alias)}
/>
))}
)}
);
}
OpenAICompatibleModelsSection.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,
};
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}
)}
);
}
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, isOpenAICompatible, 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"
/>
{validationResult && (
{validationResult === "success" ? "Valid" : "Invalid"}
)}
{isOpenAICompatible && (
Validation checks {providerName || "OpenAI Compatible"} via /models on your base URL.
)}
setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
/>
Save
Cancel
);
}
AddApiKeyModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
provider: PropTypes.string,
providerName: PropTypes.string,
isOpenAICompatible: 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);
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 = () => {
const updates = { name: formData.name, priority: formData.priority };
if (!isOAuth && formData.apiKey) {
updates.apiKey = formData.apiKey;
if (validationResult === "success") {
updates.testStatus = "active";
updates.lastError = null;
updates.lastErrorAt = null;
}
}
onSave(updates);
};
if (!connection) return null;
const isOAuth = connection.authType === "oauth";
const isCompatible = isOpenAICompatibleProvider(connection.provider);
return (
setFormData({ ...formData, name: e.target.value })}
placeholder={isOAuth ? "Account name" : "Production Key"}
/>
{isOAuth && connection.email && (
)}
setFormData({ ...formData, priority: Number.parseInt(e.target.value) || 1 })}
/>
{!isOAuth && (
<>
{validationResult && (
{validationResult === "success" ? "Valid" : "Invalid"}
)}
>
)}
{/* Test Connection */}
{!isCompatible && (
{testing ? "Testing..." : "Test Connection"}
{testResult && (
{testResult === "success" ? "Valid" : "Failed"}
)}
)}
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 EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
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 || "https://api.openai.com/v1",
});
}
}, [node]);
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 {
await onSave({
name: formData.name,
prefix: formData.prefix,
apiType: formData.apiType,
baseUrl: formData.baseUrl,
});
} 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 }),
});
const data = await res.json();
setValidationResult(data.valid ? "success" : "failed");
} catch {
setValidationResult("failed");
} finally {
setValidating(false);
}
};
if (!node) return null;
return (
);
}
EditOpenAICompatibleModal.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,
};