From da5bdef4cbc03103b298b989871b8864e0330429 Mon Sep 17 00:00:00 2001 From: ramhaidar <49301219+ramhaidar@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:11:41 +0700 Subject: [PATCH 1/2] feat: Add Anthropic Compatible provider support - Added support for 'anthropic-compatible' provider nodes in backend. - Implemented isAnthropicCompatible logic in open-sse for /messages URL construction and headers. - Added UI for creating and managing Anthropic Compatible providers in the dashboard. - Updated validation logic for Anthropic-compatible endpoints. - Sanitize base URL input (strip trailing /messages) to prevent 404s and improve UX. - Improve validation: use GET /models (2xx success), and support x-api-key / Authorization Bearer hybrid proxies. - Enable model import via /models for Anthropic Compatible providers. - Ensure Authorization is omitted when x-api-key is present to avoid strict proxy conflicts. - Resolve Anthropic-compatible credentials by prefix during model resolution (e.g., acx/model). - Update default executor to match provider header/url behavior for Anthropic-compatible providers. --- open-sse/executors/default.js | 18 +- open-sse/services/provider.js | 173 ++++++++++------ .../dashboard/providers/[id]/page.js | 130 +++++++----- .../(dashboard)/dashboard/providers/page.js | 187 +++++++++++++++++- src/app/api/provider-nodes/[id]/route.js | 30 ++- src/app/api/provider-nodes/route.js | 54 +++-- src/app/api/provider-nodes/validate/route.js | 30 ++- src/app/api/providers/[id]/models/route.js | 43 +++- src/app/api/providers/[id]/test/route.js | 28 ++- src/app/api/providers/route.js | 24 ++- src/app/api/providers/validate/route.js | 30 ++- src/shared/constants/providers.js | 5 + src/sse/services/model.js | 16 +- 13 files changed, 614 insertions(+), 154 deletions(-) diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 7409f61..f9d0a83 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -13,6 +13,11 @@ export class DefaultExecutor extends BaseExecutor { const path = this.provider.includes("responses") ? "/responses" : "/chat/completions"; return `${normalized}${path}`; } + if (this.provider?.startsWith?.("anthropic-compatible-")) { + const baseUrl = credentials?.providerSpecificData?.baseUrl || "https://api.anthropic.com/v1"; + const normalized = baseUrl.replace(/\/$/, ""); + return `${normalized}/messages`; + } switch (this.provider) { case "claude": case "glm": @@ -42,7 +47,18 @@ export class DefaultExecutor extends BaseExecutor { headers["x-api-key"] = credentials.apiKey; break; default: - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; + if (this.provider?.startsWith?.("anthropic-compatible-")) { + if (credentials.apiKey) { + headers["x-api-key"] = credentials.apiKey; + } else if (credentials.accessToken) { + headers["Authorization"] = `Bearer ${credentials.accessToken}`; + } + if (!headers["anthropic-version"]) { + headers["anthropic-version"] = "2023-06-01"; + } + } else { + headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; + } } if (stream) headers["Accept"] = "text/event-stream"; diff --git a/open-sse/services/provider.js b/open-sse/services/provider.js index 828537a..f6e527b 100644 --- a/open-sse/services/provider.js +++ b/open-sse/services/provider.js @@ -5,10 +5,19 @@ const OPENAI_COMPATIBLE_DEFAULTS = { baseUrl: "https://api.openai.com/v1", }; +const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-"; +const ANTHROPIC_COMPATIBLE_DEFAULTS = { + baseUrl: "https://api.anthropic.com/v1", +}; + function isOpenAICompatible(provider) { return typeof provider === "string" && provider.startsWith(OPENAI_COMPATIBLE_PREFIX); } +function isAnthropicCompatible(provider) { + return typeof provider === "string" && provider.startsWith(ANTHROPIC_COMPATIBLE_PREFIX); +} + function getOpenAICompatibleType(provider) { if (!isOpenAICompatible(provider)) return "chat"; return provider.includes("responses") ? "responses" : "chat"; @@ -20,6 +29,11 @@ function buildOpenAICompatibleUrl(baseUrl, apiType) { return `${normalized}${path}`; } +function buildAnthropicCompatibleUrl(baseUrl) { + const normalized = baseUrl.replace(/\/$/, ""); + return `${normalized}/messages`; +} + // Detect request format from body structure export function detectFormat(body) { // OpenAI Responses API: has input[] array instead of messages[] @@ -104,6 +118,13 @@ export function getProviderConfig(provider) { baseUrl: OPENAI_COMPATIBLE_DEFAULTS.baseUrl, }; } + if (isAnthropicCompatible(provider)) { + return { + ...PROVIDERS.anthropic, // Use Anthropic defaults (header: x-api-key) + format: "claude", + baseUrl: ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl, + }; + } return PROVIDERS[provider] || PROVIDERS.openai; } @@ -120,6 +141,10 @@ export function buildProviderUrl(provider, model, stream = true, options = {}) { const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl; return buildOpenAICompatibleUrl(baseUrl, apiType); } + if (isAnthropicCompatible(provider)) { + const baseUrl = options?.baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl; + return buildAnthropicCompatibleUrl(baseUrl); + } const config = getProviderConfig(provider); switch (provider) { @@ -170,72 +195,87 @@ export function buildProviderHeaders(provider, credentials, stream = true, body }; // Add auth header - switch (provider) { - case "gemini": - if (credentials.apiKey) { - headers["x-goog-api-key"] = credentials.apiKey; - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - break; - - case "antigravity": - case "gemini-cli": - // Antigravity and Gemini CLI use OAuth access token - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - break; - - case "claude": - // Claude uses x-api-key header for API key, or Authorization for OAuth - if (credentials.apiKey) { - headers["x-api-key"] = credentials.apiKey; - } else if (credentials.accessToken) { - headers["Authorization"] = `Bearer ${credentials.accessToken}`; - } - break; - - case "github": - // GitHub Copilot requires special headers to mimic VSCode - // Prioritize copilotToken from providerSpecificData, fallback to accessToken - const githubToken = credentials.copilotToken || credentials.accessToken; - // Add headers in exact same order as test endpoint - headers["Authorization"] = `Bearer ${githubToken}`; - headers["Content-Type"] = "application/json"; - headers["copilot-integration-id"] = "vscode-chat"; - headers["editor-version"] = "vscode/1.107.1"; - headers["editor-plugin-version"] = "copilot-chat/0.26.7"; - headers["user-agent"] = "GitHubCopilotChat/0.26.7"; - headers["openai-intent"] = "conversation-panel"; - headers["x-github-api-version"] = "2025-04-01"; - // Generate a UUID for x-request-id (Cloudflare Workers compatible) - headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() : - 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - headers["x-vscode-user-agent-library-version"] = "electron-fetch"; - headers["X-Initiator"] = "user"; - headers["Accept"] = "application/json"; - break; - - case "codex": - case "qwen": - case "openai": - case "openrouter": - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - break; - - case "glm": - case "kimi": - case "minimax": - // Claude-compatible API providers use x-api-key + // Specific override for Anthropic Compatible + if (isAnthropicCompatible(provider)) { + if (credentials.apiKey) { headers["x-api-key"] = credentials.apiKey; - break; - - default: - headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; - break; + // Do NOT send Authorization header when apiKey is present for Anthropic Compatible + // as it causes issues with some providers (e.g. opencode.ai) + } else if (credentials.accessToken) { + headers["Authorization"] = `Bearer ${credentials.accessToken}`; + } + // Add default Anthropic version if not present (some proxies require it) + if (!headers["anthropic-version"]) { + headers["anthropic-version"] = "2023-06-01"; + } + } else { + switch (provider) { + case "gemini": + if (credentials.apiKey) { + headers["x-goog-api-key"] = credentials.apiKey; + } else if (credentials.accessToken) { + headers["Authorization"] = `Bearer ${credentials.accessToken}`; + } + break; + + case "antigravity": + case "gemini-cli": + // Antigravity and Gemini CLI use OAuth access token + headers["Authorization"] = `Bearer ${credentials.accessToken}`; + break; + + case "claude": + // Claude uses x-api-key header for API key, or Authorization for OAuth + if (credentials.apiKey) { + headers["x-api-key"] = credentials.apiKey; + } else if (credentials.accessToken) { + headers["Authorization"] = `Bearer ${credentials.accessToken}`; + } + break; + + case "github": + // GitHub Copilot requires special headers to mimic VSCode + // Prioritize copilotToken from providerSpecificData, fallback to accessToken + const githubToken = credentials.copilotToken || credentials.accessToken; + // Add headers in exact same order as test endpoint + headers["Authorization"] = `Bearer ${githubToken}`; + headers["Content-Type"] = "application/json"; + headers["copilot-integration-id"] = "vscode-chat"; + headers["editor-version"] = "vscode/1.107.1"; + headers["editor-plugin-version"] = "copilot-chat/0.26.7"; + headers["user-agent"] = "GitHubCopilotChat/0.26.7"; + headers["openai-intent"] = "conversation-panel"; + headers["x-github-api-version"] = "2025-04-01"; + // Generate a UUID for x-request-id (Cloudflare Workers compatible) + headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() : + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + headers["x-vscode-user-agent-library-version"] = "electron-fetch"; + headers["X-Initiator"] = "user"; + headers["Accept"] = "application/json"; + break; + + case "codex": + case "qwen": + case "openai": + case "openrouter": + headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; + break; + + case "glm": + case "kimi": + case "minimax": + // Claude-compatible API providers use x-api-key + headers["x-api-key"] = credentials.apiKey; + break; + + default: + headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; + break; + } } // Stream accept header @@ -251,6 +291,9 @@ export function getTargetFormat(provider) { if (isOpenAICompatible(provider)) { return getOpenAICompatibleType(provider) === "responses" ? "openai-responses" : "openai"; } + if (isAnthropicCompatible(provider)) { + return "claude"; + } const config = getProviderConfig(provider); return config.format || "openai"; } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 2af51c1..d508a6b 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -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 ( - ); } @@ -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.png"; + } return `/providers/${providerInfo.id}.png`; }; @@ -361,14 +370,14 @@ export default function ProviderDetailPage() { - {isOpenAICompatible && providerNode && ( + {isCompatible && providerNode && (
-

OpenAI Compatible Details

+

{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}

- {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")}

@@ -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() {
{connections.length > 0 && (

- 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.

)} @@ -420,7 +429,7 @@ export default function ProviderDetailPage() {

Connections

- {!isOpenAICompatible && ( + {!isCompatible && ( @@ -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)} /> - setShowEditNodeModal(false)} - /> + {isCompatible && ( + setShowEditNodeModal(false)} + isAnthropic={isAnthropicCompatible} + /> + )}
); } @@ -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 (

- 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 })} + /> + )} 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

- +
+ + +
{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.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" + /> +
+ +
+
+ {validationResult && ( + + {validationResult === "success" ? "Valid" : "Invalid"} + + )} +
+ + +
+
+
+ ); +} + +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 { From 8ceb8f24c3e7bda9c85577a3cd39251fb50d755b Mon Sep 17 00:00:00 2001 From: ramhaidar <49301219+ramhaidar@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:41:10 +0700 Subject: [PATCH 2/2] chore(providers): use Anthropic Compatible logo - Update Anthropic Compatible provider configuration to use the new m-variant logo (anthropic-m.png) instead of the standard anthropic.png for consistent branding across the provider icons. - Target: provider icons --- public/providers/anthropic-m.png | Bin 0 -> 3707 bytes .../(dashboard)/dashboard/providers/[id]/page.js | 2 +- src/app/(dashboard)/dashboard/providers/page.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 public/providers/anthropic-m.png diff --git a/public/providers/anthropic-m.png b/public/providers/anthropic-m.png new file mode 100644 index 0000000000000000000000000000000000000000..59807db5295d4356a25509b9e08bdab8e8e3bfcd GIT binary patch literal 3707 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 SRk}2gK;waqYMHWi$o~N<<-eE! literal 0 HcmV?d00001 diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index d508a6b..4bf067e 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -324,7 +324,7 @@ export default function ProviderDetailPage() { return providerInfo.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png"; } if (isAnthropicCompatible) { - return "/providers/anthropic.png"; + return "/providers/anthropic-m.png"; } return `/providers/${providerInfo.id}.png`; }; diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index a760757..bdd9838 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -279,7 +279,7 @@ function ApiKeyProviderCard({ providerId, provider, stats }) { return provider.apiType === "responses" ? "/providers/oai-r.png" : "/providers/oai-cc.png"; } if (isAnthropicCompatible) { - return "/providers/anthropic.png"; // Use Anthropic icon as base + return "/providers/anthropic-m.png"; // Use Anthropic icon as base } return `/providers/${provider.id}.png`; };