diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js
index 008cce0..faca363 100644
--- a/open-sse/config/providerModels.js
+++ b/open-sse/config/providerModels.js
@@ -44,7 +44,7 @@ export const PROVIDER_MODELS = {
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" },
{ id: "vision-model", name: "Qwen3 Vision Model" },
- { id: "coder-model", name: "Qwen3.5 Coder Model" },
+ { id: "coder-model", name: "Qwen3.6 Coder Model" },
],
if: [ // iFlow AI
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
diff --git a/open-sse/executors/cursor.js b/open-sse/executors/cursor.js
index 18b9adf..2873782 100644
--- a/open-sse/executors/cursor.js
+++ b/open-sse/executors/cursor.js
@@ -215,7 +215,7 @@ export class CursorExecutor extends BaseExecutor {
const transformedBody = this.transformRequest(model, body, stream, credentials);
try {
- const shouldForceFetch = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true;
+ const shouldForceFetch = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true || !!proxyOptions?.vercelRelayUrl;
const response = (http2 && !shouldForceFetch)
? await this.makeHttp2Request(url, headers, transformedBody, signal)
: await this.makeFetchRequest(url, headers, transformedBody, signal, proxyOptions);
diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js
index fc564d3..0b3bcc1 100644
--- a/open-sse/handlers/chatCore.js
+++ b/open-sse/handlers/chatCore.js
@@ -98,9 +98,14 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
connectionProxyEnabled: credentials?.providerSpecificData?.connectionProxyEnabled === true,
connectionProxyUrl: credentials?.providerSpecificData?.connectionProxyUrl || "",
connectionNoProxy: credentials?.providerSpecificData?.connectionNoProxy || "",
+ vercelRelayUrl: credentials?.providerSpecificData?.vercelRelayUrl || "",
};
- if (proxyOptions.connectionProxyEnabled && proxyOptions.connectionProxyUrl) {
+ if (proxyOptions.vercelRelayUrl) {
+ const connectionName = credentials?.connectionName || credentials?.connectionId || "unknown";
+ const poolId = credentials?.providerSpecificData?.connectionProxyPoolId || "none";
+ log?.info?.("PROXY", `${provider.toUpperCase()} | ${model} | conn=${connectionName} | pool=${poolId} | vercel-relay=${proxyOptions.vercelRelayUrl}`);
+ } else if (proxyOptions.connectionProxyEnabled && proxyOptions.connectionProxyUrl) {
let maskedProxyUrl = proxyOptions.connectionProxyUrl;
try {
const parsed = new URL(proxyOptions.connectionProxyUrl);
diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js
index 2e7b730..c60854c 100644
--- a/open-sse/utils/proxyFetch.js
+++ b/open-sse/utils/proxyFetch.js
@@ -198,6 +198,18 @@ async function createBypassRequest(parsedUrl, realIP, options) {
export async function proxyAwareFetch(url, options = {}, proxyOptions = null) {
const targetUrl = typeof url === "string" ? url : url.toString();
+ // Vercel relay: forward request via relay headers
+ const vercelRelayUrl = normalizeString(proxyOptions?.vercelRelayUrl);
+ if (vercelRelayUrl) {
+ const parsed = new URL(targetUrl);
+ const relayHeaders = {
+ ...options.headers,
+ "x-relay-target": `${parsed.protocol}//${parsed.host}`,
+ "x-relay-path": `${parsed.pathname}${parsed.search}`,
+ };
+ return originalFetch(vercelRelayUrl, { ...options, headers: relayHeaders });
+ }
+
const connectionProxyUrl = resolveConnectionProxyUrl(targetUrl, proxyOptions);
const envProxyUrl = connectionProxyUrl ? null : normalizeProxyUrl(getEnvProxyUrl(targetUrl));
const proxyUrl = connectionProxyUrl || envProxyUrl;
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js
index b5f1374..3adb0a6 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js
@@ -1019,7 +1019,10 @@ function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCusto
>
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
- {fullModel}
+
+ {fullModel}
+ {model.name && {model.name} }
+
{onTest && (
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
- {fullModel}
+
+ {fullModel}
+ {model.name && {model.name} }
+
{onTest && (
diff --git a/src/app/(dashboard)/dashboard/proxy-pools/page.js b/src/app/(dashboard)/dashboard/proxy-pools/page.js
index 36db1ec..2fe68cf 100644
--- a/src/app/(dashboard)/dashboard/proxy-pools/page.js
+++ b/src/app/(dashboard)/dashboard/proxy-pools/page.js
@@ -32,11 +32,14 @@ export default function ProxyPoolsPage() {
const [loading, setLoading] = useState(true);
const [showFormModal, setShowFormModal] = useState(false);
const [showBatchImportModal, setShowBatchImportModal] = useState(false);
+ const [showVercelModal, setShowVercelModal] = 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 [saving, setSaving] = useState(false);
const [importing, setImporting] = useState(false);
+ const [deploying, setDeploying] = useState(false);
const [testingId, setTestingId] = useState(null);
const notify = useNotificationStore();
@@ -169,6 +172,41 @@ export default function ProxyPoolsPage() {
setShowBatchImportModal(false);
};
+ const openVercelModal = () => {
+ setVercelForm({ vercelToken: "", projectName: "vercel-relay" });
+ setShowVercelModal(true);
+ };
+
+ const closeVercelModal = () => {
+ if (deploying) return;
+ setShowVercelModal(false);
+ };
+
+ const handleVercelDeploy = async () => {
+ if (!vercelForm.vercelToken.trim()) return;
+ setDeploying(true);
+ try {
+ const res = await fetch("/api/proxy-pools/vercel-deploy", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(vercelForm),
+ });
+ const data = await res.json();
+ if (res.ok) {
+ await fetchProxyPools();
+ closeVercelModal();
+ notify.success(`Deployed: ${data.deployUrl}`);
+ } else {
+ notify.error(data.error || "Deploy failed");
+ }
+ } catch (error) {
+ console.log("Error deploying Vercel relay:", error);
+ notify.error("Deploy failed");
+ } finally {
+ setDeploying(false);
+ }
+ };
+
const parseProxyLine = (line) => {
const trimmed = line.trim();
if (!trimmed) return null;
@@ -305,8 +343,11 @@ export default function ProxyPoolsPage() {
+
+ Vercel Relay
+
- Batch Import Proxies
+ Batch Import
Add Proxy Pool
@@ -341,6 +382,9 @@ export default function ProxyPoolsPage() {
{pool.isActive ? "active" : "inactive"}
+ {pool.type === "vercel" && (
+ vercel relay
+ )}
{pool.boundConnectionCount || 0} bound
@@ -420,6 +464,54 @@ export default function ProxyPoolsPage() {
+
+
+
+
What is Vercel Relay?
+
+ Deploys an edge relay function to Vercel. All AI provider requests will be forwarded through Vercel's edge network, masking your real IP from providers.
+
+
+ Your IP is replaced by Vercel's dynamic edge IPs (hundreds of IPs across 20+ global regions)
+ Vercel serves millions of apps — providers can't block Vercel IPs without affecting legitimate traffic
+ Free tier: 100GB bandwidth/month, 500K edge invocations
+ Deploy multiple relays on different accounts for more IP diversity
+
+
+
setVercelForm((prev) => ({ ...prev, vercelToken: e.target.value }))}
+ placeholder="your-vercel-api-token"
+ hint={<>Token is used once for deployment and not stored.
Get token → >}
+ type="password"
+ />
+
setVercelForm((prev) => ({ ...prev, projectName: e.target.value }))}
+ placeholder="my-relay"
+ hint="Unique name for your Vercel project. Leave empty for auto-generated name."
+ />
+
+
+ {deploying ? "Deploying... (may take ~1 min)" : "Deploy"}
+
+
+ Cancel
+
+
+
+
+
controller.abort(), timeoutMs);
+ try {
+ const res = await undiciFetch(relayUrl, {
+ method: "GET",
+ headers: {
+ "x-relay-target": "https://httpbin.org",
+ "x-relay-path": "/get",
+ },
+ signal: controller.signal,
+ });
+ return {
+ ok: res.ok,
+ status: res.status,
+ statusText: res.statusText,
+ elapsedMs: Date.now() - startedAt,
+ };
+ } catch (err) {
+ return {
+ ok: false,
+ status: 500,
+ error: err?.name === "AbortError" ? "Relay test timed out" : (err?.message || String(err)),
+ };
+ } finally {
+ clearTimeout(timer);
+ }
+}
// POST /api/proxy-pools/[id]/test - Test proxy pool entry
export async function POST(request, { params }) {
@@ -12,7 +43,9 @@ export async function POST(request, { params }) {
return NextResponse.json({ error: "Proxy pool not found" }, { status: 404 });
}
- const result = await testProxyUrl({ proxyUrl: proxyPool.proxyUrl });
+ const result = proxyPool.type === "vercel"
+ ? await testVercelRelay(proxyPool.proxyUrl)
+ : await testProxyUrl({ proxyUrl: proxyPool.proxyUrl });
const now = new Date().toISOString();
await updateProxyPool(id, {
diff --git a/src/app/api/proxy-pools/route.js b/src/app/api/proxy-pools/route.js
index c5b8d9f..998a0d5 100644
--- a/src/app/api/proxy-pools/route.js
+++ b/src/app/api/proxy-pools/route.js
@@ -7,12 +7,15 @@ function toBoolean(value) {
return undefined;
}
+const VALID_PROXY_TYPES = ["http", "vercel"];
+
function normalizeProxyPoolInput(body = {}) {
const name = typeof body?.name === "string" ? body.name.trim() : "";
const proxyUrl = typeof body?.proxyUrl === "string" ? body.proxyUrl.trim() : "";
const noProxy = typeof body?.noProxy === "string" ? body.noProxy.trim() : "";
const isActive = body?.isActive === undefined ? true : body.isActive === true;
const strictProxy = body?.strictProxy === true;
+ const type = VALID_PROXY_TYPES.includes(body?.type) ? body.type : "http";
if (!name) {
return { error: "Name is required" };
@@ -22,7 +25,7 @@ function normalizeProxyPoolInput(body = {}) {
return { error: "Proxy URL is required" };
}
- return { name, proxyUrl, noProxy, isActive, strictProxy };
+ return { name, proxyUrl, noProxy, isActive, strictProxy, type };
}
function buildUsageMap(connections = []) {
diff --git a/src/app/api/proxy-pools/vercel-deploy/route.js b/src/app/api/proxy-pools/vercel-deploy/route.js
new file mode 100644
index 0000000..e87390f
--- /dev/null
+++ b/src/app/api/proxy-pools/vercel-deploy/route.js
@@ -0,0 +1,142 @@
+import { NextResponse } from "next/server";
+import { createProxyPool } from "@/models";
+
+const VERCEL_API = "https://api.vercel.com";
+
+// Relay function source code deployed to Vercel
+// Forwards requests to target URL specified in x-relay-target header
+const RELAY_FUNCTION_CODE = `
+export const config = { runtime: "edge" };
+
+export default async function handler(req) {
+ const target = req.headers.get("x-relay-target");
+ const relayPath = req.headers.get("x-relay-path") || "/";
+ if (!target) {
+ return new Response(JSON.stringify({ error: "Missing x-relay-target header" }), {
+ status: 400,
+ headers: { "content-type": "application/json" },
+ });
+ }
+
+ const targetUrl = target.replace(/\\/$/, "") + relayPath;
+
+ const headers = new Headers(req.headers);
+ headers.delete("x-relay-target");
+ headers.delete("x-relay-path");
+ headers.delete("host");
+
+ const response = await fetch(targetUrl, {
+ method: req.method,
+ headers,
+ body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
+ duplex: "half",
+ });
+
+ return new Response(response.body, {
+ status: response.status,
+ headers: response.headers,
+ });
+}
+`;
+
+async function pollDeployment(deploymentId, token, maxMs = 120000) {
+ const start = Date.now();
+ while (Date.now() - start < maxMs) {
+ const res = await fetch(`${VERCEL_API}/v13/deployments/${deploymentId}`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ const data = await res.json();
+ if (data.readyState === "READY") return data;
+ if (data.readyState === "ERROR" || data.readyState === "CANCELED") {
+ throw new Error(`Deployment failed: ${data.readyState}`);
+ }
+ await new Promise((r) => setTimeout(r, 3000));
+ }
+ throw new Error("Deployment timed out");
+}
+
+// POST /api/proxy-pools/vercel-deploy
+export async function POST(request) {
+ try {
+ const body = await request.json();
+ const vercelToken = body.vercelToken;
+ const projectName = body.projectName?.trim() || `relay-${Date.now().toString(36)}`;
+
+ if (!vercelToken) {
+ return NextResponse.json({ error: "Vercel API token is required" }, { status: 400 });
+ }
+
+ // Deploy relay function to Vercel
+ const deployRes = await fetch(`${VERCEL_API}/v13/deployments`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${vercelToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: projectName,
+ files: [
+ {
+ file: "api/relay.js",
+ data: RELAY_FUNCTION_CODE,
+ },
+ {
+ file: "package.json",
+ data: JSON.stringify({ name: projectName, version: "1.0.0" }),
+ },
+ {
+ file: "vercel.json",
+ data: JSON.stringify({
+ rewrites: [{ source: "/(.*)", destination: "/api/relay" }],
+ }),
+ },
+ ],
+ projectSettings: {
+ framework: null,
+ },
+ target: "production",
+ }),
+ });
+
+ if (!deployRes.ok) {
+ const err = await deployRes.json().catch(() => ({}));
+ return NextResponse.json(
+ { error: err.error?.message || "Failed to create Vercel deployment" },
+ { status: deployRes.status }
+ );
+ }
+
+ const deployment = await deployRes.json();
+ const deploymentId = deployment.id || deployment.uid;
+
+ // Disable deployment protection (Vercel Authentication)
+ const projectId = deployment.projectId || projectName;
+ await fetch(`${VERCEL_API}/v9/projects/${projectId}`, {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${vercelToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ ssoProtection: null }),
+ });
+
+ // Poll until deployment is ready
+ const ready = await pollDeployment(deploymentId, vercelToken);
+ const deployUrl = `https://${ready.url}`;
+
+ // Create proxy pool entry with type vercel
+ const proxyPool = await createProxyPool({
+ name: projectName,
+ proxyUrl: deployUrl,
+ type: "vercel",
+ noProxy: "",
+ isActive: true,
+ strictProxy: false,
+ });
+
+ return NextResponse.json({ proxyPool, deployUrl }, { status: 201 });
+ } catch (error) {
+ console.log("Error deploying Vercel relay:", error);
+ return NextResponse.json({ error: error.message || "Deploy failed" }, { status: 500 });
+ }
+}
diff --git a/src/lib/localDb.js b/src/lib/localDb.js
index 581afcf..0884808 100644
--- a/src/lib/localDb.js
+++ b/src/lib/localDb.js
@@ -496,6 +496,7 @@ export async function createProxyPool(data) {
name: data.name,
proxyUrl: data.proxyUrl,
noProxy: data.noProxy || "",
+ type: data.type || "http",
isActive: data.isActive !== undefined ? data.isActive : true,
strictProxy: data.strictProxy === true,
testStatus: data.testStatus || "unknown",
diff --git a/src/lib/network/connectionProxy.js b/src/lib/network/connectionProxy.js
index cb51f11..7243033 100644
--- a/src/lib/network/connectionProxy.js
+++ b/src/lib/network/connectionProxy.js
@@ -28,6 +28,20 @@ export async function resolveConnectionProxyConfig(providerSpecificData = {}) {
const noProxy = normalizeString(proxyPool?.noProxy);
if (proxyPool && proxyPool.isActive === true && proxyUrl) {
+ // Vercel relay: rewrite base URL instead of using HTTP_PROXY
+ if (proxyPool.type === "vercel") {
+ return {
+ source: "vercel",
+ proxyPoolId,
+ proxyPool,
+ connectionProxyEnabled: false,
+ connectionProxyUrl: "",
+ connectionNoProxy: noProxy,
+ strictProxy: proxyPool.strictProxy === true,
+ vercelRelayUrl: proxyUrl,
+ };
+ }
+
return {
source: "pool",
proxyPoolId,
diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js
index 3f426c1..9f13ffb 100644
--- a/src/sse/services/auth.js
+++ b/src/sse/services/auth.js
@@ -145,6 +145,7 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
connectionProxyUrl: resolvedProxy.connectionProxyUrl,
connectionNoProxy: resolvedProxy.connectionNoProxy,
connectionProxyPoolId: resolvedProxy.proxyPoolId || null,
+ vercelRelayUrl: resolvedProxy.vercelRelayUrl || "",
},
connectionId: connection.id,
// Include current status for optimization check