Skip to content

Commit 1bb6213

Browse files
author
decolua
committed
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.
1 parent 111e789 commit 1bb6213

18 files changed

Lines changed: 325 additions & 71 deletions

File tree

open-sse/config/providerModels.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,10 @@ export const PROVIDER_MODELS = {
344344
{ id: "GLM-4.7", name: "GLM-4.7" },
345345
{ id: "DeepSeek-V3.2", name: "DeepSeek-V3.2" },
346346
],
347+
"cloudflare-ai": [
348+
{ id: "@cf/moonshotai/kimi-k2.6", name: "Kimi K2.6" },
349+
{ id: "@cf/zai-org/glm-4.7-flash", name: "GLM 4.7 Flash" },
350+
],
347351
byteplus: [
348352
{ id: "seed-2-0-pro-260328", name: "Seed 2.0 Pro" },
349353
{ id: "seed-2-0-code-preview-260328", name: "Seed 2.0 Code Preview" },

open-sse/config/providers.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@ export const PROVIDERS = {
367367
format: "openai",
368368
headers: {}
369369
},
370+
// Cloudflare Workers AI - {accountId} resolved from credentials.providerSpecificData.accountId
371+
"cloudflare-ai": {
372+
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1/chat/completions",
373+
format: "openai"
374+
},
370375
};
371376

372377
export const OLLAMA_LOCAL_DEFAULT_HOST = "http://localhost:11434";

open-sse/executors/default.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,15 @@ export class DefaultExecutor extends BaseExecutor {
3232
return `${this.config.baseUrl}?beta=true`;
3333
case "gemini":
3434
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
35-
default:
36-
return this.config.baseUrl;
35+
default: {
36+
const url = this.config.baseUrl;
37+
if (url?.includes("{accountId}")) {
38+
const accountId = credentials?.providerSpecificData?.accountId;
39+
if (!accountId) throw new Error(`${this.provider} requires accountId in providerSpecificData`);
40+
return url.replace("{accountId}", accountId);
41+
}
42+
return url;
43+
}
3744
}
3845
}
3946

open-sse/services/tokenRefresh.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export async function refreshCodexToken(refreshToken, log) {
206206
grant_type: "refresh_token",
207207
refresh_token: refreshToken,
208208
client_id: PROVIDERS.codex.clientId,
209-
scope: "openid profile email",
209+
scope: "openid profile email offline_access",
210210
}),
211211
});
212212

src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function APIPageClient({ machineId }) {
2525
const [requireLogin, setRequireLogin] = useState(true);
2626
const [hasPassword, setHasPassword] = useState(true);
2727
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
28-
const [rtkEnabled, setRtkEnabledState] = useState(false);
28+
const [rtkEnabled, setRtkEnabledState] = useState(true);
2929

3030
// Cloudflare Tunnel state
3131
const [tunnelChecking, setTunnelChecking] = useState(true);
@@ -81,7 +81,7 @@ export default function APIPageClient({ machineId }) {
8181
setRequireLogin(data.requireLogin !== false);
8282
setHasPassword(data.hasPassword || false);
8383
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
84-
setRtkEnabledState(data.rtkEnabled || false);
84+
setRtkEnabledState(data.rtkEnabled !== false);
8585
}
8686
if (statusRes.ok) {
8787
const data = await statusRes.json();
@@ -816,30 +816,13 @@ export default function APIPageClient({ machineId }) {
816816
{/* Token Saver (RTK) */}
817817
<Card id="rtk">
818818
<div className="flex items-center justify-between mb-2">
819-
<div className="flex items-center gap-2">
820-
<h2 className="text-lg font-semibold">Token Saver</h2>
821-
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
822-
Experimental
823-
</span>
824-
</div>
819+
<h2 className="text-lg font-semibold">Token Saver</h2>
825820
</div>
826821
<div className="flex items-center justify-between pt-2">
827822
<div className="pr-4">
828823
<p className="font-medium">Compress tool output</p>
829824
<p className="text-sm text-text-muted">
830-
Auto-compress git diff / status / grep / find / ls / tree / logs in <code>tool_result</code> before sending to LLM. Check server console for <code>[RTK] saved ...</code> log.
831-
</p>
832-
<p className="text-xs text-text-muted mt-1">
833-
Inspired by{" "}
834-
<a
835-
href="https://github.com/rtk-ai/rtk"
836-
target="_blank"
837-
rel="noopener noreferrer"
838-
className="underline hover:text-primary"
839-
>
840-
RTK (Rust Token Killer)
841-
</a>
842-
{" "}— ported to JavaScript. This feature is still under testing; disable it if you notice unexpected results.
825+
Auto-compress tool output (git diff/grep/ls/tree/logs) before sending to LLM to save tokens. Disable if you see issues.
843826
</p>
844827
</div>
845828
<Toggle

src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useParams, notFound, useRouter } from "next/navigation";
44
import Link from "next/link";
55
import { useState, useEffect } from "react";
6-
import { Card, Badge, Button, AddCustomEmbeddingModal } from "@/shared/components";
6+
import { Card, Badge, Button, AddCustomEmbeddingModal, NoAuthProxyCard, ProviderInfoCard } from "@/shared/components";
77
import ProviderIcon from "@/shared/components/ProviderIcon";
88
import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProviderAlias, isCustomEmbeddingProvider } from "@/shared/constants/providers";
99
import { getModelsByProviderId } from "@/shared/constants/models";
@@ -49,13 +49,23 @@ const KIND_EXAMPLE_CONFIG = {
4949
defaultInput: "What is the latest news about AI?",
5050
bodyKey: "query",
5151
defaultResponse: `{\n "results": [\n { "title": "...", "url": "...", "snippet": "..." }\n ]\n}`,
52+
extraFields: [
53+
{ key: "search_type", label: "Type", type: "select", default: "web", options: ["web", "news"] },
54+
{ key: "max_results", label: "Max results", type: "number", default: 5, min: 1, max: 100 },
55+
{ key: "country", label: "Country", type: "text", default: "" },
56+
{ key: "language", label: "Language", type: "text", default: "" },
57+
],
5258
},
5359
webFetch: {
5460
inputLabel: "URL",
5561
inputPlaceholder: "https://example.com",
5662
defaultInput: "https://example.com",
5763
bodyKey: "url",
5864
defaultResponse: `{\n "content": "...",\n "title": "...",\n "url": "..."\n}`,
65+
extraFields: [
66+
{ key: "format", label: "Format", type: "select", default: "markdown", options: ["markdown", "text", "html"] },
67+
{ key: "max_characters", label: "Max chars", type: "number", default: 0, min: 0 },
68+
],
5969
},
6070
image: {
6171
inputLabel: "Prompt",
@@ -916,7 +926,8 @@ function GenericExampleCard({ providerId, kind }) {
916926

917927
const endpoint = useTunnel ? tunnelEndpoint : localEndpoint;
918928
const apiPath = kindConfig.endpoint.path;
919-
const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : "";
929+
// For kinds without model concept (webSearch/webFetch), use providerAlias directly
930+
const modelFull = kindModels.length === 0 ? providerAlias : (selectedModel ? `${providerAlias}/${selectedModel}` : "");
920931

921932
// Build request body with optional extra fields (only non-empty values)
922933
const extraBodyFromFields = Object.entries(extraValues).reduce((acc, [k, v]) => {
@@ -1160,9 +1171,9 @@ function GenericExampleCard({ providerId, kind }) {
11601171
</Row>
11611172
)}
11621173

1163-
{/* Extra fields (filtered by model.params; if undefined → none shown) */}
1174+
{/* Extra fields — for kinds without model concept (webSearch/webFetch), show all; otherwise filter by model.params */}
11641175
{(exConfig.extraFields || [])
1165-
.filter((f) => Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key))
1176+
.filter((f) => kindModels.length === 0 || (Array.isArray(selectedModelObj?.params) && selectedModelObj.params.includes(f.key)))
11661177
.map((f) => (
11671178
<Row key={f.key} label={f.label}>
11681179
{f.type === "select" ? (
@@ -1175,6 +1186,14 @@ function GenericExampleCard({ providerId, kind }) {
11751186
<option key={opt} value={opt}>{opt === "" ? "(default)" : opt}</option>
11761187
))}
11771188
</select>
1189+
) : f.type === "text" ? (
1190+
<input
1191+
type="text"
1192+
value={extraValues[f.key] ?? ""}
1193+
placeholder={f.placeholder}
1194+
onChange={(e) => setExtraValues((s) => ({ ...s, [f.key]: e.target.value }))}
1195+
className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
1196+
/>
11781197
) : (
11791198
<input
11801199
type="number"
@@ -1413,30 +1432,28 @@ export default function MediaProviderDetailPage() {
14131432

14141433
{/* Connections */}
14151434
{!isCustom && provider.noAuth ? (
1416-
<Card>
1417-
<div className="flex items-center gap-3">
1418-
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-green-500/10 text-green-500">
1419-
<span className="material-symbols-outlined text-[20px]">lock_open</span>
1420-
</div>
1421-
<div>
1422-
<p className="text-sm font-medium">No authentication required</p>
1423-
<p className="text-xs text-text-muted">This provider is ready to use.</p>
1424-
</div>
1425-
</div>
1426-
</Card>
1435+
<NoAuthProxyCard providerId={id} />
14271436
) : (
14281437
<ConnectionsCard providerId={id} isOAuth={false} />
14291438
)}
14301439

1431-
{/* Models - only for non-tts kinds; custom uses prefix as alias */}
1432-
{kind !== "tts" && (
1440+
{/* Models - hidden for tts/webSearch/webFetch (provider IS the model); custom uses prefix as alias */}
1441+
{kind !== "tts" && kind !== "webSearch" && kind !== "webFetch" && (
14331442
<ModelsCard
14341443
providerId={id}
14351444
kindFilter={kind}
14361445
providerAliasOverride={isCustom ? customNode?.prefix : undefined}
14371446
/>
14381447
)}
14391448

1449+
{/* Provider Info — config-driven, only for providers with searchConfig/fetchConfig */}
1450+
{!isCustom && (provider.searchConfig || provider.fetchConfig) && (
1451+
<ProviderInfoCard
1452+
config={kind === "webFetch" ? provider.fetchConfig : provider.searchConfig}
1453+
title={`${kindConfig.label} Config`}
1454+
/>
1455+
)}
1456+
14401457
{/* Example — per kind */}
14411458
{kind === "embedding" && (
14421459
<EmbeddingExampleCard providerId={id} customAlias={customNode?.prefix} />

src/app/(dashboard)/dashboard/providers/[id]/AddApiKeyModal.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
1414
: "";
1515

1616
const isAzure = provider === "azure";
17+
const isCloudflareAi = provider === "cloudflare-ai";
1718

1819
const [formData, setFormData] = useState({
1920
name: "",
@@ -28,6 +29,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
2829
deployment: "",
2930
organization: "",
3031
});
32+
const [cloudflareData, setCloudflareData] = useState({ accountId: "" });
3133
const [validating, setValidating] = useState(false);
3234
const [validationResult, setValidationResult] = useState(null);
3335
const [saving, setSaving] = useState(false);
@@ -44,6 +46,9 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
4446
organization: azureData.organization,
4547
};
4648
}
49+
if (isCloudflareAi) {
50+
return { accountId: cloudflareData.accountId };
51+
}
4752
return undefined;
4853
};
4954

@@ -180,6 +185,20 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
180185
}
181186
</p>
182187
)}
188+
{isCloudflareAi && (
189+
<div className="bg-sidebar/50 p-4 rounded-lg border border-accent/20">
190+
<h3 className="font-semibold mb-3 text-sm">Cloudflare Workers AI</h3>
191+
<Input
192+
label="Account ID"
193+
value={cloudflareData.accountId}
194+
onChange={(e) => setCloudflareData({ ...cloudflareData, accountId: e.target.value })}
195+
placeholder="abc123def456..."
196+
/>
197+
<p className="text-xs text-text-muted mt-2">
198+
Find your Account ID in the right sidebar of <a href="https://dash.cloudflare.com" target="_blank" rel="noopener noreferrer" className="text-primary underline">dash.cloudflare.com</a>
199+
</p>
200+
</div>
201+
)}
183202
{isAzure && (
184203
<div className="bg-sidebar/50 p-4 rounded-lg border border-accent/20">
185204
<h3 className="font-semibold mb-3 text-sm">Azure OpenAI Configuration</h3>
@@ -241,7 +260,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
241260
</p>
242261

243262
<div className="flex gap-2">
244-
<Button onClick={handleSubmit} fullWidth disabled={saving || (!isOllamaLocal && (!formData.name || !formData.apiKey)) || (isAzure && (!azureData.azureEndpoint || !azureData.deployment || !azureData.organization))}>
263+
<Button onClick={handleSubmit} fullWidth disabled={saving || (!isOllamaLocal && (!formData.name || !formData.apiKey)) || (isAzure && (!azureData.azureEndpoint || !azureData.deployment || !azureData.organization)) || (isCloudflareAi && !cloudflareData.accountId)}>
245264

246265
{saving ? "Saving..." : "Save"}
247266
</Button>

src/app/(dashboard)/dashboard/providers/[id]/page.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from "react";
44
import { useParams, useRouter } from "next/navigation";
55
import Link from "next/link";
66
import Image from "next/image";
7-
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal } from "@/shared/components";
7+
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, CursorAuthModal, IFlowCookieModal, GitLabAuthModal, Toggle, Select, EditConnectionModal, NoAuthProxyCard } from "@/shared/components";
88
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, getProviderAlias, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, AI_PROVIDERS, THINKING_CONFIG } from "@/shared/constants/providers";
99
import { getModelsByProviderId } from "@/shared/constants/models";
1010
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
@@ -849,17 +849,7 @@ export default function ProviderDetailPage() {
849849

850850
{/* Connections */}
851851
{isFreeNoAuth ? (
852-
<Card>
853-
<div className="flex items-center gap-3">
854-
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-green-500/10 text-green-500">
855-
<span className="material-symbols-outlined text-[20px]">lock_open</span>
856-
</div>
857-
<div>
858-
<p className="text-sm font-medium">No authentication required</p>
859-
<p className="text-xs text-text-muted">This provider is ready to use.</p>
860-
</div>
861-
</div>
862-
</Card>
852+
<NoAuthProxyCard providerId={providerId} />
863853
) : (
864854
<Card>
865855
<div className="flex items-center justify-between mb-4">

src/app/api/providers/[id]/test/testUtils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,19 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
367367

368368
try {
369369
switch (connection.provider) {
370+
case "cloudflare-ai": {
371+
const psd = connection.providerSpecificData || {};
372+
const accountId = psd.accountId;
373+
if (!accountId) return { valid: false, error: "Missing Account ID" };
374+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
375+
const res = await fetchWithConnectionProxy(url, {
376+
method: "POST",
377+
headers: { "Authorization": `Bearer ${connection.apiKey}`, "Content-Type": "application/json" },
378+
body: JSON.stringify({ model: getDefaultModel("cloudflare-ai"), messages: [{ role: "user", content: "test" }], max_tokens: 1 }),
379+
}, effectiveProxy);
380+
const valid = res.status !== 401 && res.status !== 403 && res.status !== 404;
381+
return { valid, error: valid ? null : "Invalid API token or Account ID" };
382+
}
370383
case "azure": {
371384
const psd = connection.providerSpecificData || {};
372385
const endpoint = (psd.azureEndpoint || "").replace(/\/$/, "");

src/app/api/providers/validate/route.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,29 @@ export async function POST(request) {
9595
});
9696
}
9797

98+
if (provider === "cloudflare-ai") {
99+
const { providerSpecificData } = body;
100+
const accountId = providerSpecificData?.accountId;
101+
if (!accountId) {
102+
return NextResponse.json({ valid: false, error: "Missing Account ID" });
103+
}
104+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
105+
const cfRes = await fetch(url, {
106+
method: "POST",
107+
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
108+
body: JSON.stringify({
109+
model: getDefaultModel("cloudflare-ai"),
110+
messages: [{ role: "user", content: "test" }],
111+
max_tokens: 1,
112+
}),
113+
});
114+
isValid = cfRes.status !== 401 && cfRes.status !== 403 && cfRes.status !== 404;
115+
return NextResponse.json({
116+
valid: isValid,
117+
error: isValid ? null : "Invalid API token or Account ID",
118+
});
119+
}
120+
98121
if (provider === "azure") {
99122
const { providerSpecificData } = body;
100123
const endpoint = (providerSpecificData?.azureEndpoint || "").replace(/\/$/, "");

0 commit comments

Comments
 (0)