chore: prepare fork docker deployment
This commit is contained in:
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository }}
|
||||
DOCKERHUB_IMAGE: decolua/9router
|
||||
DOCKERHUB_IMAGE: mhiqrambhrng/9router-mibp-version
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
9router:
|
||||
image: decolua/9router:latest
|
||||
image: mhiqrambhrng/9router-mibp-version:latest
|
||||
container_name: 9router
|
||||
restart: always
|
||||
ports:
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
website: "https://freebuff.com",
|
||||
notice: {
|
||||
signupUrl: "https://freebuff.com",
|
||||
text: "Free ad-supported coding agent by Codebuff. Sign in with your Freebuff/Codebuff account via browser login. Free tier is ad-supported and limited in some regions (limited mode: 6 x 1-hour sessions/day); full mode runs in select countries.",
|
||||
text: "Free ad-supported coding agent by Codebuff. Sign in with your Freebuff/Codebuff account via browser login. Free tier is ad-supported and limited in some regions (limited mode: 6 x 1-hour sessions/day); full mode runs in select countries. ⚠️ One account has ONE active session locked to ONE model — requesting a different model while a session is active returns 'model_locked' (409); use a separate account per model, or wait for the session to expire.",
|
||||
},
|
||||
},
|
||||
category: "free",
|
||||
|
||||
@@ -9,27 +9,72 @@ import CooldownTimer from "./CooldownTimer";
|
||||
export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onUpdateProxy, onEdit, onDelete, oneByOneStatus = null, autoPing = null }) {
|
||||
const [showProxyDropdown, setShowProxyDropdown] = useState(false);
|
||||
const [updatingProxy, setUpdatingProxy] = useState(false);
|
||||
const [selectedProxyIds, setSelectedProxyIds] = useState([]);
|
||||
const [rotationStrategy, setRotationStrategy] = useState("none");
|
||||
const proxyDropdownRef = useRef(null);
|
||||
|
||||
// Initialize proxy state from connection
|
||||
useEffect(() => {
|
||||
const proxyPoolIds = connection.providerSpecificData?.proxyPoolIds || [];
|
||||
const legacyProxyPoolId = connection.providerSpecificData?.proxyPoolId;
|
||||
|
||||
// Migrate legacy single proxy to array format
|
||||
if (legacyProxyPoolId && proxyPoolIds.length === 0) {
|
||||
setSelectedProxyIds([legacyProxyPoolId]);
|
||||
} else {
|
||||
setSelectedProxyIds(proxyPoolIds);
|
||||
}
|
||||
|
||||
setRotationStrategy(connection.providerSpecificData?.proxyRotationStrategy || "none");
|
||||
}, [connection]);
|
||||
|
||||
const proxyPoolMap = new Map((proxyPools || []).map((pool) => [pool.id, pool]));
|
||||
const boundProxyPoolId = connection.providerSpecificData?.proxyPoolId || null;
|
||||
const boundProxyPool = boundProxyPoolId ? proxyPoolMap.get(boundProxyPoolId) : null;
|
||||
|
||||
// Display logic - support both new (multi-proxy) and legacy (single proxy) formats
|
||||
const hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl;
|
||||
const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy;
|
||||
const proxyDisplayText = boundProxyPool
|
||||
? `Pool: ${boundProxyPool.name}`
|
||||
: boundProxyPoolId
|
||||
? `Pool: ${boundProxyPoolId} (inactive/missing)`
|
||||
: hasLegacyProxy
|
||||
? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`
|
||||
: "";
|
||||
const hasAnyProxy = selectedProxyIds.length > 0 || hasLegacyProxy;
|
||||
|
||||
const getProxyDisplayText = () => {
|
||||
if (selectedProxyIds.length === 0 && !hasLegacyProxy) return "";
|
||||
|
||||
if (selectedProxyIds.length === 1) {
|
||||
const pool = proxyPoolMap.get(selectedProxyIds[0]);
|
||||
return pool ? `Pool: ${pool.name}` : `Pool: ${selectedProxyIds[0]} (inactive/missing)`;
|
||||
}
|
||||
|
||||
if (selectedProxyIds.length > 1) {
|
||||
const strategyLabel = rotationStrategy === "random" ? "Random" : rotationStrategy === "round-robin" ? "Round Robin" : rotationStrategy === "failover" ? "Failover" : "Multiple";
|
||||
return `${selectedProxyIds.length} pools (${strategyLabel})`;
|
||||
}
|
||||
|
||||
if (hasLegacyProxy) {
|
||||
return `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const proxyDisplayText = getProxyDisplayText();
|
||||
const autoPingTooltip = autoPing?.provider === "codex"
|
||||
? "Auto-starts the next 5h Codex window after reset by sending a tiny gpt-5.5 request. Consumes a small amount of quota."
|
||||
: "When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away.";
|
||||
|
||||
let maskedProxyUrl = "";
|
||||
if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) {
|
||||
const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl;
|
||||
if (selectedProxyIds.length > 0) {
|
||||
const selectedPools = selectedProxyIds.map(id => proxyPoolMap.get(id)).filter(Boolean);
|
||||
if (selectedPools.length > 0) {
|
||||
try {
|
||||
const parsed = new URL(selectedPools[0].proxyUrl);
|
||||
maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
||||
if (selectedPools.length > 1) {
|
||||
maskedProxyUrl += ` (+${selectedPools.length - 1} more)`;
|
||||
}
|
||||
} catch {
|
||||
maskedProxyUrl = selectedPools[0].proxyUrl;
|
||||
}
|
||||
}
|
||||
} else if (connection.providerSpecificData?.connectionProxyUrl) {
|
||||
const rawProxyUrl = connection.providerSpecificData?.connectionProxyUrl;
|
||||
try {
|
||||
const parsed = new URL(rawProxyUrl);
|
||||
maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
||||
@@ -38,12 +83,15 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
}
|
||||
}
|
||||
|
||||
const noProxyText = boundProxyPool?.noProxy || connection.providerSpecificData?.connectionNoProxy || "";
|
||||
const noProxyText = selectedProxyIds.length > 0
|
||||
? proxyPoolMap.get(selectedProxyIds[0])?.noProxy || ""
|
||||
: connection.providerSpecificData?.connectionNoProxy || "";
|
||||
|
||||
let proxyBadgeVariant = "default";
|
||||
if (boundProxyPool?.isActive === true) {
|
||||
proxyBadgeVariant = "success";
|
||||
} else if (boundProxyPoolId || hasLegacyProxy) {
|
||||
if (selectedProxyIds.length > 0) {
|
||||
const allActive = selectedProxyIds.every(id => proxyPoolMap.get(id)?.isActive === true);
|
||||
proxyBadgeVariant = allActive ? "success" : "error";
|
||||
} else if (hasLegacyProxy) {
|
||||
proxyBadgeVariant = "error";
|
||||
}
|
||||
|
||||
@@ -59,10 +107,54 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [showProxyDropdown]);
|
||||
|
||||
const handleSelectProxy = async (poolId) => {
|
||||
const handleToggleProxySelection = (poolId) => {
|
||||
setSelectedProxyIds(prev => {
|
||||
if (prev.includes(poolId)) {
|
||||
return prev.filter(id => id !== poolId);
|
||||
} else {
|
||||
return [...prev, poolId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleStrategyChange = (strategy) => {
|
||||
setRotationStrategy(strategy);
|
||||
|
||||
// Auto-select all active proxy pools when switching to rotation strategies
|
||||
if (strategy !== "none" && selectedProxyIds.length === 0) {
|
||||
const activePoolIds = (proxyPools || [])
|
||||
.filter(pool => pool.isActive === true)
|
||||
.map(pool => pool.id);
|
||||
setSelectedProxyIds(activePoolIds);
|
||||
}
|
||||
|
||||
// Reset to single proxy when switching to "none"
|
||||
if (strategy === "none" && selectedProxyIds.length > 1) {
|
||||
setSelectedProxyIds(selectedProxyIds.slice(0, 1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyProxyChanges = async () => {
|
||||
setUpdatingProxy(true);
|
||||
try {
|
||||
await onUpdateProxy(poolId === "__none__" ? null : poolId);
|
||||
await onUpdateProxy({
|
||||
proxyPoolIds: selectedProxyIds,
|
||||
proxyRotationStrategy: rotationStrategy,
|
||||
});
|
||||
setShowProxyDropdown(false);
|
||||
} finally {
|
||||
setUpdatingProxy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectProxy = async (poolId) => {
|
||||
// Legacy single-proxy mode (backwards compatibility)
|
||||
setUpdatingProxy(true);
|
||||
try {
|
||||
await onUpdateProxy({
|
||||
proxyPoolIds: poolId === "__none__" ? [] : [poolId],
|
||||
proxyRotationStrategy: "none",
|
||||
});
|
||||
} finally {
|
||||
setUpdatingProxy(false);
|
||||
setShowProxyDropdown(false);
|
||||
@@ -226,22 +318,117 @@ 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-[78vw] min-w-[160px] rounded-lg border border-border bg-bg py-1 shadow-lg">
|
||||
<button
|
||||
onClick={() => handleSelectProxy("__none__")}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${!boundProxyPoolId ? "text-primary font-medium" : "text-text-main"}`}
|
||||
>
|
||||
None
|
||||
</button>
|
||||
{(proxyPools || []).map((pool) => (
|
||||
<button
|
||||
key={pool.id}
|
||||
onClick={() => handleSelectProxy(pool.id)}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${boundProxyPoolId === pool.id ? "text-primary font-medium" : "text-text-main"}`}
|
||||
<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">
|
||||
{/* 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>
|
||||
<select
|
||||
value={rotationStrategy}
|
||||
onChange={(e) => handleStrategyChange(e.target.value)}
|
||||
className="w-full rounded border border-border bg-bg px-2 py-1.5 text-sm text-text-main focus:border-primary focus:outline-none"
|
||||
>
|
||||
{pool.name}
|
||||
<option value="none">None (Single Proxy)</option>
|
||||
<option value="random">Random</option>
|
||||
<option value="round-robin">Round Robin</option>
|
||||
<option value="failover">Failover</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"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Proxy Pool Selection */}
|
||||
<div className="max-h-[60vh] overflow-y-auto py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedProxyIds([]);
|
||||
setRotationStrategy("none");
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${selectedProxyIds.length === 0 ? "text-primary font-medium" : "text-text-main"}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{selectedProxyIds.length === 0 ? "check_box" : "check_box_outline_blank"}
|
||||
</span>
|
||||
<span>None</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Select All Button (only for rotation strategies) */}
|
||||
{rotationStrategy !== "none" && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const activePoolIds = (proxyPools || [])
|
||||
.filter(pool => pool.isActive === true)
|
||||
.map(pool => pool.id);
|
||||
const allSelected = activePoolIds.length > 0 && activePoolIds.every(id => selectedProxyIds.includes(id));
|
||||
|
||||
if (allSelected) {
|
||||
setSelectedProxyIds([]);
|
||||
} else {
|
||||
setSelectedProxyIds(activePoolIds);
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm border-b border-border hover:bg-black/5 dark:hover:bg-white/5 ${selectedProxyIds.length === (proxyPools || []).filter(p => p.isActive).length ? "bg-black/5 dark:bg-white/5" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{selectedProxyIds.length === (proxyPools || []).filter(p => p.isActive).length && selectedProxyIds.length > 0
|
||||
? "check_box"
|
||||
: "check_box_outline_blank"
|
||||
}
|
||||
</span>
|
||||
<span className="font-medium">Select All Active</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(proxyPools || []).map((pool) => {
|
||||
const isSelected = selectedProxyIds.includes(pool.id);
|
||||
const isActive = pool.isActive === true;
|
||||
return (
|
||||
<button
|
||||
key={pool.id}
|
||||
onClick={() => {
|
||||
if (rotationStrategy === "none") {
|
||||
setSelectedProxyIds([pool.id]);
|
||||
} else {
|
||||
handleToggleProxySelection(pool.id);
|
||||
}
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${isSelected ? "bg-black/5 dark:bg-white/5" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{rotationStrategy === "none"
|
||||
? (isSelected ? "radio_button_checked" : "radio_button_unchecked")
|
||||
: (isSelected ? "check_box" : "check_box_outline_blank")
|
||||
}
|
||||
</span>
|
||||
<span className={isSelected ? "text-primary font-medium" : "text-text-main"}>{pool.name}</span>
|
||||
{!isActive && (
|
||||
<span className="ml-auto text-[10px] text-red-500">(inactive)</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Apply Button */}
|
||||
<div className="border-t border-border p-2">
|
||||
<button
|
||||
onClick={handleApplyProxyChanges}
|
||||
disabled={updatingProxy}
|
||||
className="w-full rounded bg-primary px-3 py-2 text-sm font-medium text-white hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{updatingProxy ? "Applying..." : "Apply"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -968,17 +968,39 @@ export default function ProviderDetailPage() {
|
||||
onToggle: (on) => handleAutoPingConnection(conn.id, on),
|
||||
provider: providerId,
|
||||
} : null}
|
||||
onUpdateProxy={async (proxyPoolId) => {
|
||||
onUpdateProxy={async (proxyConfig) => {
|
||||
try {
|
||||
// Support both new format (object) and legacy format (string/null)
|
||||
const updatePayload = typeof proxyConfig === 'object' && proxyConfig !== null
|
||||
? {
|
||||
proxyPoolIds: proxyConfig.proxyPoolIds || [],
|
||||
proxyRotationStrategy: proxyConfig.proxyRotationStrategy || "none",
|
||||
}
|
||||
: {
|
||||
// Legacy single-proxy format
|
||||
proxyPoolId: proxyConfig || null,
|
||||
};
|
||||
|
||||
const res = await fetch(`/api/providers/${conn.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ proxyPoolId: proxyPoolId || null }),
|
||||
body: JSON.stringify(updatePayload),
|
||||
});
|
||||
if (res.ok) {
|
||||
setConnections(prev => prev.map(c =>
|
||||
c.id === conn.id
|
||||
? { ...c, providerSpecificData: { ...c.providerSpecificData, proxyPoolId: proxyPoolId || null } }
|
||||
? {
|
||||
...c,
|
||||
providerSpecificData: {
|
||||
...c.providerSpecificData,
|
||||
...(updatePayload.proxyPoolIds !== undefined ? {
|
||||
proxyPoolIds: updatePayload.proxyPoolIds,
|
||||
proxyRotationStrategy: updatePayload.proxyRotationStrategy,
|
||||
} : {
|
||||
proxyPoolId: updatePayload.proxyPoolId,
|
||||
})
|
||||
}
|
||||
}
|
||||
: c
|
||||
));
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ export async function GET(request, { params }) {
|
||||
"codebuddy-intl",
|
||||
"qoder",
|
||||
"grok-cli",
|
||||
"freebuff",
|
||||
];
|
||||
let deviceData;
|
||||
if (noPkceDeviceProviders.includes(provider)) {
|
||||
|
||||
@@ -391,7 +391,7 @@ const PROVIDER_MODELS_CONFIG = {
|
||||
},
|
||||
"grok-cli": {
|
||||
customResolver: async (connection) => {
|
||||
const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
|
||||
const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
|
||||
const result = await resolveGrokCliModels({
|
||||
...connection,
|
||||
connectionId: connection.id,
|
||||
|
||||
@@ -33,7 +33,31 @@ function normalizeProxyConfig(body = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function normalizeProxyPoolUpdate(proxyPoolIdInput) {
|
||||
async function normalizeProxyPoolUpdate(body) {
|
||||
// Handle new multi-proxy format
|
||||
if (body.proxyPoolIds !== undefined) {
|
||||
const proxyPoolIds = Array.isArray(body.proxyPoolIds) ? body.proxyPoolIds : [];
|
||||
const proxyRotationStrategy = body.proxyRotationStrategy || "none";
|
||||
|
||||
// Validate all proxy pool IDs exist
|
||||
for (const poolId of proxyPoolIds) {
|
||||
if (poolId) {
|
||||
const pool = await getProxyPoolById(poolId);
|
||||
if (!pool) {
|
||||
return { hasProxyPoolField: true, error: `Proxy pool ${poolId} not found` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasProxyPoolField: true,
|
||||
proxyPoolIds: proxyPoolIds.filter(Boolean),
|
||||
proxyRotationStrategy,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle legacy single proxy format
|
||||
const proxyPoolIdInput = body.proxyPoolId;
|
||||
if (proxyPoolIdInput === undefined) {
|
||||
return { hasProxyPoolField: false, proxyPoolId: null };
|
||||
}
|
||||
@@ -111,7 +135,7 @@ export async function PUT(request, { params }) {
|
||||
return NextResponse.json({ error: proxyConfig.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const proxyPoolResult = await normalizeProxyPoolUpdate(body.proxyPoolId);
|
||||
const proxyPoolResult = await normalizeProxyPoolUpdate(body);
|
||||
if (proxyPoolResult.error) {
|
||||
return NextResponse.json({ error: proxyPoolResult.error }, { status: 400 });
|
||||
}
|
||||
@@ -147,10 +171,19 @@ export async function PUT(request, { params }) {
|
||||
}
|
||||
|
||||
if (proxyPoolResult.hasProxyPoolField) {
|
||||
if (proxyPoolResult.proxyPoolId === null) {
|
||||
// Handle new multi-proxy format
|
||||
if (proxyPoolResult.proxyPoolIds !== undefined) {
|
||||
updateData.providerSpecificData.proxyPoolIds = proxyPoolResult.proxyPoolIds;
|
||||
updateData.providerSpecificData.proxyRotationStrategy = proxyPoolResult.proxyRotationStrategy;
|
||||
// Clear legacy field
|
||||
delete updateData.providerSpecificData.proxyPoolId;
|
||||
} else {
|
||||
updateData.providerSpecificData.proxyPoolId = proxyPoolResult.proxyPoolId;
|
||||
// Handle legacy single-proxy format
|
||||
if (proxyPoolResult.proxyPoolId === null) {
|
||||
delete updateData.providerSpecificData.proxyPoolId;
|
||||
} else {
|
||||
updateData.providerSpecificData.proxyPoolId = proxyPoolResult.proxyPoolId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +803,7 @@ export async function testSingleConnection(id) {
|
||||
const connection = await getProviderConnectionById(id);
|
||||
if (!connection) return { valid: false, error: "Connection not found", latencyMs: 0, testedAt: new Date().toISOString() };
|
||||
|
||||
const effectiveProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
|
||||
const effectiveProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
|
||||
|
||||
if (effectiveProxy.connectionProxyEnabled && effectiveProxy.connectionProxyUrl && !effectiveProxy.vercelRelayUrl) {
|
||||
const proxyResult = await testProxyUrl({ proxyUrl: effectiveProxy.connectionProxyUrl });
|
||||
|
||||
@@ -145,7 +145,7 @@ export async function GET(request, { params }) {
|
||||
}
|
||||
|
||||
// Resolve connection proxy config; force strictProxy=false so quota/refresh fall back to direct on failure
|
||||
const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData);
|
||||
const proxyConfig = await resolveConnectionProxyConfig(connection.providerSpecificData, connection.id);
|
||||
const proxyOptions = {
|
||||
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
|
||||
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
|
||||
|
||||
@@ -77,7 +77,7 @@ const LIVE_MODEL_RESOLVERS = {
|
||||
return result?.models?.length ? { models: result.models } : null;
|
||||
},
|
||||
"grok-cli": async (conn) => {
|
||||
const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {});
|
||||
const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {}, conn.id);
|
||||
const result = await resolveGrokCliModels({
|
||||
...conn,
|
||||
connectionId: conn.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ const OPTIONAL_FIELDS = [
|
||||
"scope", "projectId", "apiKey", "testStatus",
|
||||
"lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode",
|
||||
"consecutiveUseCount", "idToken", "lastRefreshAt",
|
||||
"proxyRotationStrategy", "proxyPoolIds",
|
||||
];
|
||||
|
||||
function rowToConn(row) {
|
||||
|
||||
@@ -59,75 +59,107 @@ function normalizeLegacyProxy(providerSpecificData = {}) {
|
||||
* Resolve final proxy configuration.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Proxy Pool
|
||||
* 2. Legacy Proxy
|
||||
* 3. No Proxy
|
||||
* 1. Multi-Proxy Pool (new format with rotation)
|
||||
* 2. Single Proxy Pool (legacy)
|
||||
* 3. Legacy Proxy
|
||||
* 4. No Proxy
|
||||
*/
|
||||
export async function resolveConnectionProxyConfig(
|
||||
providerSpecificData = {}
|
||||
providerSpecificData = {},
|
||||
connectionId = null
|
||||
) {
|
||||
try {
|
||||
const proxyPoolIdRaw = normalizeString(
|
||||
providerSpecificData?.proxyPoolId
|
||||
);
|
||||
|
||||
// "__none__" means explicitly disabled
|
||||
const proxyPoolId =
|
||||
proxyPoolIdRaw === "__none__" ? "" : proxyPoolIdRaw;
|
||||
// Handle new multi-proxy format
|
||||
const proxyPoolIds = providerSpecificData?.proxyPoolIds || [];
|
||||
const proxyRotationStrategy = providerSpecificData?.proxyRotationStrategy || "none";
|
||||
|
||||
// Handle legacy single-proxy format
|
||||
const legacyProxyPoolId = normalizeString(providerSpecificData?.proxyPoolId);
|
||||
const proxyPoolIdRaw = legacyProxyPoolId === "__none__" ? "" : legacyProxyPoolId;
|
||||
|
||||
const legacy = normalizeLegacyProxy(providerSpecificData);
|
||||
|
||||
/**
|
||||
* -----------------------------
|
||||
* Proxy Pool Resolution
|
||||
* Multi-Proxy Pool Resolution (NEW)
|
||||
* -----------------------------
|
||||
*/
|
||||
if (proxyPoolId) {
|
||||
const proxyPool = await getProxyPoolById(proxyPoolId);
|
||||
if (proxyPoolIds.length > 0) {
|
||||
const selectedPoolId = pickProxyPoolId(proxyPoolIds, proxyRotationStrategy, connectionId);
|
||||
|
||||
if (selectedPoolId) {
|
||||
const proxyPool = await getProxyPoolById(selectedPoolId);
|
||||
const proxyUrl = normalizeString(proxyPool?.proxyUrl);
|
||||
const noProxy = normalizeString(proxyPool?.noProxy);
|
||||
|
||||
const isValidPool = proxyPool && proxyPool.isActive === true && proxyUrl;
|
||||
|
||||
if (isValidPool) {
|
||||
/**
|
||||
* Vercel/Cloudflare relay proxies use base URL rewriting
|
||||
* instead of HTTP_PROXY environment variables.
|
||||
*/
|
||||
if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno") {
|
||||
return {
|
||||
source: proxyPool.type,
|
||||
proxyPoolId: selectedPoolId,
|
||||
proxyPool,
|
||||
connectionProxyEnabled: false,
|
||||
connectionProxyUrl: "",
|
||||
connectionNoProxy: noProxy,
|
||||
strictProxy: proxyPool.strictProxy === true,
|
||||
vercelRelayUrl: proxyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard proxy pool
|
||||
*/
|
||||
return {
|
||||
source: "pool",
|
||||
proxyPoolId: selectedPoolId,
|
||||
proxyPool,
|
||||
connectionProxyEnabled: true,
|
||||
connectionProxyUrl: proxyUrl,
|
||||
connectionNoProxy: noProxy,
|
||||
strictProxy: proxyPool.strictProxy === true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* -----------------------------
|
||||
* Single Proxy Pool Resolution (LEGACY)
|
||||
* -----------------------------
|
||||
*/
|
||||
if (proxyPoolIdRaw) {
|
||||
const proxyPool = await getProxyPoolById(proxyPoolIdRaw);
|
||||
const proxyUrl = normalizeString(proxyPool?.proxyUrl);
|
||||
const noProxy = normalizeString(proxyPool?.noProxy);
|
||||
|
||||
const isValidPool =
|
||||
proxyPool &&
|
||||
proxyPool.isActive === true &&
|
||||
proxyUrl;
|
||||
const isValidPool = proxyPool && proxyPool.isActive === true && proxyUrl;
|
||||
|
||||
if (isValidPool) {
|
||||
/**
|
||||
* Vercel/Cloudflare relay proxies use base URL rewriting
|
||||
* instead of HTTP_PROXY environment variables.
|
||||
*/
|
||||
if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno") {
|
||||
return {
|
||||
source: proxyPool.type,
|
||||
|
||||
proxyPoolId,
|
||||
proxyPoolId: proxyPoolIdRaw,
|
||||
proxyPool,
|
||||
|
||||
connectionProxyEnabled: false,
|
||||
connectionProxyUrl: "",
|
||||
connectionNoProxy: noProxy,
|
||||
|
||||
strictProxy: proxyPool.strictProxy === true,
|
||||
|
||||
vercelRelayUrl: proxyUrl, // Still mapped to vercelRelayUrl in the unified payload since they use the exact same header spec
|
||||
vercelRelayUrl: proxyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard proxy pool
|
||||
*/
|
||||
return {
|
||||
source: "pool",
|
||||
|
||||
proxyPoolId,
|
||||
proxyPoolId: proxyPoolIdRaw,
|
||||
proxyPool,
|
||||
|
||||
connectionProxyEnabled: true,
|
||||
connectionProxyUrl: proxyUrl,
|
||||
connectionNoProxy: noProxy,
|
||||
|
||||
strictProxy: proxyPool.strictProxy === true,
|
||||
};
|
||||
}
|
||||
@@ -145,7 +177,7 @@ export async function resolveConnectionProxyConfig(
|
||||
return {
|
||||
source: "legacy",
|
||||
|
||||
proxyPoolId: proxyPoolId || null,
|
||||
proxyPoolId: proxyPoolIdRaw || null,
|
||||
proxyPool: null,
|
||||
|
||||
...legacy,
|
||||
@@ -160,7 +192,7 @@ export async function resolveConnectionProxyConfig(
|
||||
return {
|
||||
source: "none",
|
||||
|
||||
proxyPoolId: proxyPoolId || null,
|
||||
proxyPoolId: proxyPoolIdRaw || null,
|
||||
proxyPool: null,
|
||||
|
||||
...legacy,
|
||||
|
||||
@@ -234,6 +234,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
"codebuddy-intl",
|
||||
"qoder",
|
||||
"grok-cli",
|
||||
"freebuff",
|
||||
];
|
||||
if (deviceCodeProviders.includes(provider)) {
|
||||
setIsDeviceCode(true);
|
||||
|
||||
@@ -172,7 +172,7 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
|
||||
connection = availableConnections[0];
|
||||
}
|
||||
|
||||
const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
|
||||
const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
|
||||
|
||||
return {
|
||||
authType: connection.authType,
|
||||
|
||||
Reference in New Issue
Block a user