- 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.
@@ -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"
/>
@@ -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"}
)}
- {isOpenAICompatible && (
+ {isCompatible && (
- 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.`
+ }
)}
@@ -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 (
-
+
setFormData({ ...formData, name: e.target.value })}
- placeholder="OpenAI Compatible (Prod)"
+ placeholder={`${isAnthropic ? "Anthropic" : "OpenAI"} Compatible (Prod)`}
hint="Required. A friendly label for this node."
/>
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."
/>
-
setFormData({ ...formData, apiType: e.target.value })}
- />
+ {!isAnthropic && (
+ setFormData({ ...formData, apiType: e.target.value })}
+ />
+ )}
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.`}
/>
{
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() {
API Key Providers
-
setShowAddCompatibleModal(true)}>
- Add OpenAI Compatible
-
+
+ setShowAddAnthropicCompatibleModal(true)}>
+ Add Anthropic Compatible
+
+ setShowAddCompatibleModal(true)}
+ className="!bg-white !text-black hover:!bg-gray-100"
+ >
+ Add OpenAI Compatible
+
+
{Object.entries(apiKeyProviders).map(([key, info]) => (
@@ -164,6 +189,14 @@ export default function ProvidersPage() {
setShowAddCompatibleModal(false);
}}
/>
+
setShowAddAnthropicCompatibleModal(false)}
+ onCreated={(node) => {
+ setProviderNodes((prev) => [...prev, node]);
+ setShowAddAnthropicCompatibleModal(false);
+ }}
+ />
);
}
@@ -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"}
)}
+ {isAnthropicCompatible && (
+
+ Messages
+
+ )}
{errorTime &&
• {errorTime} }
@@ -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 (
+
+
+
setFormData({ ...formData, name: e.target.value })}
+ placeholder="Anthropic Compatible (Prod)"
+ hint="Required. A friendly label for this node."
+ />
+
setFormData({ ...formData, prefix: e.target.value })}
+ placeholder="ac-prod"
+ hint="Required. Used as the provider prefix for model IDs."
+ />
+
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."
+ />
+
+
setCheckKey(e.target.value)}
+ className="flex-1"
+ />
+
+
+ {validating ? "Checking..." : "Check"}
+
+
+
+ {validationResult && (
+
+ {validationResult === "success" ? "Valid" : "Invalid"}
+
+ )}
+
+
+ {submitting ? "Creating..." : "Create"}
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+AddAnthropicCompatibleModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onCreated: PropTypes.func.isRequired,
+};
diff --git a/src/app/api/provider-nodes/[id]/route.js b/src/app/api/provider-nodes/[id]/route.js
index 7d5027a..35ea6a0 100644
--- a/src/app/api/provider-nodes/[id]/route.js
+++ b/src/app/api/provider-nodes/[id]/route.js
@@ -21,7 +21,8 @@ export async function PUT(request, { params }) {
return NextResponse.json({ error: "Prefix is required" }, { status: 400 });
}
- if (!apiType || !["chat", "responses"].includes(apiType)) {
+ // Only validate apiType for OpenAI Compatible nodes
+ if (node.type === "openai-compatible" && (!apiType || !["chat", "responses"].includes(apiType))) {
return NextResponse.json({ error: "Invalid OpenAI compatible API type" }, { status: 400 });
}
@@ -29,12 +30,27 @@ export async function PUT(request, { params }) {
return NextResponse.json({ error: "Base URL is required" }, { status: 400 });
}
- const updated = await updateProviderNode(id, {
+ let sanitizedBaseUrl = baseUrl.trim();
+
+ // Sanitize Base URL for Anthropic Compatible
+ if (node.type === "anthropic-compatible") {
+ sanitizedBaseUrl = sanitizedBaseUrl.replace(/\/$/, "");
+ if (sanitizedBaseUrl.endsWith("/messages")) {
+ sanitizedBaseUrl = sanitizedBaseUrl.slice(0, -9); // remove /messages
+ }
+ }
+
+ const updates = {
name: name.trim(),
prefix: prefix.trim(),
- apiType,
- baseUrl: baseUrl.trim(),
- });
+ baseUrl: sanitizedBaseUrl,
+ };
+
+ if (node.type === "openai-compatible") {
+ updates.apiType = apiType;
+ }
+
+ const updated = await updateProviderNode(id, updates);
const connections = await getProviderConnections({ provider: id });
await Promise.all(connections.map((connection) => (
@@ -42,8 +58,8 @@ export async function PUT(request, { params }) {
providerSpecificData: {
...(connection.providerSpecificData || {}),
prefix: prefix.trim(),
- apiType,
- baseUrl: baseUrl.trim(),
+ apiType: node.type === "openai-compatible" ? apiType : undefined,
+ baseUrl: sanitizedBaseUrl,
nodeName: updated.name,
}
})
diff --git a/src/app/api/provider-nodes/route.js b/src/app/api/provider-nodes/route.js
index de02431..cba1516 100644
--- a/src/app/api/provider-nodes/route.js
+++ b/src/app/api/provider-nodes/route.js
@@ -1,11 +1,15 @@
import { NextResponse } from "next/server";
import { createProviderNode, getProviderNodes } from "@/models";
-import { OPENAI_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
+import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX } from "@/shared/constants/providers";
const OPENAI_COMPATIBLE_DEFAULTS = {
baseUrl: "https://api.openai.com/v1",
};
+const ANTHROPIC_COMPATIBLE_DEFAULTS = {
+ baseUrl: "https://api.anthropic.com/v1",
+};
+
// GET /api/provider-nodes - List all provider nodes
export async function GET() {
try {
@@ -21,7 +25,7 @@ export async function GET() {
export async function POST(request) {
try {
const body = await request.json();
- const { name, prefix, apiType, baseUrl } = body;
+ const { name, prefix, apiType, baseUrl, type } = body;
if (!name?.trim()) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
@@ -31,20 +35,44 @@ export async function POST(request) {
return NextResponse.json({ error: "Prefix is required" }, { status: 400 });
}
- if (!apiType || !["chat", "responses"].includes(apiType)) {
- return NextResponse.json({ error: "Invalid OpenAI compatible API type" }, { status: 400 });
+ // Determine type
+ const nodeType = type || "openai-compatible";
+
+ if (nodeType === "openai-compatible") {
+ if (!apiType || !["chat", "responses"].includes(apiType)) {
+ return NextResponse.json({ error: "Invalid OpenAI compatible API type" }, { status: 400 });
+ }
+
+ const node = await createProviderNode({
+ id: `${OPENAI_COMPATIBLE_PREFIX}${apiType}-${crypto.randomUUID()}`,
+ type: "openai-compatible",
+ prefix: prefix.trim(),
+ apiType,
+ baseUrl: (baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl).trim(),
+ name: name.trim(),
+ });
+ return NextResponse.json({ node }, { status: 201 });
}
- const node = await createProviderNode({
- id: `${OPENAI_COMPATIBLE_PREFIX}${apiType}-${crypto.randomUUID()}`,
- type: "openai-compatible",
- prefix: prefix.trim(),
- apiType,
- baseUrl: (baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl).trim(),
- name: name.trim(),
- });
+ if (nodeType === "anthropic-compatible") {
+ // Sanitize Base URL: remove trailing slash, and remove trailing /messages if user added it
+ // This prevents double-appending /messages at runtime
+ let sanitizedBaseUrl = (baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl).trim().replace(/\/$/, "");
+ if (sanitizedBaseUrl.endsWith("/messages")) {
+ sanitizedBaseUrl = sanitizedBaseUrl.slice(0, -9); // remove /messages
+ }
- return NextResponse.json({ node }, { status: 201 });
+ const node = await createProviderNode({
+ id: `${ANTHROPIC_COMPATIBLE_PREFIX}${crypto.randomUUID()}`,
+ type: "anthropic-compatible",
+ prefix: prefix.trim(),
+ baseUrl: sanitizedBaseUrl,
+ name: name.trim(),
+ });
+ return NextResponse.json({ node }, { status: 201 });
+ }
+
+ return NextResponse.json({ error: "Invalid provider node type" }, { status: 400 });
} catch (error) {
console.log("Error creating provider node:", error);
return NextResponse.json({ error: "Failed to create provider node" }, { status: 500 });
diff --git a/src/app/api/provider-nodes/validate/route.js b/src/app/api/provider-nodes/validate/route.js
index fdc12df..6d70a37 100644
--- a/src/app/api/provider-nodes/validate/route.js
+++ b/src/app/api/provider-nodes/validate/route.js
@@ -1,15 +1,39 @@
import { NextResponse } from "next/server";
-// POST /api/provider-nodes/validate - Validate API key against base URL /models
+// POST /api/provider-nodes/validate - Validate API key against base URL
export async function POST(request) {
try {
const body = await request.json();
- const { baseUrl, apiKey } = body;
+ const { baseUrl, apiKey, type } = body;
if (!baseUrl || !apiKey) {
return NextResponse.json({ error: "Base URL and API key required" }, { status: 400 });
}
+ // Anthropic Compatible Validation
+ if (type === "anthropic-compatible") {
+ // Robustly construct URL: remove trailing slash, and remove trailing /messages if user added it
+ let normalizedBase = baseUrl.trim().replace(/\/$/, "");
+ if (normalizedBase.endsWith("/messages")) {
+ normalizedBase = normalizedBase.slice(0, -9); // remove /messages
+ }
+
+ // Use /models endpoint for validation as many compatible providers support it (like OpenAI)
+ const modelsUrl = `${normalizedBase}/models`;
+
+ const res = await fetch(modelsUrl, {
+ method: "GET",
+ headers: {
+ "x-api-key": apiKey,
+ "anthropic-version": "2023-06-01",
+ "Authorization": `Bearer ${apiKey}` // Add Bearer token for hybrid proxies
+ }
+ });
+
+ return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
+ }
+
+ // OpenAI Compatible Validation (Default)
const modelsUrl = `${baseUrl.replace(/\/$/, "")}/models`;
const res = await fetch(modelsUrl, {
headers: { "Authorization": `Bearer ${apiKey}` },
@@ -17,7 +41,7 @@ export async function POST(request) {
return NextResponse.json({ valid: res.ok, error: res.ok ? null : "Invalid API key" });
} catch (error) {
- console.log("Error validating OpenAI compatible base URL:", error);
+ console.log("Error validating provider node:", error);
return NextResponse.json({ error: "Validation failed" }, { status: 500 });
}
}
diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js
index 7745b91..b1dff57 100644
--- a/src/app/api/providers/[id]/models/route.js
+++ b/src/app/api/providers/[id]/models/route.js
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById } from "@/models";
-import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
+import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
// Provider models endpoints configuration
const PROVIDER_MODELS_CONFIG = {
@@ -119,6 +119,47 @@ export async function GET(request, { params }) {
});
}
+ if (isAnthropicCompatibleProvider(connection.provider)) {
+ let baseUrl = connection.providerSpecificData?.baseUrl;
+ if (!baseUrl) {
+ return NextResponse.json({ error: "No base URL configured for Anthropic compatible provider" }, { status: 400 });
+ }
+
+ baseUrl = baseUrl.replace(/\/$/, "");
+ if (baseUrl.endsWith("/messages")) {
+ baseUrl = baseUrl.slice(0, -9);
+ }
+
+ const url = `${baseUrl}/models`;
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": connection.apiKey,
+ "anthropic-version": "2023-06-01",
+ "Authorization": `Bearer ${connection.apiKey}`
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.log(`Error fetching models from ${connection.provider}:`, errorText);
+ return NextResponse.json(
+ { error: `Failed to fetch models: ${response.status}` },
+ { status: response.status }
+ );
+ }
+
+ const data = await response.json();
+ const models = data.data || data.models || [];
+
+ return NextResponse.json({
+ provider: connection.provider,
+ connectionId: connection.id,
+ models
+ });
+ }
+
const config = PROVIDER_MODELS_CONFIG[connection.provider];
if (!config) {
return NextResponse.json(
diff --git a/src/app/api/providers/[id]/test/route.js b/src/app/api/providers/[id]/test/route.js
index 77abde0..21bc6de 100644
--- a/src/app/api/providers/[id]/test/route.js
+++ b/src/app/api/providers/[id]/test/route.js
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { getProviderConnectionById, updateProviderConnection, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
-import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
+import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import {
GEMINI_CONFIG,
ANTIGRAVITY_CONFIG,
@@ -322,6 +322,32 @@ async function testApiKeyConnection(connection) {
}
}
+ // Anthropic Compatible providers - test via /models endpoint
+ if (isAnthropicCompatibleProvider(connection.provider)) {
+ let modelsBase = connection.providerSpecificData?.baseUrl;
+ if (!modelsBase) {
+ return { valid: false, error: "Missing base URL" };
+ }
+ try {
+ modelsBase = modelsBase.replace(/\/$/, "");
+ if (modelsBase.endsWith("/messages")) {
+ modelsBase = modelsBase.slice(0, -9);
+ }
+
+ const modelsUrl = `${modelsBase}/models`;
+ const res = await fetch(modelsUrl, {
+ headers: {
+ "x-api-key": connection.apiKey,
+ "anthropic-version": "2023-06-01",
+ "Authorization": `Bearer ${connection.apiKey}`
+ },
+ });
+ return { valid: res.ok, error: res.ok ? null : "Invalid API key or base URL" };
+ } catch (err) {
+ return { valid: false, error: err.message };
+ }
+ }
+
try {
switch (connection.provider) {
case "openai": {
diff --git a/src/app/api/providers/route.js b/src/app/api/providers/route.js
index 8f621d2..bb95719 100644
--- a/src/app/api/providers/route.js
+++ b/src/app/api/providers/route.js
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
import { getProviderConnections, createProviderConnection, getProviderNodeById, isCloudEnabled } from "@/models";
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
-import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
+import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
@@ -33,7 +33,11 @@ export async function POST(request) {
const { provider, apiKey, name, priority, globalPriority, defaultModel, testStatus } = body;
// Validation
- if (!provider || (!APIKEY_PROVIDERS[provider] && !isOpenAICompatibleProvider(provider))) {
+ const isValidProvider = APIKEY_PROVIDERS[provider] ||
+ isOpenAICompatibleProvider(provider) ||
+ isAnthropicCompatibleProvider(provider);
+
+ if (!provider || !isValidProvider) {
return NextResponse.json({ error: "Invalid provider" }, { status: 400 });
}
if (!apiKey) {
@@ -62,6 +66,22 @@ export async function POST(request) {
baseUrl: node.baseUrl,
nodeName: node.name,
};
+ } else if (isAnthropicCompatibleProvider(provider)) {
+ const node = await getProviderNodeById(provider);
+ if (!node) {
+ return NextResponse.json({ error: "Anthropic Compatible node not found" }, { status: 404 });
+ }
+
+ const existingConnections = await getProviderConnections({ provider });
+ if (existingConnections.length > 0) {
+ return NextResponse.json({ error: "Only one connection is allowed for this Anthropic Compatible node" }, { status: 400 });
+ }
+
+ providerSpecificData = {
+ prefix: node.prefix,
+ baseUrl: node.baseUrl,
+ nodeName: node.name,
+ };
}
const newConnection = await createProviderConnection({
diff --git a/src/app/api/providers/validate/route.js b/src/app/api/providers/validate/route.js
index 008db0b..d6c94ac 100644
--- a/src/app/api/providers/validate/route.js
+++ b/src/app/api/providers/validate/route.js
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { getProviderNodeById } from "@/models";
-import { isOpenAICompatibleProvider } from "@/shared/constants/providers";
+import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
// POST /api/providers/validate - Validate API key with provider
export async function POST(request) {
@@ -33,6 +33,34 @@ export async function POST(request) {
});
}
+ if (isAnthropicCompatibleProvider(provider)) {
+ const node = await getProviderNodeById(provider);
+ if (!node) {
+ return NextResponse.json({ error: "Anthropic Compatible node not found" }, { status: 404 });
+ }
+
+ let normalizedBase = node.baseUrl?.trim().replace(/\/$/, "") || "";
+ if (normalizedBase.endsWith("/messages")) {
+ normalizedBase = normalizedBase.slice(0, -9); // remove /messages
+ }
+
+ const modelsUrl = `${normalizedBase}/models`;
+
+ const res = await fetch(modelsUrl, {
+ headers: {
+ "x-api-key": apiKey,
+ "anthropic-version": "2023-06-01",
+ "Authorization": `Bearer ${apiKey}`
+ },
+ });
+
+ isValid = res.ok;
+ return NextResponse.json({
+ valid: isValid,
+ error: isValid ? null : "Invalid API key",
+ });
+ }
+
switch (provider) {
case "openai":
const openaiRes = await fetch("https://api.openai.com/v1/models", {
diff --git a/src/shared/constants/providers.js b/src/shared/constants/providers.js
index 32247be..ebb0def 100644
--- a/src/shared/constants/providers.js
+++ b/src/shared/constants/providers.js
@@ -23,11 +23,16 @@ export const APIKEY_PROVIDERS = {
};
export const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
+export const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
export function isOpenAICompatibleProvider(providerId) {
return typeof providerId === "string" && providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
}
+export function isAnthropicCompatibleProvider(providerId) {
+ return typeof providerId === "string" && providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
+}
+
// All providers (combined)
export const AI_PROVIDERS = { ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS };
diff --git a/src/sse/services/model.js b/src/sse/services/model.js
index e93d906..4cec31d 100644
--- a/src/sse/services/model.js
+++ b/src/sse/services/model.js
@@ -20,10 +20,18 @@ export async function getModelInfo(modelStr) {
if (!parsed.isAlias) {
if (parsed.provider === parsed.providerAlias) {
- const providerNodes = await getProviderNodes({ type: "openai-compatible" });
- const matchedNode = providerNodes.find((node) => node.prefix === parsed.providerAlias);
- if (matchedNode) {
- return { provider: matchedNode.id, model: parsed.model };
+ // Check OpenAI Compatible nodes
+ const openaiNodes = await getProviderNodes({ type: "openai-compatible" });
+ const matchedOpenAI = openaiNodes.find((node) => node.prefix === parsed.providerAlias);
+ if (matchedOpenAI) {
+ return { provider: matchedOpenAI.id, model: parsed.model };
+ }
+
+ // Check Anthropic Compatible nodes
+ const anthropicNodes = await getProviderNodes({ type: "anthropic-compatible" });
+ const matchedAnthropic = anthropicNodes.find((node) => node.prefix === parsed.providerAlias);
+ if (matchedAnthropic) {
+ return { provider: matchedAnthropic.id, model: parsed.model };
}
}
return {