feat(proxy-fitness): pool fitness registry + Smart rotation + pool-scoped retry

Pool/IP fitness registry (globalThis-backed, provider::model scopes, 5-min
cooldown, provider::* wildcard) fed by pool-scoped failures: freebuff
limited-IP / model-locked gates and opencode free per-IP limits. chatCore
retries a failed pool via another pool without locking the account; new
Smart rotation strategy (per-connection + no-auth providers) skips unfit
pools. Proxy Fitness dashboard page: active-block table with provider/IP
filters, per-record Clear and provider-scoped Clear All; egress column via
pool geo enrichment. Unit tests for the registry.
This commit is contained in:
MUH. IQRAM BAHRING
2026-08-07 03:51:19 +08:00
parent 90ff8191ff
commit fd22290a34
17 changed files with 1000 additions and 35 deletions
@@ -43,7 +43,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
}
if (selectedProxyIds.length > 1) {
const strategyLabel = rotationStrategy === "random" ? "Random" : rotationStrategy === "round-robin" ? "Round Robin" : rotationStrategy === "failover" ? "Failover" : "Multiple";
const strategyLabel = rotationStrategy === "random" ? "Random" : rotationStrategy === "round-robin" ? "Round Robin" : rotationStrategy === "failover" ? "Failover" : rotationStrategy === "smart" ? "Smart" : "Multiple";
return `${selectedProxyIds.length} pools (${strategyLabel})`;
}
@@ -318,7 +318,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
<span className="text-[10px] leading-tight">Proxy</span>
</button>
{showProxyDropdown && (
<div className="absolute right-0 top-full z-50 mt-1 max-w-[90vw] min-w-[280px] rounded-lg border border-border bg-bg shadow-lg">
<div className="absolute left-0 top-full z-50 mt-1 max-w-[calc(100vw-2rem)] min-w-[280px] rounded-lg border border-border bg-bg shadow-lg sm:left-auto sm:right-0">
{/* Rotation Strategy Selector */}
<div className="border-b border-border p-3">
<label className="block text-xs font-medium text-text-muted mb-2">Rotation Strategy</label>
@@ -331,18 +331,20 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
<option value="random">Random</option>
<option value="round-robin">Round Robin</option>
<option value="failover">Failover</option>
<option value="smart">Smart</option>
</select>
{rotationStrategy !== "none" && (
<p className="mt-1 text-[10px] text-text-muted">
{rotationStrategy === "random" && "Randomly select proxy on each request"}
{rotationStrategy === "round-robin" && "Rotate proxies in order across requests"}
{rotationStrategy === "failover" && "Try next proxy on failure"}
{rotationStrategy === "smart" && "Skip pools whose egress IP is blocked for this provider/model (e.g. Freebuff limited-IP). See Proxy Fitness to clear/block."}
</p>
)}
</div>
{/* Proxy Pool Selection */}
<div className="max-h-[60vh] overflow-y-auto py-1">
<div className="max-h-[200px] overflow-y-auto py-1">
<button
onClick={() => {
setSelectedProxyIds([]);
@@ -0,0 +1,384 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Badge, Button, Card, CardSkeleton, Input, ConfirmModal } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import { useNotificationStore } from "@/store/notificationStore";
function fmtTime(value) {
if (!value) return "-";
const d = new Date(value);
return Number.isNaN(d.getTime()) ? "-" : d.toLocaleTimeString();
}
// "freebuff::openai/gpt-5.6-luna" -> { provider: "freebuff", model: "openai/gpt-5.6-luna" }
// "freebuff::*" -> { provider: "freebuff", model: null }
function parseScope(scope) {
const sep = String(scope || "").indexOf("::");
if (sep < 0) return { provider: String(scope || ""), model: null };
const provider = scope.slice(0, sep);
const model = scope.slice(sep + 2);
return { provider, model: model === "*" || model === "" ? null : model };
}
function maskProxyUrl(url) {
try {
const parsed = new URL(url);
const host = parsed.hostname || "";
const port = parsed.port ? `:${parsed.port}` : "";
return `${parsed.protocol}//${host}${port}`;
} catch {
return String(url || "");
}
}
// Flatten fitness snapshot into one record per (pool, scope); drops expired marks.
function buildRecords(fitness, pools, now = Date.now()) {
const poolById = new Map((pools || []).map((p) => [p.id, p]));
const records = [];
for (const [poolId, byScope] of Object.entries(fitness || {})) {
const pool = poolById.get(poolId);
for (const [scope, info] of Object.entries(byScope || {})) {
const until = info?.until ? new Date(info.until).getTime() : 0;
if (!until || until <= now) continue;
const { provider, model } = parseScope(scope);
records.push({
poolId,
scope,
provider,
model,
until,
reason: info?.reason || "blocked",
poolName: pool?.name || poolId.slice(0, 8),
proxyUrl: pool?.proxyUrl || "",
egress: pool?.egress || null,
});
}
}
return records;
}
export default function ProxyFitnessPage() {
const [pools, setPools] = useState([]);
const [fitness, setFitness] = useState({});
const [loading, setLoading] = useState(true);
const [providerFilter, setProviderFilter] = useState("all");
const [search, setSearch] = useState("");
const [providerMenuOpen, setProviderMenuOpen] = useState(false);
const [providerMenuDropUp, setProviderMenuDropUp] = useState(false);
const [clearingScope, setClearingScope] = useState(null); // poolId::scope while clearing
const [confirmClearAll, setConfirmClearAll] = useState(false);
const [clearingAll, setClearingAll] = useState(false);
const providerMenuRef = useRef(null);
const notify = useNotificationStore();
useEffect(() => {
const handleClickOutside = (e) => {
if (providerMenuRef.current && !providerMenuRef.current.contains(e.target)) {
setProviderMenuOpen(false);
}
};
if (providerMenuOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [providerMenuOpen]);
const fetchAll = useCallback(async () => {
try {
const [poolRes, fitRes] = await Promise.all([
fetch("/api/proxy-pools?includeUsage=true", { cache: "no-store" }),
fetch("/api/proxy-pools/fitness", { cache: "no-store" }),
]);
const poolData = await poolRes.json().catch(() => ({ proxyPools: [] }));
setPools(poolData.proxyPools || []);
if (fitRes.ok) {
const fitData = await fitRes.json().catch(() => ({}));
setFitness(fitData.pools || fitData.fitness || {});
} else {
setFitness({});
}
} catch (error) {
console.log("Error fetching proxy fitness:", error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const records = useMemo(() => buildRecords(fitness, pools), [fitness, pools]);
// Providers present in the fitness data (filter options)
const providerOptions = useMemo(() => {
const set = new Set();
for (const rec of records) {
if (rec.provider) set.add(rec.provider);
}
return Array.from(set).sort();
}, [records]);
const filteredRecords = useMemo(() => {
const q = search.trim().toLowerCase();
return records.filter((rec) => {
if (providerFilter !== "all" && rec.provider !== providerFilter) return false;
if (q) {
const hay = [rec.proxyUrl, rec.egress?.ip, rec.egress?.country, rec.egress?.region, rec.poolName]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
}, [records, providerFilter, search]);
const handleClear = async (rec) => {
setClearingScope(rec.scope);
try {
const res = await fetch(`/api/proxy-pools/${rec.poolId}/fitness/clear`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scope: rec.scope }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
notify.success(`Cleared ${rec.scope}`);
fetchAll();
} catch (err) {
notify.error(`Clear failed: ${err.message}`);
} finally {
setClearingScope(null);
}
};
const handleClearAll = async () => {
setClearingAll(true);
try {
const res = await fetch("/api/proxy-pools/fitness/clear-all", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(providerFilter !== "all" ? { provider: providerFilter } : {}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
notify.success(providerFilter !== "all" ? `Cleared all ${providerFilter} blocks` : "Cleared all blocks");
setConfirmClearAll(false);
fetchAll();
} catch (err) {
notify.error(`Clear all failed: ${err.message}`);
} finally {
setClearingAll(false);
}
};
const selectedProviderLabel = providerFilter === "all" ? "All providers" : providerFilter;
// Open the provider menu as an overlay anchored to its container. Flips to
// drop-up when there isn't enough viewport space below (menu max-h ~288px).
const toggleProviderMenu = () => {
if (providerMenuOpen) {
setProviderMenuOpen(false);
return;
}
const el = providerMenuRef.current;
if (el) {
const rect = el.getBoundingClientRect();
setProviderMenuDropUp(window.innerHeight - rect.bottom < 300);
}
setProviderMenuOpen(true);
};
return (
<div className="mx-auto w-full max-w-5xl space-y-4 p-4 sm:p-6">
{loading ? (
<CardSkeleton />
) : (
<>
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-text-main">Proxy Fitness</h1>
<Badge variant={records.length > 0 ? "error" : "default"} size="sm">
{records.length} active block{records.length === 1 ? "" : "s"}
</Badge>
</div>
<div className="flex items-center gap-2">
{records.length > 0 && (
<Button
variant="danger"
size="sm"
icon="delete_sweep"
onClick={() => setConfirmClearAll(true)}
disabled={clearingAll}
>
Clear All{providerFilter !== "all" ? ` (${providerFilter})` : ""}
</Button>
)}
<Button variant="secondary" size="sm" icon="refresh" onClick={fetchAll}>Refresh</Button>
</div>
</div>
<p className="text-sm text-text-muted">
Shows which provider is blocked on which proxy IP (from provider region gates, per-IP
limits, or manual marks). Smart rotation skips these pools while the block is active.
</p>
{/* Filters */}
<div className="flex flex-wrap items-end gap-3 rounded-lg border border-border-subtle bg-surface p-3">
<div className="relative flex w-64 max-w-full flex-col" ref={providerMenuRef}>
<label className="mb-1 text-xs font-medium text-text-muted">Provider</label>
<button
type="button"
onClick={toggleProviderMenu}
className="flex h-8 w-full items-center justify-between gap-1 rounded-lg border border-black/10 bg-black/[0.02] px-2 text-xs text-text-main transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10"
aria-haspopup="menu"
aria-expanded={providerMenuOpen}
title="Filter by provider"
>
<span className="flex min-w-0 items-center gap-1.5">
{providerFilter === "all" ? (
<span className="material-symbols-outlined text-[14px] text-text-muted">apps</span>
) : (
<ProviderIcon providerId={providerFilter} size={18} className="size-[18px] rounded object-contain" fallbackText={providerFilter.slice(0, 2).toUpperCase()} />
)}
<span className="truncate capitalize">{selectedProviderLabel}</span>
</span>
<span className="material-symbols-outlined text-[14px] text-text-muted">expand_more</span>
</button>
{providerMenuOpen && (
<div className={`absolute left-0 z-20 w-full max-h-72 overflow-y-auto rounded-lg border border-border-subtle bg-surface p-1 shadow-lg ${providerMenuDropUp ? "bottom-full mb-1" : "top-full mt-1"}`}>
<button
type="button"
onClick={() => { setProviderFilter("all"); setProviderMenuOpen(false); }}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === "all" ? "bg-primary/10 text-primary" : "text-text-main hover:bg-black/5 dark:hover:bg-white/10"}`}
>
<span className="material-symbols-outlined text-[22px]">apps</span>
<span className="font-medium">All providers</span>
{providerFilter === "all" && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
</button>
<div className="my-1 h-px bg-black/10 dark:bg-white/10" />
{providerOptions.map((provider) => (
<button
key={provider}
type="button"
onClick={() => { setProviderFilter(provider); setProviderMenuOpen(false); }}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === provider ? "bg-primary/10 text-primary" : "text-text-main hover:bg-black/5 dark:hover:bg-white/10"}`}
>
<ProviderIcon providerId={provider} size={24} className="size-6 rounded-md object-contain" fallbackText={provider.slice(0, 2).toUpperCase()} />
<span className="font-medium capitalize">{provider}</span>
{providerFilter === provider && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
</button>
))}
</div>
)}
</div>
<div className="flex min-w-48 flex-1 flex-col">
<label className="mb-1 text-xs font-medium text-text-muted">IP / Proxy / Pool</label>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="e.g. 104.28, vercel-relay…"
icon="search"
/>
</div>
</div>
{/* Records table */}
<Card className="p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[700px] border-collapse text-sm">
<thead>
<tr className="border-b border-border-subtle text-left text-xs uppercase tracking-wider text-text-muted">
<th className="px-4 py-2 font-semibold">Provider</th>
<th className="px-4 py-2 font-semibold">Model</th>
<th className="px-4 py-2 font-semibold">IP / Proxy</th>
<th className="px-4 py-2 font-semibold">Pool</th>
<th className="px-4 py-2 font-semibold">Reason</th>
<th className="px-4 py-2 font-semibold">Until</th>
<th className="px-4 py-2 text-right font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{filteredRecords.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-text-muted">
{records.length === 0
? "No active blocks. Blocks appear here when a provider region-gates a proxy IP."
: "No blocks match the current filters."}
</td>
</tr>
) : (
filteredRecords.map((rec) => (
<tr key={`${rec.poolId}::${rec.scope}`} className="border-b border-border-subtle last:border-0 align-top hover:bg-surface-2/40">
<td className="px-4 py-3">
<span className="inline-flex items-center gap-2 rounded bg-red-500/10 px-2 py-0.5 text-xs font-medium text-red-600 dark:text-red-400">
<span className="material-symbols-outlined text-[14px]">block</span>
<span className="capitalize">{rec.provider}</span>
</span>
</td>
<td className="max-w-56 px-4 py-3">
<span className="truncate text-text-main">{rec.model || <em className="text-text-muted">all models</em>}</span>
</td>
<td className="px-4 py-3">
<div className="flex min-w-0 flex-col gap-0.5">
<code className="truncate font-mono text-xs text-text-main">{maskProxyUrl(rec.proxyUrl)}</code>
{rec.egress?.ip && (
<span className="flex items-center gap-1 text-[11px] text-text-muted">
<span className="truncate">egress {rec.egress.ip}{rec.egress.country ? ` · ${rec.egress.country}` : ""}</span>
{rec.egress.isUnstable && (
<span
className="inline-flex shrink-0 items-center gap-0.5 rounded bg-amber-500/15 px-1 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400"
title={rec.egress.ipCount >= 2 ? `Egress IP changed ${rec.egress.ipCount}× — relay egress is not stable` : "Egress is unstable"}
>
unstable
</span>
)}
</span>
)}
</div>
</td>
<td className="px-4 py-3 text-text-muted">{rec.poolName}</td>
<td className="px-4 py-3 text-text-muted">{rec.reason}</td>
<td className="px-4 py-3 text-text-muted">{fmtTime(rec.until)}</td>
<td className="px-4 py-3">
<div className="flex justify-end">
<Button
variant="ghost"
size="sm"
icon="close"
onClick={() => handleClear(rec)}
disabled={clearingScope === rec.scope}
title="Clear this block"
>
{clearingScope === rec.scope ? "Clearing..." : "Clear"}
</Button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</>
)}
<ConfirmModal
isOpen={confirmClearAll}
onClose={() => setConfirmClearAll(false)}
onConfirm={handleClearAll}
title="Clear all blocks"
message={providerFilter !== "all"
? `Clear all active blocks for provider "${providerFilter}"? This lets those pools be selected again immediately.`
: "Clear all active proxy blocks? This lets all pools be selected again immediately."}
confirmText={clearingAll ? "Clearing..." : "Clear All"}
cancelText="Cancel"
variant="danger"
/>
</div>
);
}
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { clearPoolUnfit } from "open-sse/services/proxyPoolFitness.js";
// POST /api/proxy-pools/[id]/fitness/clear
// Body: { scope: "provider::model" } — clears the mark for this pool + scope.
export async function POST(request, { params }) {
try {
const { id } = await params;
let body = {};
try {
body = await request.json();
} catch {
body = {};
}
const scope = typeof body?.scope === "string" ? body.scope.trim() : "";
if (!id || !scope) {
return NextResponse.json({ error: "pool id and scope are required" }, { status: 400 });
}
clearPoolUnfit(id, scope);
return NextResponse.json({ ok: true, poolId: id, scope });
} catch (error) {
console.log("Error clearing pool fitness:", error);
return NextResponse.json({ error: "Failed to clear pool fitness" }, { status: 500 });
}
}
@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { clearAllPoolUnfit } from "open-sse/services/proxyPoolFitness.js";
// POST /api/proxy-pools/fitness/clear-all
// Body: { provider?: string } — clears every mark, or only marks scoped to the
// given provider ("provider::*") when provided.
export async function POST(request) {
try {
let body = {};
try {
body = await request.json();
} catch {
body = {};
}
const provider = typeof body?.provider === "string" && body.provider.trim() ? body.provider.trim() : null;
clearAllPoolUnfit(provider);
return NextResponse.json({ ok: true, provider: provider || null });
} catch (error) {
console.log("Error clearing proxy fitness:", error);
return NextResponse.json({ error: "Failed to clear proxy fitness" }, { status: 500 });
}
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { poolFitnessSnapshot } from "open-sse/services/proxyPoolFitness.js";
// GET /api/proxy-pools/fitness — in-memory snapshot of pool fitness marks.
// Returns { pools: { [poolId]: { [scope]: { until, reason } } } }.
export async function GET() {
try {
return NextResponse.json({ pools: poolFitnessSnapshot() });
} catch (error) {
console.log("Error reading proxy fitness:", error);
return NextResponse.json({ error: "Failed to read proxy fitness" }, { status: 500 });
}
}
+3
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createProxyPool, getProviderConnections, getProxyPools } from "@/models";
import { getPoolGeo } from "open-sse/services/poolGeo.js";
function toBoolean(value) {
if (value === "true") return true;
@@ -65,6 +66,8 @@ export async function GET(request) {
const enrichedProxyPools = proxyPools.map((pool) => ({
...pool,
boundConnectionCount: usageMap.get(pool.id) || 0,
// Egress geo from the background probe cache (null until first probe).
egress: getPoolGeo(pool.id) || null,
}));
return NextResponse.json({ proxyPools: enrichedProxyPools });
+29 -9
View File
@@ -1,4 +1,5 @@
import { getProxyPoolById } from "@/models";
import { fitPoolIds } from "open-sse/services/proxyPoolFitness.js";
// Safely normalize any value into a trimmed string.
function normalizeString(value) {
@@ -13,24 +14,41 @@ const rotateState = new Map(); // providerId → { index }
* Pick one proxy pool ID from a list based on strategy.
* round-robin: cycle sequentially (in-memory, resets on restart)
* random: uniform random pick
* smart: region-aware — skip pools unfit for `scope`, round-robin on the fit subset
* none/single: return first entry
* @param {string[]} poolIds
* @param {string} strategy
* @param {string} providerId
* @param {{ scope?: string, excludeIds?: string[] }} [opts]
*/
export function pickProxyPoolId(poolIds, strategy, providerId) {
export function pickProxyPoolId(poolIds, strategy, providerId, opts = {}) {
if (!poolIds || poolIds.length === 0) return null;
if (poolIds.length === 1) return poolIds[0];
const { scope = null, excludeIds = [] } = opts || {};
if (strategy === "round-robin") {
let eligible = poolIds.filter((id) => !(excludeIds || []).includes(id));
// Region-aware filtering is opt-in via the "smart" strategy.
if (strategy === "smart" && scope) eligible = fitPoolIds(eligible, scope);
if (eligible.length === 0) {
// Every candidate is unfit/excluded — fall back to the first non-excluded
// pool rather than deadlocking; its egress may have recovered.
eligible = poolIds.filter((id) => !(excludeIds || []).includes(id));
if (eligible.length === 0) return null;
}
if (eligible.length === 1) return eligible[0];
if (strategy === "round-robin" || strategy === "smart") {
const state = rotateState.get(providerId) || { index: -1 };
state.index = (state.index + 1) % poolIds.length;
state.index = (state.index + 1) % eligible.length;
rotateState.set(providerId, state);
return poolIds[state.index];
return eligible[state.index];
}
if (strategy === "random") {
return poolIds[Math.floor(Math.random() * poolIds.length)];
return eligible[Math.floor(Math.random() * eligible.length)];
}
return poolIds[0]; // "none" or unknown
return eligible[0]; // "none" or unknown
}
/**
@@ -66,7 +84,8 @@ function normalizeLegacyProxy(providerSpecificData = {}) {
*/
export async function resolveConnectionProxyConfig(
providerSpecificData = {},
connectionId = null
connectionId = null,
excludePoolIds = null
) {
try {
// Handle new multi-proxy format
@@ -85,7 +104,8 @@ export async function resolveConnectionProxyConfig(
* -----------------------------
*/
if (proxyPoolIds.length > 0) {
const selectedPoolId = pickProxyPoolId(proxyPoolIds, proxyRotationStrategy, connectionId);
const scope = providerSpecificData?.proxyPoolScope || null;
const selectedPoolId = pickProxyPoolId(proxyPoolIds, proxyRotationStrategy, connectionId, { scope, excludeIds: excludePoolIds });
if (selectedPoolId) {
const proxyPool = await getProxyPoolById(selectedPoolId);
+4 -1
View File
@@ -11,6 +11,7 @@ const STRATEGIES = [
{ value: "none", label: "None (single pool)" },
{ value: "round-robin", label: "Round-robin" },
{ value: "random", label: "Random" },
{ value: "smart", label: "Smart" },
];
export default function NoAuthProxyCard({ providerId }) {
@@ -121,7 +122,9 @@ export default function NoAuthProxyCard({ providerId }) {
: isRotation
? rotateStrategy === "round-robin"
? `Rotating through all ${proxyPools.length} active pools in order. State is in-memory (resets on restart).`
: `Picking a random pool from ${proxyPools.length} active pools each request.`
: rotateStrategy === "smart"
? `Skip pools whose egress IP is blocked for this provider (e.g. per-IP limits). See Proxy Fitness to clear/block.`
: `Picking a random pool from ${proxyPools.length} active pools each request.`
: `Uses the selected pool above. Set to Round-robin or Random to rotate across all active pools.`}
</p>
</div>
+1
View File
@@ -25,6 +25,7 @@ const navItems = [
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" },
{ href: "/dashboard/proxy-fitness", label: "Proxy Fitness", icon: "network_check" },
// { href: "/dashboard/pxpipe", label: "PXPIPE", icon: "image" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
];
+18
View File
@@ -17,6 +17,7 @@ import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { handleComboChat, handleFusionChat, detectRequiredCapabilities } from "open-sse/services/combo.js";
import { augmentModelsWithCapacityAdapter, withCapacityAdapterStripping, getActiveAdapterStrategy } from "open-sse/services/capacityAdapter.js";
import { handleBypassRequest } from "open-sse/utils/bypassHandler.js";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
import { detectFormatByEndpoint } from "open-sse/translator/formats.js";
import * as log from "../utils/logger.js";
@@ -284,6 +285,23 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
pxpipeTransform: chatSettings.pxpipeEnabled ? await getPxpipeTransform() : null,
onPxpipeEvent: appendPxpipeEvent,
providerThinking,
// Pool-scoped failure recovery: re-resolve proxy config excluding the
// failed pool so the request retries via another pool, not a dead end.
resolveProxyConfig: async (creds, excludePoolIds = []) => {
const psd = { ...(creds?.providerSpecificData || {}) };
if (psd.proxyPoolIds?.length) psd.proxyPoolScope = `${provider}::${model}`;
const resolved = await resolveConnectionProxyConfig(psd, creds?.connectionId || creds?.id, excludePoolIds);
if (!resolved?.proxyPoolId) return null;
return {
connectionProxyEnabled: resolved.connectionProxyEnabled,
connectionProxyUrl: resolved.connectionProxyUrl,
connectionNoProxy: resolved.connectionNoProxy,
connectionProxyPoolId: resolved.proxyPoolId || null,
vercelRelayUrl: resolved.vercelRelayUrl || "",
proxyPoolId: resolved.proxyPoolId || null,
strictProxy: resolved.strictProxy === true,
};
},
// Detect source format by endpoint + body
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
onCredentialsRefreshed: async (newCreds) => {
+22 -3
View File
@@ -47,10 +47,16 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
const override = (settings.providerStrategies || {})[providerId] || {};
const strategy = override.rotateStrategy || "none";
let pickedId = override.proxyPoolId || null;
let poolIds = [];
if (strategy !== "none") {
const allPools = await getProxyPools({ isActive: true });
const poolIds = allPools.filter(p => p.proxyUrl).map(p => p.id);
pickedId = pickProxyPoolId(poolIds, strategy, providerId);
poolIds = allPools.filter(p => p.proxyUrl).map(p => p.id);
// Scope region-aware ("smart") filtering to this provider/model so
// pools marked unfit here are skipped.
const scope = `${providerId}::${model || "*"}`;
pickedId = pickProxyPoolId(poolIds, strategy, providerId, { scope });
} else if (override.proxyPoolId) {
poolIds = [override.proxyPoolId];
}
const resolvedProxy = await resolveConnectionProxyConfig({ proxyPoolId: pickedId || "" });
return {
@@ -64,6 +70,13 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
connectionNoProxy: resolvedProxy.connectionNoProxy,
connectionProxyPoolId: resolvedProxy.proxyPoolId || null,
vercelRelayUrl: resolvedProxy.vercelRelayUrl || "",
proxyPoolId: resolvedProxy.proxyPoolId || null,
strictProxy: resolvedProxy.strictProxy === true,
// Let chatCore's pool-scoped retry rotate across the same candidate
// pool set (excluding the failed pool) instead of reusing it — this
// is what makes per-IP limit retries work for no-auth providers.
proxyPoolIds: poolIds,
proxyRotationStrategy: strategy,
},
};
}
@@ -172,7 +185,11 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
connection = availableConnections[0];
}
const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
// Scope the region-aware picker to this provider/model (e.g. freebuff::gpt-5.6-luna)
const psdForProxy = connection.providerSpecificData?.proxyPoolIds?.length
? { ...connection.providerSpecificData, proxyPoolScope: `${providerId}::${model || ""}` }
: connection.providerSpecificData;
const resolvedProxy = await resolveConnectionProxyConfig(psdForProxy || {}, connection.id);
return {
authType: connection.authType,
@@ -193,6 +210,8 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
connectionNoProxy: resolvedProxy.connectionNoProxy,
connectionProxyPoolId: resolvedProxy.proxyPoolId || null,
vercelRelayUrl: resolvedProxy.vercelRelayUrl || "",
proxyPoolId: resolvedProxy.proxyPoolId || null,
strictProxy: resolvedProxy.strictProxy === true,
},
connectionId: connection.id,
// Include current status for optimization check