Connections
@@ -1630,6 +1639,7 @@ export default function ProviderDetailPage() {
>
)}
+
)}
{/* Models */}
diff --git a/src/lib/network/connectionProxy.js b/src/lib/network/connectionProxy.js
index f4a0ae6bfb..f5e1ce8de9 100644
--- a/src/lib/network/connectionProxy.js
+++ b/src/lib/network/connectionProxy.js
@@ -186,3 +186,57 @@ export function getProxyHash(providerSpecificData = {}) {
if (poolId) return `pool-${djb2(poolId)}`;
return "direct";
}
+
+/**
+ * ---------------------------------------------------------------------------
+ * Proxy Rotation Strategy
+ * ---------------------------------------------------------------------------
+ *
+ * Module-level state tracking for round-robin rotation.
+ * Maps providerId -> { currentIndex, poolHash }
+ * When the pool list changes (pools added/removed), the hash changes and the
+ * cursor resets automatically.
+ */
+
+const _roundRobinState = new Map();
+
+function computePoolHash(poolIds) {
+ return [...poolIds].sort().join(",");
+}
+
+/**
+ * Pick a proxy pool ID from the available pool list using the configured
+ * rotation strategy.
+ *
+ * @param {string[]} poolIds - Active proxy pool IDs to choose from.
+ * @param {string} strategy - "random" or "round-robin".
+ * @param {string} providerId - Provider identifier (state key, each
+ * provider rotates independently).
+ * @returns {string|null} Selected proxy pool ID, or null if poolIds empty.
+ */
+export function pickProxyPoolId(poolIds, strategy, providerId) {
+ if (!Array.isArray(poolIds) || poolIds.length === 0) return null;
+
+ if (strategy === "random") {
+ const idx = Math.floor(Math.random() * poolIds.length);
+ return poolIds[idx];
+ }
+
+ if (strategy === "round-robin") {
+ const currentHash = computePoolHash(poolIds);
+ const prev = _roundRobinState.get(providerId);
+
+ // Reset cursor when pool composition changes (pools added/removed)
+ if (!prev || prev.poolHash !== currentHash) {
+ _roundRobinState.set(providerId, { currentIndex: 0, poolHash: currentHash });
+ return poolIds[0];
+ }
+
+ const nextIndex = (prev.currentIndex + 1) % poolIds.length;
+ _roundRobinState.set(providerId, { currentIndex: nextIndex, poolHash: currentHash });
+ return poolIds[nextIndex];
+ }
+
+ // Unknown strategy — fall back to first pool
+ return poolIds[0];
+}
diff --git a/src/shared/components/NoAuthProxyCard.js b/src/shared/components/NoAuthProxyCard.js
index 34fd4f4393..920cd68eab 100644
--- a/src/shared/components/NoAuthProxyCard.js
+++ b/src/shared/components/NoAuthProxyCard.js
@@ -7,9 +7,10 @@ import Badge from "./Badge";
const NONE_PROXY_POOL_VALUE = "__none__";
-export default function NoAuthProxyCard({ providerId }) {
+export default function NoAuthProxyCard({ providerId, isFreeNoAuth = true }) {
const [proxyPools, setProxyPools] = useState([]);
const [proxyPoolId, setProxyPoolId] = useState(NONE_PROXY_POOL_VALUE);
+ const [rotateStrategy, setRotateStrategy] = useState("none");
const [saving, setSaving] = useState(false);
const [savedFlash, setSavedFlash] = useState(false);
@@ -23,23 +24,41 @@ export default function NoAuthProxyCard({ providerId }) {
setProxyPools(poolData.proxyPools || []);
const override = (settingsData.providerStrategies || {})[providerId] || {};
setProxyPoolId(override.proxyPoolId || NONE_PROXY_POOL_VALUE);
+ setRotateStrategy(override.rotateStrategy || "none");
}).catch(() => {});
return () => controller.abort();
}, [providerId]);
- const handleChange = async (newValue) => {
- setProxyPoolId(newValue);
+ const handleSave = async (updatedFields) => {
setSaving(true);
try {
const res = await fetch("/api/settings", { cache: "no-store" });
const data = res.ok ? await res.json() : {};
const current = data.providerStrategies || {};
const override = { ...(current[providerId] || {}) };
- if (newValue === NONE_PROXY_POOL_VALUE) delete override.proxyPoolId;
- else override.proxyPoolId = newValue;
+
+ if ("rotateStrategy" in updatedFields) {
+ if (updatedFields.rotateStrategy === "none") {
+ delete override.rotateStrategy;
+ } else {
+ override.rotateStrategy = updatedFields.rotateStrategy;
+ }
+ }
+ if ("proxyPoolId" in updatedFields) {
+ if (updatedFields.proxyPoolId === NONE_PROXY_POOL_VALUE) {
+ delete override.proxyPoolId;
+ } else {
+ override.proxyPoolId = updatedFields.proxyPoolId;
+ }
+ }
+
const updated = { ...current };
- if (Object.keys(override).length === 0) delete updated[providerId];
- else updated[providerId] = override;
+ if (Object.keys(override).length === 0) {
+ delete updated[providerId];
+ } else {
+ updated[providerId] = override;
+ }
+
await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
@@ -48,35 +67,77 @@ export default function NoAuthProxyCard({ providerId }) {
setSavedFlash(true);
setTimeout(() => setSavedFlash(false), 1500);
} catch (e) {
- console.log("Save proxyPoolId error:", e);
+ console.log("Save proxy override error:", e);
} finally {
setSaving(false);
}
};
+ const handleStrategyChange = (newVal) => {
+ setRotateStrategy(newVal);
+ handleSave({ rotateStrategy: newVal });
+ };
+
+ const handlePoolChange = (newVal) => {
+ setProxyPoolId(newVal);
+ handleSave({ proxyPoolId: newVal });
+ };
+
return (
- lock_open
+ lan
-
No authentication required
-
This provider is ready to use. Optionally route requests through a proxy pool to bypass IP-based limits.
+
Proxy Routing & Strategy
+
+ Configure how traffic for this provider is routed through proxy pools.
+
{savedFlash &&
Saved}
-
);
}
-
diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js
index 1c71f3b9c3..a928fc3d62 100644
--- a/src/sse/services/auth.js
+++ b/src/sse/services/auth.js
@@ -178,7 +178,22 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
connection = availableConnections[0];
}
- const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
+ // Proxy rotation — override per-connection proxy pool with dynamically selected
+ // pool when the provider has a rotation strategy configured (round-robin/random).
+ const proxyOverride = (settings.providerStrategies || {})[providerId] || {};
+ const proxyStrategy = proxyOverride.rotateStrategy || "none";
+ let rotProxyPoolId = connection.providerSpecificData?.proxyPoolId || null;
+ if (proxyStrategy !== "none") {
+ const allPools = await getProxyPools({ isActive: true });
+ const poolIds = allPools.filter(p => p.proxyUrl).map(p => p.id);
+ if (poolIds.length > 0) {
+ rotProxyPoolId = pickProxyPoolId(poolIds, proxyStrategy, providerId);
+ }
+ }
+ const resolvedProxy = await resolveConnectionProxyConfig({
+ ...(connection.providerSpecificData || {}),
+ proxyPoolId: rotProxyPoolId || "",
+ });
return {
authType: connection.authType,