diff --git a/src/app/(dashboard)/dashboard/proxy-pools/page.js b/src/app/(dashboard)/dashboard/proxy-pools/page.js
index cae61044..6c6fc4b1 100644
--- a/src/app/(dashboard)/dashboard/proxy-pools/page.js
+++ b/src/app/(dashboard)/dashboard/proxy-pools/page.js
@@ -33,10 +33,12 @@ export default function ProxyPoolsPage() {
const [showFormModal, setShowFormModal] = useState(false);
const [showBatchImportModal, setShowBatchImportModal] = useState(false);
const [showVercelModal, setShowVercelModal] = useState(false);
+ const [showCloudflareModal, setShowCloudflareModal] = useState(false);
const [editingProxyPool, setEditingProxyPool] = useState(null);
const [formData, setFormData] = useState(normalizeFormData());
const [batchImportText, setBatchImportText] = useState("");
const [vercelForm, setVercelForm] = useState({ vercelToken: "", projectName: "vercel-relay" });
+ const [cloudflareForm, setCloudflareForm] = useState({ accountId: "", apiToken: "", projectName: "cloudflare-relay" });
const [saving, setSaving] = useState(false);
const [importing, setImporting] = useState(false);
const [deploying, setDeploying] = useState(false);
@@ -334,6 +336,16 @@ export default function ProxyPoolsPage() {
setShowVercelModal(false);
};
+ const openCloudflareModal = () => {
+ setCloudflareForm({ accountId: "", apiToken: "", projectName: "cloudflare-relay" });
+ setShowCloudflareModal(true);
+ };
+
+ const closeCloudflareModal = () => {
+ if (deploying) return;
+ setShowCloudflareModal(false);
+ };
+
const handleVercelDeploy = async () => {
if (!vercelForm.vercelToken.trim()) return;
setDeploying(true);
@@ -359,6 +371,31 @@ export default function ProxyPoolsPage() {
}
};
+ const handleCloudflareDeploy = async () => {
+ if (!cloudflareForm.accountId.trim() || !cloudflareForm.apiToken.trim()) return;
+ setDeploying(true);
+ try {
+ const res = await fetch("/api/proxy-pools/cloudflare-deploy", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(cloudflareForm),
+ });
+ const data = await res.json();
+ if (res.ok) {
+ await fetchProxyPools();
+ closeCloudflareModal();
+ notify.success(`Deployed: ${data.deployUrl}`);
+ } else {
+ notify.error(data.error || "Deploy failed");
+ }
+ } catch (error) {
+ console.log("Error deploying Cloudflare relay:", error);
+ notify.error("Deploy failed");
+ } finally {
+ setDeploying(false);
+ }
+ };
+
const parseProxyLine = (line) => {
const trimmed = line.trim();
if (!trimmed) return null;
@@ -495,6 +532,9 @@ export default function ProxyPoolsPage() {
+
@@ -588,6 +628,9 @@ export default function ProxyPoolsPage() {
{pool.type === "vercel" && (
vercel relay
)}
+ {pool.type === "cloudflare" && (
+ cloudflare relay
+ )}
{pool.boundConnectionCount || 0} bound
@@ -722,6 +765,70 @@ export default function ProxyPoolsPage() {
+
+
+
+
What is Cloudflare Relay?
+
+ Deploys a Cloudflare Worker as a proxy relay. All AI provider requests will be forwarded through Cloudflare's global edge network.
+
+
+ - High performance global routing and IP masking via Cloudflare Workers
+ - Free tier: 100,000 requests per day
+ - Requires Cloudflare Account ID and a Workers API Token (Edit Workers permission)
+
+
+
How to generate your API Token:
+
+ - Go to My Profile → API Tokens → Create Token
+ - Scroll down to Custom Token and click Get started
+ - Under Permissions: Account | Workers Scripts | Edit
+ - Under Account Resources: Include | Account | Your Account Name
+ - Click Continue to summary → Create Token
+
+
+
+
setCloudflareForm((prev) => ({ ...prev, accountId: e.target.value }))}
+ placeholder="your-cloudflare-account-id"
+ hint={<>Found on the right side of the Cloudflare dashboard overview page.>}
+ />
+
setCloudflareForm((prev) => ({ ...prev, apiToken: e.target.value }))}
+ placeholder="your-cloudflare-api-token"
+ hint={<>Requires "Workers Scripts: Edit" permission.
Get token →>}
+ type="password"
+ />
+
setCloudflareForm((prev) => ({ ...prev, projectName: e.target.value }))}
+ placeholder="my-relay"
+ hint="Unique name for your Cloudflare Worker. Leave empty for auto-generated name."
+ />
+
+
+
+
+
+
+
({}));
+ console.error("Cloudflare upload error:", err);
+ return NextResponse.json(
+ { error: err.errors?.[0]?.message || "Failed to upload Worker to Cloudflare" },
+ { status: uploadRes.status }
+ );
+ }
+
+ // 2. Enable workers.dev subdomain for the script
+ const enableSubdomainRes = await fetch(`${workerScriptUrl}/subdomain`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ enabled: true }),
+ });
+
+ if (!enableSubdomainRes.ok) {
+ const err = await enableSubdomainRes.json().catch(() => ({}));
+ console.error("Cloudflare subdomain enable error:", err);
+ // We don't fail completely here, just continue
+ }
+
+ // 3. Get the workers.dev subdomain for the account to construct the final URL
+ let deployUrl = "";
+ const subdomainRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/subdomain`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (subdomainRes.ok) {
+ const subdomainData = await subdomainRes.json();
+ if (subdomainData.result && subdomainData.result.subdomain) {
+ deployUrl = `https://${projectName}.${subdomainData.result.subdomain}.workers.dev`;
+ }
+ }
+
+ if (!deployUrl) {
+ return NextResponse.json(
+ { error: "Worker deployed but failed to retrieve workers.dev subdomain. Make sure you have setup a workers.dev subdomain in Cloudflare Dashboard." },
+ { status: 400 }
+ );
+ }
+
+ // Create proxy pool entry with type cloudflare
+ const proxyPool = await createProxyPool({
+ name: projectName,
+ proxyUrl: deployUrl,
+ type: "cloudflare",
+ noProxy: "",
+ isActive: true,
+ strictProxy: false,
+ });
+
+ return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
+ } catch (error) {
+ console.log("Error deploying Cloudflare relay:", error);
+ return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
+ }
+}
diff --git a/src/app/api/proxy-pools/route.js b/src/app/api/proxy-pools/route.js
index 998a0d50..ce119f26 100644
--- a/src/app/api/proxy-pools/route.js
+++ b/src/app/api/proxy-pools/route.js
@@ -7,7 +7,7 @@ function toBoolean(value) {
return undefined;
}
-const VALID_PROXY_TYPES = ["http", "vercel"];
+const VALID_PROXY_TYPES = ["http", "vercel", "cloudflare"];
function normalizeProxyPoolInput(body = {}) {
const name = typeof body?.name === "string" ? body.name.trim() : "";
diff --git a/src/lib/network/connectionProxy.js b/src/lib/network/connectionProxy.js
index e7836dd8..ca590819 100644
--- a/src/lib/network/connectionProxy.js
+++ b/src/lib/network/connectionProxy.js
@@ -68,12 +68,12 @@ export async function resolveConnectionProxyConfig(
if (isValidPool) {
/**
- * Vercel relay proxies use base URL rewriting
+ * Vercel/Cloudflare relay proxies use base URL rewriting
* instead of HTTP_PROXY environment variables.
*/
- if (proxyPool.type === "vercel") {
+ if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare") {
return {
- source: "vercel",
+ source: proxyPool.type,
proxyPoolId,
proxyPool,
@@ -84,7 +84,7 @@ export async function resolveConnectionProxyConfig(
strictProxy: proxyPool.strictProxy === true,
- vercelRelayUrl: proxyUrl,
+ vercelRelayUrl: proxyUrl, // Still mapped to vercelRelayUrl in the unified payload since they use the exact same header spec
};
}