Merge branch 'pr-44'
This commit is contained in:
commit
677cabc850
14 files changed
+614
-154
No files matched your search
@@ -6,7 +6,7 @@ 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 { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
|
||||
@@ -29,19 +29,24 @@ export default function ProviderDetailPage() {
|
||||
const providerInfo = providerNode
|
||||
? {
|
||||
id: providerNode.id,
|
||||
name: providerNode.name || "OpenAI Compatible",
|
||||
color: "#10A37F",
|
||||
textIcon: "OC",
|
||||
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]);
|
||||
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
|
||||
const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId);
|
||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible;
|
||||
|
||||
const providerStorageAlias = isCompatible ? providerId : providerAlias;
|
||||
const providerDisplayAlias = isCompatible
|
||||
? (providerNode?.prefix || providerId)
|
||||
: providerAlias;
|
||||
|
||||
@@ -238,9 +243,9 @@ export default function ProviderDetailPage() {
|
||||
};
|
||||
|
||||
const renderModelsSection = () => {
|
||||
if (isOpenAICompatible) {
|
||||
if (isCompatible) {
|
||||
return (
|
||||
<OpenAICompatibleModelsSection
|
||||
<CompatibleModelsSection
|
||||
providerStorageAlias={providerStorageAlias}
|
||||
providerDisplayAlias={providerDisplayAlias}
|
||||
modelAliases={modelAliases}
|
||||
@@ -249,6 +254,7 @@ export default function ProviderDetailPage() {
|
||||
onSetAlias={handleSetAlias}
|
||||
onDeleteAlias={handleDeleteAlias}
|
||||
connections={connections}
|
||||
isAnthropic={isAnthropicCompatible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -317,6 +323,9 @@ export default function ProviderDetailPage() {
|
||||
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`;
|
||||
};
|
||||
|
||||
@@ -361,14 +370,14 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpenAICompatible && providerNode && (
|
||||
{isCompatible && providerNode && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">OpenAI Compatible Details</h2>
|
||||
<h2 className="text-lg font-semibold">{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
{providerNode.apiType === "responses" ? "Responses API" : "Chat Completions"} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
|
||||
{providerNode.apiType === "responses" ? "responses" : "chat/completions"}
|
||||
{isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
|
||||
{isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -393,7 +402,7 @@ export default function ProviderDetailPage() {
|
||||
variant="secondary"
|
||||
icon="delete"
|
||||
onClick={async () => {
|
||||
if (!confirm("Delete this OpenAI Compatible node?")) return;
|
||||
if (!confirm(`Delete this ${isAnthropicCompatible ? "Anthropic" : "OpenAI"} Compatible node?`)) return;
|
||||
try {
|
||||
const res = await fetch(`/api/provider-nodes/${providerId}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
@@ -410,7 +419,7 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
{connections.length > 0 && (
|
||||
<p className="text-sm text-text-muted">
|
||||
Only one connection is allowed per OpenAI Compatible node. Add another node if you need more connections.
|
||||
Only one connection is allowed per compatible node. Add another node if you need more connections.
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
@@ -420,7 +429,7 @@ export default function ProviderDetailPage() {
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">Connections</h2>
|
||||
{!isOpenAICompatible && (
|
||||
{!isCompatible && (
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
@@ -438,7 +447,7 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">No connections yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
|
||||
{!isOpenAICompatible && (
|
||||
{!isCompatible && (
|
||||
<Button icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
|
||||
Add Connection
|
||||
</Button>
|
||||
@@ -499,7 +508,8 @@ export default function ProviderDetailPage() {
|
||||
isOpen={showAddApiKeyModal}
|
||||
provider={providerId}
|
||||
providerName={providerInfo.name}
|
||||
isOpenAICompatible={isOpenAICompatible}
|
||||
isCompatible={isCompatible}
|
||||
isAnthropic={isAnthropicCompatible}
|
||||
onSave={handleSaveApiKey}
|
||||
onClose={() => setShowAddApiKeyModal(false)}
|
||||
/>
|
||||
@@ -509,12 +519,15 @@ export default function ProviderDetailPage() {
|
||||
onSave={handleUpdateConnection}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
/>
|
||||
<EditOpenAICompatibleModal
|
||||
isOpen={showEditNodeModal}
|
||||
node={providerNode}
|
||||
onSave={handleUpdateNode}
|
||||
onClose={() => setShowEditNodeModal(false)}
|
||||
/>
|
||||
{isCompatible && (
|
||||
<EditCompatibleNodeModal
|
||||
isOpen={showEditNodeModal}
|
||||
node={providerNode}
|
||||
onSave={handleUpdateNode}
|
||||
onClose={() => setShowEditNodeModal(false)}
|
||||
isAnthropic={isAnthropicCompatible}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -685,7 +698,7 @@ PassthroughModelRow.propTypes = {
|
||||
onDeleteAlias: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function OpenAICompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections }) {
|
||||
function CompatibleModelsSection({ providerStorageAlias, providerDisplayAlias, modelAliases, copied, onCopy, onSetAlias, onDeleteAlias, connections, isAnthropic }) {
|
||||
const [newModel, setNewModel] = useState("");
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
@@ -775,7 +788,7 @@ function OpenAICompatibleModelsSection({ providerStorageAlias, providerDisplayAl
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-text-muted">
|
||||
Add OpenAI-compatible models manually or import them from the /models endpoint.
|
||||
Add {isAnthropic ? "Anthropic" : "OpenAI"}-compatible models manually or import them from the /models endpoint.
|
||||
</p>
|
||||
|
||||
<div className="flex items-end gap-2 flex-wrap">
|
||||
@@ -787,7 +800,7 @@ function OpenAICompatibleModelsSection({ providerStorageAlias, providerDisplayAl
|
||||
value={newModel}
|
||||
onChange={(e) => setNewModel(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
||||
placeholder="gpt-4o"
|
||||
placeholder={isAnthropic ? "claude-3-opus-20240229" : "gpt-4o"}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
@@ -823,7 +836,7 @@ function OpenAICompatibleModelsSection({ providerStorageAlias, providerDisplayAl
|
||||
);
|
||||
}
|
||||
|
||||
OpenAICompatibleModelsSection.propTypes = {
|
||||
CompatibleModelsSection.propTypes = {
|
||||
providerStorageAlias: PropTypes.string.isRequired,
|
||||
providerDisplayAlias: PropTypes.string.isRequired,
|
||||
modelAliases: PropTypes.object.isRequired,
|
||||
@@ -835,6 +848,7 @@ OpenAICompatibleModelsSection.propTypes = {
|
||||
id: PropTypes.string,
|
||||
isActive: PropTypes.bool,
|
||||
})).isRequired,
|
||||
isAnthropic: PropTypes.bool,
|
||||
};
|
||||
|
||||
function CooldownTimer({ until }) {
|
||||
@@ -997,7 +1011,7 @@ ConnectionRow.propTypes = {
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function AddApiKeyModal({ isOpen, provider, providerName, isOpenAICompatible, onSave, onClose }) {
|
||||
function AddApiKeyModal({ isOpen, provider, providerName, isCompatible, isAnthropic, onSave, onClose }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
apiKey: "",
|
||||
@@ -1062,9 +1076,12 @@ function AddApiKeyModal({ isOpen, provider, providerName, isOpenAICompatible, on
|
||||
{validationResult === "success" ? "Valid" : "Invalid"}
|
||||
</Badge>
|
||||
)}
|
||||
{isOpenAICompatible && (
|
||||
{isCompatible && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Validation checks {providerName || "OpenAI Compatible"} via /models on your base URL.
|
||||
{isAnthropic
|
||||
? `Validation checks ${providerName || "Anthropic Compatible"} by verifying the API key.`
|
||||
: `Validation checks ${providerName || "OpenAI Compatible"} via /models on your base URL.`
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<Input
|
||||
@@ -1090,7 +1107,8 @@ AddApiKeyModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
provider: PropTypes.string,
|
||||
providerName: PropTypes.string,
|
||||
isOpenAICompatible: PropTypes.bool,
|
||||
isCompatible: PropTypes.bool,
|
||||
isAnthropic: PropTypes.bool,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1168,7 +1186,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }) {
|
||||
if (!connection) return null;
|
||||
|
||||
const isOAuth = connection.authType === "oauth";
|
||||
const isCompatible = isOpenAICompatibleProvider(connection.provider);
|
||||
const isCompatible = isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} title="Edit Connection" onClose={onClose}>
|
||||
@@ -1254,7 +1272,7 @@ EditConnectionModal.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
function EditCompatibleNodeModal({ isOpen, node, onSave, onClose, isAnthropic }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
prefix: "",
|
||||
@@ -1272,10 +1290,10 @@ function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
name: node.name || "",
|
||||
prefix: node.prefix || "",
|
||||
apiType: node.apiType || "chat",
|
||||
baseUrl: node.baseUrl || "https://api.openai.com/v1",
|
||||
baseUrl: node.baseUrl || (isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
|
||||
});
|
||||
}
|
||||
}, [node]);
|
||||
}, [node, isAnthropic]);
|
||||
|
||||
const apiTypeOptions = [
|
||||
{ value: "chat", label: "Chat Completions" },
|
||||
@@ -1286,12 +1304,15 @@ function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave({
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
apiType: formData.apiType,
|
||||
baseUrl: formData.baseUrl,
|
||||
});
|
||||
};
|
||||
if (!isAnthropic) {
|
||||
payload.apiType = formData.apiType;
|
||||
}
|
||||
await onSave(payload);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -1303,7 +1324,11 @@ function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
const res = await fetch("/api/provider-nodes/validate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ baseUrl: formData.baseUrl, apiKey: checkKey }),
|
||||
body: JSON.stringify({
|
||||
baseUrl: formData.baseUrl,
|
||||
apiKey: checkKey,
|
||||
type: isAnthropic ? "anthropic-compatible" : "openai-compatible"
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data.valid ? "success" : "failed");
|
||||
@@ -1317,34 +1342,36 @@ function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
if (!node) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} title="Edit OpenAI Compatible" onClose={onClose}>
|
||||
<Modal isOpen={isOpen} title={`Edit ${isAnthropic ? "Anthropic" : "OpenAI"} Compatible`} onClose={onClose}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="OpenAI Compatible (Prod)"
|
||||
placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`}
|
||||
hint="Required. A friendly label for this node."
|
||||
/>
|
||||
<Input
|
||||
label="Prefix"
|
||||
value={formData.prefix}
|
||||
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
|
||||
placeholder="oc-prod"
|
||||
placeholder={isAnthropic ? "ac-prod" : "oc-prod"}
|
||||
hint="Required. Used as the provider prefix for model IDs."
|
||||
/>
|
||||
<Select
|
||||
label="API Type"
|
||||
options={apiTypeOptions}
|
||||
value={formData.apiType}
|
||||
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
|
||||
/>
|
||||
{!isAnthropic && (
|
||||
<Select
|
||||
label="API Type"
|
||||
options={apiTypeOptions}
|
||||
value={formData.apiType}
|
||||
onChange={(e) => setFormData({ ...formData, apiType: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="Base URL"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
hint="Use the base URL (ending in /v1) for your OpenAI-compatible API."
|
||||
placeholder={isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"}
|
||||
hint={`Use the base URL (ending in /v1) for your ${isAnthropic ? "Anthropic" : "OpenAI"}-compatible API.`}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@@ -1378,7 +1405,7 @@ function EditOpenAICompatibleModal({ isOpen, node, onSave, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
EditOpenAICompatibleModal.propTypes = {
|
||||
EditCompatibleNodeModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
node: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@@ -1389,4 +1416,5 @@ EditOpenAICompatibleModal.propTypes = {
|
||||
}),
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
isAnthropic: PropTypes.bool,
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import PropTypes from "prop-types";
|
||||
import { Card, CardSkeleton, Badge, Button, Input, Modal, Select } from "@/shared/components";
|
||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
|
||||
import { OPENAI_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
|
||||
import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
|
||||
import Link from "next/link";
|
||||
import { getErrorCode, getRelativeTime } from "@/shared/utils";
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function ProvidersPage() {
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddCompatibleModal, setShowAddCompatibleModal] = useState(false);
|
||||
const [showAddAnthropicCompatibleModal, setShowAddAnthropicCompatibleModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -103,12 +104,25 @@ export default function ProvidersPage() {
|
||||
apiType: node.apiType,
|
||||
}));
|
||||
|
||||
const anthropicCompatibleProviders = providerNodes
|
||||
.filter((node) => node.type === "anthropic-compatible")
|
||||
.map((node) => ({
|
||||
id: node.id,
|
||||
name: node.name || "Anthropic Compatible",
|
||||
color: "#D97757",
|
||||
textIcon: "AC",
|
||||
}));
|
||||
|
||||
const apiKeyProviders = {
|
||||
...APIKEY_PROVIDERS,
|
||||
...compatibleProviders.reduce((acc, provider) => {
|
||||
acc[provider.id] = provider;
|
||||
return acc;
|
||||
}, {}),
|
||||
...anthropicCompatibleProviders.reduce((acc, provider) => {
|
||||
acc[provider.id] = provider;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -141,9 +155,20 @@ export default function ProvidersPage() {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">API Key Providers</h2>
|
||||
<Button size="sm" icon="add" onClick={() => setShowAddCompatibleModal(true)}>
|
||||
Add OpenAI Compatible
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" icon="add" onClick={() => setShowAddAnthropicCompatibleModal(true)}>
|
||||
Add Anthropic Compatible
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => setShowAddCompatibleModal(true)}
|
||||
className="!bg-white !text-black hover:!bg-gray-100"
|
||||
>
|
||||
Add OpenAI Compatible
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{Object.entries(apiKeyProviders).map(([key, info]) => (
|
||||
@@ -164,6 +189,14 @@ export default function ProvidersPage() {
|
||||
setShowAddCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
<AddAnthropicCompatibleModal
|
||||
isOpen={showAddAnthropicCompatibleModal}
|
||||
onClose={() => setShowAddAnthropicCompatibleModal(false)}
|
||||
onCreated={(node) => {
|
||||
setProviderNodes((prev) => [...prev, node]);
|
||||
setShowAddAnthropicCompatibleModal(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -237,6 +270,7 @@ ProviderCard.propTypes = {
|
||||
function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
const { connected, error, errorCode, errorTime } = stats;
|
||||
const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
|
||||
const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
// Determine icon path: OpenAI Compatible providers use specialized icons
|
||||
@@ -244,6 +278,9 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
if (isCompatible) {
|
||||
return provider.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png";
|
||||
}
|
||||
if (isAnthropicCompatible) {
|
||||
return "/providers/anthropic-m.png"; // Use Anthropic icon as base
|
||||
}
|
||||
return `/providers/${provider.id}.png`;
|
||||
};
|
||||
|
||||
@@ -284,6 +321,11 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
{provider.apiType === "responses" ? "Responses" : "Chat"}
|
||||
</Badge>
|
||||
)}
|
||||
{isAnthropicCompatible && (
|
||||
<Badge variant="default" size="sm">
|
||||
Messages
|
||||
</Badge>
|
||||
)}
|
||||
{errorTime && <span className="text-text-muted">• {errorTime}</span>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -351,6 +393,7 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
prefix: formData.prefix,
|
||||
apiType: formData.apiType,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "openai-compatible",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
@@ -378,7 +421,7 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
const res = await fetch("/api/provider-nodes/validate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ baseUrl: formData.baseUrl, apiKey: checkKey }),
|
||||
body: JSON.stringify({ baseUrl: formData.baseUrl, apiKey: checkKey, type: "openai-compatible" }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data.valid ? "success" : "failed");
|
||||
@@ -456,3 +499,137 @@ AddOpenAICompatibleModal.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [checkKey, setCheckKey] = useState("");
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset validation when modal opens
|
||||
if (isOpen) {
|
||||
setValidationResult(null);
|
||||
setCheckKey("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const res = await fetch("/api/provider-nodes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
prefix: formData.prefix,
|
||||
baseUrl: formData.baseUrl,
|
||||
type: "anthropic-compatible",
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
onCreated(data.node);
|
||||
setFormData({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
});
|
||||
setCheckKey("");
|
||||
setValidationResult(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error creating Anthropic Compatible node:", error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
setValidating(true);
|
||||
try {
|
||||
const res = await fetch("/api/provider-nodes/validate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: formData.baseUrl,
|
||||
apiKey: checkKey,
|
||||
type: "anthropic-compatible"
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setValidationResult(data.valid ? "success" : "failed");
|
||||
} catch {
|
||||
setValidationResult("failed");
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} title="Add Anthropic Compatible" onClose={onClose}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Anthropic Compatible (Prod)"
|
||||
hint="Required. A friendly label for this node."
|
||||
/>
|
||||
<Input
|
||||
label="Prefix"
|
||||
value={formData.prefix}
|
||||
onChange={(e) => setFormData({ ...formData, prefix: e.target.value })}
|
||||
placeholder="ac-prod"
|
||||
hint="Required. Used as the provider prefix for model IDs."
|
||||
/>
|
||||
<Input
|
||||
label="Base URL"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
|
||||
placeholder="https://api.anthropic.com/v1"
|
||||
hint="Use the base URL (ending in /v1) for your Anthropic-compatible API. The system will append /messages."
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
label="API Key (for Check)"
|
||||
type="password"
|
||||
value={checkKey}
|
||||
onChange={(e) => setCheckKey(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="pt-6">
|
||||
<Button onClick={handleValidate} disabled={!checkKey || validating || !formData.baseUrl.trim()} variant="secondary">
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{validationResult && (
|
||||
<Badge variant={validationResult === "success" ? "success" : "error"}>
|
||||
{validationResult === "success" ? "Valid" : "Invalid"}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSubmit} fullWidth disabled={!formData.name.trim() || !formData.prefix.trim() || !formData.baseUrl.trim() || submitting}>
|
||||
{submitting ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddAnthropicCompatibleModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreated: PropTypes.func.isRequired,
|
||||
};
|
||||
Reference in new issue
Block a user