From 1bb621317d9d9c3fafefd00e5279663d93d622cf Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 28 Apr 2026 11:07:39 +0700 Subject: [PATCH] Add Cloudflare AI provider support and enhance connection management - Introduced Cloudflare AI as a new provider with specific configurations in providerModels.js and providers.js. - Updated DefaultExecutor to handle account ID resolution for Cloudflare AI connections. - Enhanced AddApiKeyModal and EditConnectionModal to include account ID input for Cloudflare AI. - Implemented validation for Cloudflare AI API key connections in testUtils.js and route.js. - Updated UI components to reflect changes in provider management and connection handling. --- open-sse/config/providerModels.js | 4 + open-sse/config/providers.js | 5 ++ open-sse/executors/default.js | 11 ++- open-sse/services/tokenRefresh.js | 2 +- .../dashboard/endpoint/EndpointPageClient.js | 25 +----- .../media-providers/[kind]/[id]/page.js | 51 +++++++---- .../providers/[id]/AddApiKeyModal.js | 21 ++++- .../dashboard/providers/[id]/page.js | 14 +-- src/app/api/providers/[id]/test/testUtils.js | 13 +++ src/app/api/providers/validate/route.js | 23 +++++ src/lib/localDb.js | 2 +- src/shared/components/EditConnectionModal.js | 25 +++++- src/shared/components/NoAuthProxyCard.js | 86 +++++++++++++++++++ src/shared/components/ProviderInfoCard.js | 62 +++++++++++++ src/shared/components/Sidebar.js | 2 +- src/shared/components/index.js | 2 + src/shared/constants/providers.js | 29 ++++--- src/sse/services/auth.js | 19 +++- 18 files changed, 325 insertions(+), 71 deletions(-) create mode 100644 src/shared/components/NoAuthProxyCard.js create mode 100644 src/shared/components/ProviderInfoCard.js diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index 37b7c70c..86043b44 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -344,6 +344,10 @@ export const PROVIDER_MODELS = { { id: "GLM-4.7", name: "GLM-4.7" }, { id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" }, ], + "cloudflare-ai": [ + { id: "@cf/moonshotai/kimi-k2.6", name: "Kimi K2.6" }, + { id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" }, + ], byteplus: [ { id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" }, { id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" }, diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index 3422a21e..dd0c53de 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -367,6 +367,11 @@ export const PROVIDERS = { format: "openai", headers: {} }, + // Cloudflare Workers AI - {accountId} resolved from credentials.providerSpecificData.accountId + "cloudflare-ai": { + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions", + format: "openai" + }, }; export const OLLAMA_LOCAL_DEFAULT_HOST = "http://localhost:11434"; diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index 35e7e384..c5fce149 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -32,8 +32,15 @@ export class DefaultExecutor extends BaseExecutor { return `${this.config.baseUrl}?beta=true`; case "gemini": return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; - default: - return this.config.baseUrl; + default: { + const url = this.config.baseUrl; + if (url?.includes("{accountId}")) { + const accountId = credentials?.providerSpecificData?.accountId; + if (!accountId) throw new Error(`${this.provider} requires accountId in providerSpecificData`); + return url.replace("{accountId}", accountId); + } + return url; + } } } diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index e76b1fc0..040b6e00 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -206,7 +206,7 @@ export async function refreshCodexToken(refreshToken, log) { grant_type: "refresh_token", refresh_token: refreshToken, client_id: PROVIDERS.codex.clientId, - scope: "openid profile email", + scope: "openid profile email offline_access", }), }); diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 027c147a..62342d23 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -25,7 +25,7 @@ export default function APIPageClient({ machineId }) { const [requireLogin, setRequireLogin] = useState(true); const [hasPassword, setHasPassword] = useState(true); const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false); - const [rtkEnabled, setRtkEnabledState] = useState(false); + const [rtkEnabled, setRtkEnabledState] = useState(true); // Cloudflare Tunnel state const [tunnelChecking, setTunnelChecking] = useState(true); @@ -81,7 +81,7 @@ export default function APIPageClient({ machineId }) { setRequireLogin(data.requireLogin !== false); setHasPassword(data.hasPassword || false); setTunnelDashboardAccess(data.tunnelDashboardAccess || false); - setRtkEnabledState(data.rtkEnabled || false); + setRtkEnabledState(data.rtkEnabled !== false); } if (statusRes.ok) { const data = await statusRes.json(); @@ -816,30 +816,13 @@ export default function APIPageClient({ machineId }) { {/* Token Saver (RTK) */}
-
-

Token Saver

- - Experimental - -
+

Token Saver

Compress tool output

- Auto-compress git diff / status / grep / find / ls / tree / logs in tool_result before sending to LLM. Check server console for [RTK] saved ... log. -

-

- Inspired by{" "} - - RTK (Rust Token Killer) - - {" "}— ported to JavaScript. This feature is still under testing; disable it if you notice unexpected results. + Auto-compress tool output (git diff/grep/ls/tree/logs) before sending to LLM to save tokens. Disable if you see issues.

{ @@ -1160,9 +1171,9 @@ function GenericExampleCard({ providerId, kind }) { )} - {/* Extra fields (filtered by model.params; if undefined → none shown) */} + {/* Extra fields — for kinds without model concept (webSearch/webFetch), show all; otherwise filter by model.params */} {(exConfig.extraFields || []) - .filter((f) => Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key)) + .filter((f) => kindModels.length === 0 || (Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key))) .map((f) => ( {f.type === "select" ? ( @@ -1175,6 +1186,14 @@ function GenericExampleCard({ providerId, kind }) { ))} + ) : f.type === "text" ? ( + setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))} + className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" + /> ) : ( -
-
- lock_open -
-
-

No authentication required

-

This provider is ready to use.

-
-
- + ) : ( )} - {/* Models - only for non-tts kinds; custom uses prefix as alias */} - {kind !== "tts" && ( + {/* Models - hidden for tts/webSearch/webFetch (provider IS the model); custom uses prefix as alias */} + {kind !== "tts" && kind !== "webSearch" && kind !== "webFetch" && ( )} + {/* Provider Info — config-driven, only for providers with searchConfig/fetchConfig */} + {!isCustom && (provider.searchConfig || provider.fetchConfig) && ( + + )} + {/* Example — per kind */} {kind === "embedding" && ( diff --git a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js index 91782acd..ebfcc712 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js @@ -14,6 +14,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa : ""; const isAzure = provider === "azure"; + const isCloudflareAi = provider === "cloudflare-ai"; const [formData, setFormData] = useState({ name: "", @@ -28,6 +29,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa deployment: "", organization: "", }); + const [cloudflareData, setCloudflareData] = useState({ accountId: "" }); const [validating, setValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [saving, setSaving] = useState(false); @@ -44,6 +46,9 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa organization: azureData.organization, }; } + if (isCloudflareAi) { + return { accountId: cloudflareData.accountId }; + } return undefined; }; @@ -180,6 +185,20 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa }

)} + {isCloudflareAi && ( +
+

Cloudflare Workers AI

+ setCloudflareData({ ...cloudflareData, accountId: e.target.value })} + placeholder="abc123def456..." + /> +

+ Find your Account ID in the right sidebar of dash.cloudflare.com +

+
+ )} {isAzure && (

Azure OpenAI Configuration

@@ -241,7 +260,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa

- diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 8c07456a..70fcddb9 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; -import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal } from "@/shared/components"; +import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; @@ -849,17 +849,7 @@ export default function ProviderDetailPage() { {/* Connections */} {isFreeNoAuth ? ( - -
-
- lock_open -
-
-

No authentication required

-

This provider is ready to use.

-
-
-
+ ) : (
diff --git a/src/app/api/providers/[id]/test/testUtils.js b/src/app/api/providers/[id]/test/testUtils.js index 7b647713..72b34a82 100644 --- a/src/app/api/providers/[id]/test/testUtils.js +++ b/src/app/api/providers/[id]/test/testUtils.js @@ -367,6 +367,19 @@ async function testApiKeyConnection(connection, effectiveProxy = null) { try { switch (connection.provider) { + case "cloudflare-ai": { + const psd = connection.providerSpecificData || {}; + const accountId = psd.accountId; + if (!accountId) return { valid: false, error: "Missing Account ID" }; + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`; + const res = await fetchWithConnectionProxy(url, { + method: "POST", + headers: { "Authorization": `Bearer ${connection.apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ model: getDefaultModel("cloudflare-ai"), messages: [{ role: "user", content: "test" }], max_tokens: 1 }), + }, effectiveProxy); + const valid = res.status !== 401 && res.status !== 403 && res.status !== 404; + return { valid, error: valid ? null : "Invalid API token or Account ID" }; + } case "azure": { const psd = connection.providerSpecificData || {}; const endpoint = (psd.azureEndpoint || "").replace(/\/$/, ""); diff --git a/src/app/api/providers/validate/route.js b/src/app/api/providers/validate/route.js index 0a73eda9..4d3a890d 100644 --- a/src/app/api/providers/validate/route.js +++ b/src/app/api/providers/validate/route.js @@ -95,6 +95,29 @@ export async function POST(request) { }); } + if (provider === "cloudflare-ai") { + const { providerSpecificData } = body; + const accountId = providerSpecificData?.accountId; + if (!accountId) { + return NextResponse.json({ valid: false, error: "Missing Account ID" }); + } + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`; + const cfRes = await fetch(url, { + method: "POST", + headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + model: getDefaultModel("cloudflare-ai"), + messages: [{ role: "user", content: "test" }], + max_tokens: 1, + }), + }); + isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404; + return NextResponse.json({ + valid: isValid, + error: isValid ? null : "Invalid API token or Account ID", + }); + } + if (provider === "azure") { const { providerSpecificData } = body; const endpoint = (providerSpecificData?.azureEndpoint || "").replace(/\/$/, ""); diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 7498a97d..7387ee3d 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -36,7 +36,7 @@ const DEFAULT_SETTINGS = { outboundProxyUrl: "", outboundNoProxy: "", mitmRouterBaseUrl: DEFAULT_MITM_ROUTER_BASE, - rtkEnabled: false, + rtkEnabled: true, }; function cloneDefaultData() { diff --git a/src/shared/components/EditConnectionModal.js b/src/shared/components/EditConnectionModal.js index 2f1b35a4..e8f870ab 100644 --- a/src/shared/components/EditConnectionModal.js +++ b/src/shared/components/EditConnectionModal.js @@ -20,6 +20,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on deployment: "", organization: "", }); + const [cloudflareData, setCloudflareData] = useState({ accountId: "" }); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [validating, setValidating] = useState(false); @@ -42,6 +43,9 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on organization: connection.providerSpecificData.organization || "", }); } + if (connection.provider === "cloudflare-ai" && connection.providerSpecificData) { + setCloudflareData({ accountId: connection.providerSpecificData.accountId || "" }); + } setTestResult(null); setValidationResult(null); } @@ -49,6 +53,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on const isOAuth = connection?.authType === "oauth"; const isAzure = connection?.provider === "azure"; + const isCloudflareAi = connection?.provider === "cloudflare-ai"; const isCompatible = connection ? (isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider)) : false; @@ -80,6 +85,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on provider: connection.provider, apiKey: formData.apiKey, ...(isAzure ? { providerSpecificData: azureData } : {}), + ...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}), }), }); const data = await res.json(); @@ -113,6 +119,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on provider: connection.provider, apiKey: formData.apiKey, ...(isAzure ? { providerSpecificData: azureData } : {}), + ...(isCloudflareAi ? { providerSpecificData: cloudflareData } : {}), }), }); const data = await res.json(); @@ -140,6 +147,9 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on organization: azureData.organization, }; } + if (isCloudflareAi) { + updates.providerSpecificData = { accountId: cloudflareData.accountId }; + } await onSave(updates); } finally { @@ -197,6 +207,19 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on )} + {isCloudflareAi && ( +
+

Cloudflare Workers AI

+ setCloudflareData({ ...cloudflareData, accountId: e.target.value })} + placeholder="abc123def456..." + hint="Find in right sidebar of dash.cloudflare.com" + /> +
+ )} + {isAzure && (

Azure OpenAI Configuration

@@ -233,7 +256,7 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on
)} - {!isCompatible && !isAzure && ( + {!isCompatible && !isAzure && !isCloudflareAi && (