feat(proxy): add proxy distribution UI with per-connection toggles (#3172)

Integrated into release/v3.8.11
This commit is contained in:
PizzaV
2026-06-05 07:05:33 +02:00
committed by GitHub
parent de5c842301
commit c48e0851f7
3 changed files with 590 additions and 19 deletions

View File

@@ -738,6 +738,8 @@ interface ConnectionRowConnection {
tokenExpiresAt?: string;
maxConcurrent?: number | null;
authType?: string;
proxyEnabled?: boolean;
perKeyProxyEnabled?: boolean;
}
interface ConnectionRowProps {
@@ -770,6 +772,10 @@ interface ConnectionRowProps {
hasProxy?: boolean;
proxySource?: string;
proxyHost?: string;
proxyEnabled?: boolean;
perKeyProxyEnabled?: boolean;
onToggleProxyEnabled?: (enabled: boolean) => void;
onTogglePerKeyProxyEnabled?: (enabled: boolean) => void;
onRefreshToken?: () => void;
isRefreshing?: boolean;
onApplyCodexAuthLocal?: () => void;
@@ -1393,6 +1399,7 @@ export default function ProviderDetailPage() {
const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible);
const notify = useNotificationStore();
const [proxyTarget, setProxyTarget] = useState(null);
const [distributingProxies, setDistributingProxies] = useState(false);
const [proxyConfig, setProxyConfig] = useState(null);
const [connProxyMap, setConnProxyMap] = useState<
Record<string, { proxy: any; level: string } | null>
@@ -2389,6 +2396,248 @@ export default function ProviderDetailPage() {
}
};
const handleToggleProxyEnabled = async (connectionId, proxyEnabled) => {
try {
const res = await fetch(`/api/providers/${connectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proxyEnabled }),
});
if (res.ok) {
setConnections((prev) =>
prev.map((c) => (c.id === connectionId ? { ...c, proxyEnabled } : c))
);
}
} catch (error) {
console.error("Error toggling proxy enabled:", error);
}
};
const handleTogglePerKeyProxyEnabled = async (connectionId, perKeyProxyEnabled) => {
try {
const res = await fetch(`/api/providers/${connectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ perKeyProxyEnabled }),
});
if (res.ok) {
setConnections((prev) =>
prev.map((c) => (c.id === connectionId ? { ...c, perKeyProxyEnabled } : c))
);
}
} catch (error) {
console.error("Error toggling per-key proxy enabled:", error);
}
};
const handleDistributeProxies = async (tagFilter?: string) => {
const targetConnections = tagFilter
? connections.filter(
(c: any) =>
(c.providerSpecificData?.tag as string | undefined)?.trim() === tagFilter
)
: connections;
if (targetConnections.length === 0) return;
setDistributingProxies(true);
try {
const proxiesRes = await fetch("/api/settings/proxies");
if (!proxiesRes.ok) throw new Error("Failed to fetch proxies");
const proxiesData = await proxiesRes.json();
const savedProxies = (proxiesData?.items || []).filter(
(p: any) => p.status === "active"
);
if (savedProxies.length === 0) {
notify.error("No saved proxies found. Add proxies in Settings → Proxy first.");
return;
}
let assigned = 0;
const sorted = [...targetConnections].sort(
(a: any, b: any) => (a.priority || 0) - (b.priority || 0)
);
for (let i = 0; i < sorted.length; i++) {
const conn = sorted[i] as any;
const proxy = savedProxies[i % savedProxies.length];
try {
await fetch("/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: conn.id,
proxyId: null,
}),
});
} catch {
/* clear old assignment */
}
const patchRes = await fetch(`/api/providers/${conn.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proxyEnabled: true, perKeyProxyEnabled: true }),
});
if (!patchRes.ok) {
console.error(`Failed to update connection ${conn.id}`);
continue;
}
// Assign new proxy
const assignRes = await fetch("/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: conn.id,
proxyId: proxy.id,
}),
});
if (!assignRes.ok) {
console.error(`Failed to assign proxy to ${conn.id}`);
continue;
}
assigned++;
}
await fetchConnections();
const tagLabel = tagFilter ? `"${tagFilter}" ` : "";
notify.success(
`Distributed ${assigned} proxy assignment(s) across ${tagLabel}${sorted.length} connection(s).`
);
} catch (err) {
console.error("Error distributing proxies:", err);
notify.error("Failed to distribute proxies.");
} finally {
setDistributingProxies(false);
}
};
const handleToggleProxyEnabled = async (connectionId, proxyEnabled) => {
try {
const res = await fetch(`/api/providers/${connectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proxyEnabled }),
});
if (res.ok) {
setConnections((prev) =>
prev.map((c) => (c.id === connectionId ? { ...c, proxyEnabled } : c))
);
}
} catch (error) {
console.error("Error toggling proxy enabled:", error);
}
};
const handleTogglePerKeyProxyEnabled = async (connectionId, perKeyProxyEnabled) => {
try {
const res = await fetch(`/api/providers/${connectionId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ perKeyProxyEnabled }),
});
if (res.ok) {
setConnections((prev) =>
prev.map((c) => (c.id === connectionId ? { ...c, perKeyProxyEnabled } : c))
);
}
} catch (error) {
console.error("Error toggling per-key proxy enabled:", error);
}
};
const handleDistributeProxies = async (tagFilter?: string) => {
const targetConnections = tagFilter
? connections.filter(
(c: any) =>
(c.providerSpecificData?.tag as string | undefined)?.trim() === tagFilter
)
: connections;
if (targetConnections.length === 0) return;
setDistributingProxies(true);
try {
const proxiesRes = await fetch("/api/settings/proxies");
if (!proxiesRes.ok) throw new Error("Failed to fetch proxies");
const proxiesData = await proxiesRes.json();
const savedProxies = (proxiesData?.items || []).filter(
(p: any) => p.status === "active"
);
if (savedProxies.length === 0) {
notify.error("No saved proxies found. Add proxies in Settings → Proxy first.");
return;
}
let assigned = 0;
const sorted = [...targetConnections].sort(
(a: any, b: any) => (a.priority || 0) - (b.priority || 0)
);
for (let i = 0; i < sorted.length; i++) {
const conn = sorted[i] as any;
const proxy = savedProxies[i % savedProxies.length];
try {
await fetch("/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: conn.id,
proxyId: null,
}),
});
} catch {
/* clear old assignment */
}
const patchRes = await fetch(`/api/providers/${conn.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proxyEnabled: true, perKeyProxyEnabled: true }),
});
if (!patchRes.ok) {
console.error(`Failed to update connection ${conn.id}`);
continue;
}
// Assign new proxy
const assignRes = await fetch("/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: conn.id,
proxyId: proxy.id,
}),
});
if (!assignRes.ok) {
console.error(`Failed to assign proxy to ${conn.id}`);
continue;
}
assigned++;
}
await fetchConnections();
const tagLabel = tagFilter ? `"${tagFilter}" ` : "";
notify.success(
`Distributed ${assigned} proxy assignment(s) across ${tagLabel}${sorted.length} connection(s).`
);
} catch (err) {
console.error("Error distributing proxies:", err);
notify.error("Failed to distribute proxies.");
} finally {
setDistributingProxies(false);
}
};
const handleToggleRateLimit = async (connectionId, enabled) => {
try {
const res = await fetch("/api/rate-limits", {
@@ -3289,8 +3538,9 @@ export default function ProviderDetailPage() {
const [clearingModels, setClearingModels] = useState(false);
const providerAliasEntries = useMemo(
() =>
Object.entries(modelAliases).filter(([, model]) =>
(model as string).startsWith(`${providerStorageAlias}/`)
Object.entries(modelAliases).filter(
([, model]) =>
typeof model === "string" && model.startsWith(`${providerStorageAlias}/`)
),
[modelAliases, providerStorageAlias]
);
@@ -4073,6 +4323,24 @@ export default function ProviderDetailPage() {
</button>
</div>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{connections.length > 0 && (
<button
onClick={() => handleDistributeProxies()}
disabled={distributingProxies || batchTesting || !!retestingId}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
distributingProxies
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
: "bg-bg-subtle border-border text-text-muted hover:text-text-primary hover:border-primary/40"
}`}
title={t("distributeProxies")}
aria-label={t("distributeProxies")}
>
<span className="material-symbols-outlined text-[14px]">
{distributingProxies ? "sync" : "swap_horiz"}
</span>
{distributingProxies ? t("distributing") : t("distributeProxies")}
</button>
)}
{connections.length > 1 && (
<button
onClick={handleBatchTestAll}
@@ -4410,6 +4678,10 @@ export default function ProviderDetailPage() {
hasProxy={!!connProxyMap[conn.id]?.proxy}
proxySource={connProxyMap[conn.id]?.level || null}
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
proxyEnabled={conn.proxyEnabled !== false}
onToggleProxyEnabled={(enabled) => handleToggleProxyEnabled(conn.id, enabled)}
perKeyProxyEnabled={conn.perKeyProxyEnabled === true}
onTogglePerKeyProxyEnabled={(enabled) => handleTogglePerKeyProxyEnabled(conn.id, enabled)}
/>
))}
</div>
@@ -4451,17 +4723,30 @@ export default function ProviderDetailPage() {
</span>
</label>
{selectedIds.size > 0 && (
<Button
variant="danger"
size="sm"
icon="delete"
loading={batchDeleting}
onClick={handleBatchDelete}
>
{t("batchDeleteSelected", { count: selectedIds.size })}
</Button>
)}
<div className="flex items-center gap-2">
{selectedIds.size === 0 && connections.length > 0 && (
<Button
variant="secondary"
size="sm"
icon="shield"
loading={distributingProxies}
onClick={() => handleDistributeProxies()}
>
Distribute Proxies
</Button>
)}
{selectedIds.size > 0 && (
<Button
variant="danger"
size="sm"
icon="delete"
loading={batchDeleting}
onClick={handleBatchDelete}
>
{t("batchDeleteSelected", { count: selectedIds.size })}
</Button>
)}
</div>
</div>
) : null}
<div className="flex flex-col gap-0 border border-t-0 border-border rounded-b-lg overflow-hidden">
@@ -4485,6 +4770,15 @@ export default function ProviderDetailPage() {
{tag}
</span>
<div className="flex-1 h-px bg-black/[0.04] dark:bg-white/[0.04]" />
<Button
variant="ghost"
size="sm"
icon="shield"
loading={distributingProxies}
onClick={() => handleDistributeProxies(tag)}
>
Distribute Proxies
</Button>
<span className="text-[10px] text-text-muted/40">
{groupConns.length}
</span>
@@ -4597,6 +4891,10 @@ export default function ProviderDetailPage() {
hasProxy={!!connProxyMap[conn.id]?.proxy}
proxySource={connProxyMap[conn.id]?.level || null}
proxyHost={connProxyMap[conn.id]?.proxy?.host || null}
proxyEnabled={conn.proxyEnabled !== false}
onToggleProxyEnabled={(enabled) => handleToggleProxyEnabled(conn.id, enabled)}
perKeyProxyEnabled={conn.perKeyProxyEnabled === true}
onTogglePerKeyProxyEnabled={(enabled) => handleTogglePerKeyProxyEnabled(conn.id, enabled)}
/>
))}
</div>
@@ -5050,11 +5348,13 @@ export default function ProviderDetailPage() {
{importProgress.logs.length > 0 && (
<div className="max-h-48 overflow-y-auto rounded-lg bg-black/5 dark:bg-white/5 p-3 border border-black/5 dark:border-white/5">
<div className="flex flex-col gap-1">
{importProgress.logs.map((log, i) => (
{importProgress.logs.map((log, i) => (
<p
key={i}
className={`text-xs font-mono ${
log.startsWith("✓") ? "text-green-500 font-semibold" : "text-text-muted"
typeof log === "string" && log.startsWith("✓")
? "text-green-500 font-semibold"
: "text-text-muted"
}`}
>
{log}
@@ -6846,6 +7146,10 @@ function ConnectionRow({
isApplyingGeminiAuthLocal,
onExportGeminiAuthFile,
isExportingGeminiAuthFile,
perKeyProxyEnabled,
onTogglePerKeyProxyEnabled,
proxyEnabled,
onToggleProxyEnabled,
}: ConnectionRowProps) {
const t = useTranslations("providers");
const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible);
@@ -7167,6 +7471,74 @@ function ConnectionRow({
</button>
</>
)}
{onToggleProxyEnabled && (
<>
<span className="text-text-muted/30 select-none">|</span>
<button
onClick={() => onToggleProxyEnabled(!proxyEnabled)}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
proxyEnabled
? "bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title={proxyEnabled ? t("proxyEnabledTitle") : t("proxyDisabledTitle")}
>
<span className="material-symbols-outlined text-[13px]">vpn_lock</span>
{proxyEnabled ? t("proxyOn") : t("proxyOff")}
</button>
</>
)}
{onTogglePerKeyProxyEnabled && (
<>
<span className="text-text-muted/30 select-none">|</span>
<button
onClick={() => onTogglePerKeyProxyEnabled(!perKeyProxyEnabled)}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
perKeyProxyEnabled
? "bg-violet-500/15 text-violet-500 hover:bg-violet-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title={perKeyProxyEnabled ? t("perKeyProxyEnabledTitle") : t("perKeyProxyDisabledTitle")}
>
<span className="material-symbols-outlined text-[13px]">key</span>
{perKeyProxyEnabled ? t("perKeyProxyOn") : t("perKeyProxyOff")}
</button>
</>
)}
{onToggleProxyEnabled && (
<>
<span className="text-text-muted/30 select-none">|</span>
<button
onClick={() => onToggleProxyEnabled(!proxyEnabled)}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
proxyEnabled
? "bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title={proxyEnabled ? t("proxyEnabledTitle") : t("proxyDisabledTitle")}
>
<span className="material-symbols-outlined text-[13px]">vpn_lock</span>
{proxyEnabled ? t("proxyOn") : t("proxyOff")}
</button>
</>
)}
{onTogglePerKeyProxyEnabled && (
<>
<span className="text-text-muted/30 select-none">|</span>
<button
onClick={() => onTogglePerKeyProxyEnabled(!perKeyProxyEnabled)}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
perKeyProxyEnabled
? "bg-violet-500/15 text-violet-500 hover:bg-violet-500/25"
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
}`}
title={perKeyProxyEnabled ? t("perKeyProxyEnabledTitle") : t("perKeyProxyDisabledTitle")}
>
<span className="material-symbols-outlined text-[13px]">key</span>
{perKeyProxyEnabled ? t("perKeyProxyOn") : t("perKeyProxyOff")}
</button>
</>
)}
{hasProxy &&
(() => {
const colorClass =

View File

@@ -1,13 +1,33 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, Button, ProxyConfigModal } from "@/shared/components";
import { useState, useEffect, useCallback, useRef } from "react";
import { Card, Button, Toggle, ProxyConfigModal } from "@/shared/components";
import { useTranslations } from "next-intl";
type GlobalProxyConfig = { type: string; host: string; port: number } | null;
type HealthcheckResult = {
proxyUrl: string;
ok: boolean;
latencyMs: number | null;
};
type HealthcheckSummary = {
total: number;
working: number;
failed: number;
};
export default function GlobalConfigTab() {
const [proxyModalOpen, setProxyModalOpen] = useState(false);
const [globalProxy, setGlobalProxy] = useState<GlobalProxyConfig>(null);
const [perKeyProxyEnabled, setPerKeyProxyEnabled] = useState(false);
const [perKeyLoading, setPerKeyLoading] = useState(true);
const [targetUrl, setTargetUrl] = useState("https://api.openai.com/v1/models");
const [testing, setTesting] = useState(false);
const [results, setResults] = useState<HealthcheckResult[] | null>(null);
const [summary, setSummary] = useState<HealthcheckSummary | null>(null);
const [error, setError] = useState<string | null>(null);
const mountedRef = useRef(true);
const t = useTranslations("settings");
const tc = useTranslations("common");
@@ -21,10 +41,73 @@ export default function GlobalConfigTab() {
} catch {}
}, []);
const loadPerKeyProxyEnabled = useCallback(async () => {
try {
const res = await fetch("/api/settings", { cache: "no-store" });
if (res.ok) {
const data = await res.json();
if (mountedRef.current) setPerKeyProxyEnabled(data.perKeyProxyEnabled === true);
}
} catch {
/* leave default */
} finally {
if (mountedRef.current) setPerKeyLoading(false);
}
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- async data fetch on mount
mountedRef.current = true;
loadGlobalProxy();
}, [loadGlobalProxy]);
loadPerKeyProxyEnabled();
return () => {
mountedRef.current = false;
};
}, [loadGlobalProxy, loadPerKeyProxyEnabled]);
const handleTogglePerKeyProxyEnabled = async () => {
const newValue = !perKeyProxyEnabled;
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ perKeyProxyEnabled: newValue }),
});
if (res.ok) {
setPerKeyProxyEnabled(newValue);
}
} catch (err) {
console.error("Failed to update per-key proxy setting:", err);
}
};
const runHealthcheck = async () => {
setTesting(true);
setResults(null);
setSummary(null);
setError(null);
try {
const res = await fetch("/api/proxy-fallback/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetUrl }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Request failed" }));
setError(err.error || `HTTP ${res.status}`);
return;
}
const data = await res.json();
setResults(data.results);
setSummary(data.summary);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : t("healthcheckFailed"));
} finally {
setTesting(false);
}
};
return (
<>
@@ -59,6 +142,118 @@ export default function GlobalConfigTab() {
</div>
</div>
</Card>
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-xl text-violet-500" aria-hidden="true">
key
</span>
<div>
<h2 className="text-lg font-bold">{t("perKeyProxyEnabled")}</h2>
<p className="text-sm text-text-muted">{t("perKeyProxyEnabledDesc")}</p>
</div>
</div>
<Toggle
checked={perKeyProxyEnabled}
disabled={perKeyLoading}
onChange={handleTogglePerKeyProxyEnabled}
/>
</div>
</div>
</Card>
<Card className="p-0 overflow-hidden">
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
network_check
</span>
<h2 className="text-lg font-bold">Bulk Healthcheck</h2>
</div>
<p className="text-sm text-text-muted mb-4">
Test all configured proxies against a target URL to find which ones work.
</p>
<div className="flex items-center gap-3 mb-4">
<input
type="text"
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
placeholder="https://api.openai.com/v1/models"
className="flex-1 px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
<Button
size="sm"
variant="primary"
icon={testing ? "refresh" : "play_arrow"}
disabled={testing}
onClick={runHealthcheck}
>
{testing ? "Testing..." : "Healthcheck All"}
</Button>
</div>
{error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-sm text-red-400">
{error}
</div>
)}
{testing && !results && (
<div className="flex items-center gap-2 text-sm text-text-muted py-2">
<span className="material-symbols-outlined animate-spin text-lg">refresh</span>
Testing all proxies in parallel...
</div>
)}
{summary && (
<div className="flex items-center gap-4 mb-3 text-sm">
<span className="text-text-muted">
Total: <strong>{summary.total}</strong>
</span>
<span className="text-emerald-400">
Working: <strong>{summary.working}</strong>
</span>
<span className="text-red-400">
Failed: <strong>{summary.failed}</strong>
</span>
</div>
)}
{results && results.length > 0 && (
<div className="max-h-64 overflow-y-auto rounded-lg border border-black/10 dark:border-white/10">
<table className="w-full text-xs">
<thead>
<tr className="bg-black/5 dark:bg-white/5">
<th className="text-left px-3 py-2 font-medium text-text-muted">Status</th>
<th className="text-left px-3 py-2 font-medium text-text-muted">Proxy URL</th>
<th className="text-right px-3 py-2 font-medium text-text-muted">Latency</th>
</tr>
</thead>
<tbody>
{results.map((r, i) => (
<tr key={i} className="border-t border-black/5 dark:border-white/5">
<td className="px-3 py-1.5">
{r.ok ? (
<span className="text-emerald-400 text-sm"></span>
) : (
<span className="text-red-400 text-sm"></span>
)}
</td>
<td className="px-3 py-1.5 font-mono truncate max-w-xs">{r.proxyUrl}</td>
<td className="px-3 py-1.5 text-right text-text-muted">
{r.latencyMs !== null ? `${r.latencyMs}ms` : "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</Card>
<ProxyConfigModal
isOpen={proxyModalOpen}
onClose={() => setProxyModalOpen(false)}

View File

@@ -132,6 +132,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
group,
maxConcurrent,
quotaWindowThresholds: incomingWindowThresholds,
proxyEnabled,
perKeyProxyEnabled,
projectId,
providerSpecificData: incomingPsd,
} = body;
@@ -184,6 +186,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
}
}
if (projectId !== undefined) updateData.projectId = projectId;
if (proxyEnabled !== undefined) updateData.proxyEnabled = proxyEnabled;
if (perKeyProxyEnabled !== undefined) updateData.perKeyProxyEnabled = perKeyProxyEnabled;
// Merge providerSpecificData (partial update — preserve existing keys not sent by caller)
if (incomingPsd !== undefined && incomingPsd !== null && typeof incomingPsd === "object") {