chore: prepare fork docker deployment
This commit is contained in:
1 parent
eca3b9c3dc
commit
d05b84d377
16 files changed
+396
-1596
No files matched your search
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GHCR_IMAGE: ghcr.io/${{ github.repository }}
|
GHCR_IMAGE: ghcr.io/${{ github.repository }}
|
||||||
DOCKERHUB_IMAGE: decolua/9router
|
DOCKERHUB_IMAGE: mhiqrambhrng/9router-mibp-version
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
9router:
|
9router:
|
||||||
image: decolua/9router:latest
|
image: mhiqrambhrng/9router-mibp-version:latest
|
||||||
container_name: 9router
|
container_name: 9router
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default {
|
|||||||
website: "https://freebuff.com",
|
website: "https://freebuff.com",
|
||||||
notice: {
|
notice: {
|
||||||
signupUrl: "https://freebuff.com",
|
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",
|
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 }) {
|
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 [showProxyDropdown, setShowProxyDropdown] = useState(false);
|
||||||
const [updatingProxy, setUpdatingProxy] = useState(false);
|
const [updatingProxy, setUpdatingProxy] = useState(false);
|
||||||
|
const [selectedProxyIds, setSelectedProxyIds] = useState([]);
|
||||||
|
const [rotationStrategy, setRotationStrategy] = useState("none");
|
||||||
const proxyDropdownRef = useRef(null);
|
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 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 hasLegacyProxy = connection.providerSpecificData?.connectionProxyEnabled === true && !!connection.providerSpecificData?.connectionProxyUrl;
|
||||||
const hasAnyProxy = !!boundProxyPoolId || hasLegacyProxy;
|
const hasAnyProxy = selectedProxyIds.length > 0 || hasLegacyProxy;
|
||||||
const proxyDisplayText = boundProxyPool
|
|
||||||
? `Pool: ${boundProxyPool.name}`
|
const getProxyDisplayText = () => {
|
||||||
: boundProxyPoolId
|
if (selectedProxyIds.length === 0 && !hasLegacyProxy) return "";
|
||||||
? `Pool: ${boundProxyPoolId} (inactive/missing)`
|
|
||||||
: hasLegacyProxy
|
if (selectedProxyIds.length === 1) {
|
||||||
? `Legacy: ${connection.providerSpecificData?.connectionProxyUrl}`
|
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"
|
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."
|
? "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.";
|
: "When your 5h quota runs out, auto-sends a request the moment it resets so a new window starts right away.";
|
||||||
|
|
||||||
let maskedProxyUrl = "";
|
let maskedProxyUrl = "";
|
||||||
if (boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl) {
|
if (selectedProxyIds.length > 0) {
|
||||||
const rawProxyUrl = boundProxyPool?.proxyUrl || connection.providerSpecificData?.connectionProxyUrl;
|
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 {
|
try {
|
||||||
const parsed = new URL(rawProxyUrl);
|
const parsed = new URL(rawProxyUrl);
|
||||||
maskedProxyUrl = `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
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";
|
let proxyBadgeVariant = "default";
|
||||||
if (boundProxyPool?.isActive === true) {
|
if (selectedProxyIds.length > 0) {
|
||||||
proxyBadgeVariant = "success";
|
const allActive = selectedProxyIds.every(id => proxyPoolMap.get(id)?.isActive === true);
|
||||||
} else if (boundProxyPoolId || hasLegacyProxy) {
|
proxyBadgeVariant = allActive ? "success" : "error";
|
||||||
|
} else if (hasLegacyProxy) {
|
||||||
proxyBadgeVariant = "error";
|
proxyBadgeVariant = "error";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +107,54 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
|||||||
return () => document.removeEventListener("mousedown", handler);
|
return () => document.removeEventListener("mousedown", handler);
|
||||||
}, [showProxyDropdown]);
|
}, [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);
|
setUpdatingProxy(true);
|
||||||
try {
|
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 {
|
} finally {
|
||||||
setUpdatingProxy(false);
|
setUpdatingProxy(false);
|
||||||
setShowProxyDropdown(false);
|
setShowProxyDropdown(false);
|
||||||
@@ -226,22 +318,117 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
|||||||
<span className="text-[10px] leading-tight">Proxy</span>
|
<span className="text-[10px] leading-tight">Proxy</span>
|
||||||
</button>
|
</button>
|
||||||
{showProxyDropdown && (
|
{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">
|
<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">
|
||||||
<button
|
{/* Rotation Strategy Selector */}
|
||||||
onClick={() => handleSelectProxy("__none__")}
|
<div className="border-b border-border p-3">
|
||||||
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"}`}
|
<label className="block text-xs font-medium text-text-muted mb-2">Rotation Strategy</label>
|
||||||
>
|
<select
|
||||||
None
|
value={rotationStrategy}
|
||||||
</button>
|
onChange={(e) => handleStrategyChange(e.target.value)}
|
||||||
{(proxyPools || []).map((pool) => (
|
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"
|
||||||
<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"}`}
|
|
||||||
>
|
>
|
||||||
{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>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -968,17 +968,39 @@ export default function ProviderDetailPage() {
|
|||||||
onToggle: (on) => handleAutoPingConnection(conn.id, on),
|
onToggle: (on) => handleAutoPingConnection(conn.id, on),
|
||||||
provider: providerId,
|
provider: providerId,
|
||||||
} : null}
|
} : null}
|
||||||
onUpdateProxy={async (proxyPoolId) => {
|
onUpdateProxy={async (proxyConfig) => {
|
||||||
try {
|
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}`, {
|
const res = await fetch(`/api/providers/${conn.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ proxyPoolId: proxyPoolId || null }),
|
body: JSON.stringify(updatePayload),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setConnections(prev => prev.map(c =>
|
setConnections(prev => prev.map(c =>
|
||||||
c.id === conn.id
|
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
|
: c
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export async function GET(request, { params }) {
|
|||||||
"codebuddy-intl",
|
"codebuddy-intl",
|
||||||
"qoder",
|
"qoder",
|
||||||
"grok-cli",
|
"grok-cli",
|
||||||
|
"freebuff",
|
||||||
];
|
];
|
||||||
let deviceData;
|
let deviceData;
|
||||||
if (noPkceDeviceProviders.includes(provider)) {
|
if (noPkceDeviceProviders.includes(provider)) {
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ const PROVIDER_MODELS_CONFIG = {
|
|||||||
},
|
},
|
||||||
"grok-cli": {
|
"grok-cli": {
|
||||||
customResolver: async (connection) => {
|
customResolver: async (connection) => {
|
||||||
const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
|
const proxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
|
||||||
const result = await resolveGrokCliModels({
|
const result = await resolveGrokCliModels({
|
||||||
...connection,
|
...connection,
|
||||||
connectionId: connection.id,
|
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) {
|
if (proxyPoolIdInput === undefined) {
|
||||||
return { hasProxyPoolField: false, proxyPoolId: null };
|
return { hasProxyPoolField: false, proxyPoolId: null };
|
||||||
}
|
}
|
||||||
@@ -111,7 +135,7 @@ export async function PUT(request, { params }) {
|
|||||||
return NextResponse.json({ error: proxyConfig.error }, { status: 400 });
|
return NextResponse.json({ error: proxyConfig.error }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyPoolResult = await normalizeProxyPoolUpdate(body.proxyPoolId);
|
const proxyPoolResult = await normalizeProxyPoolUpdate(body);
|
||||||
if (proxyPoolResult.error) {
|
if (proxyPoolResult.error) {
|
||||||
return NextResponse.json({ error: proxyPoolResult.error }, { status: 400 });
|
return NextResponse.json({ error: proxyPoolResult.error }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -147,10 +171,19 @@ export async function PUT(request, { params }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (proxyPoolResult.hasProxyPoolField) {
|
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;
|
delete updateData.providerSpecificData.proxyPoolId;
|
||||||
} else {
|
} 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);
|
const connection = await getProviderConnectionById(id);
|
||||||
if (!connection) return { valid: false, error: "Connection not found", latencyMs: 0, testedAt: new Date().toISOString() };
|
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) {
|
if (effectiveProxy.connectionProxyEnabled && effectiveProxy.connectionProxyUrl && !effectiveProxy.vercelRelayUrl) {
|
||||||
const proxyResult = await testProxyUrl({ proxyUrl: effectiveProxy.connectionProxyUrl });
|
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
|
// 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 = {
|
const proxyOptions = {
|
||||||
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
|
connectionProxyEnabled: proxyConfig.connectionProxyEnabled === true,
|
||||||
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
|
connectionProxyUrl: proxyConfig.connectionProxyUrl || "",
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const LIVE_MODEL_RESOLVERS = {
|
|||||||
return result?.models?.length ? { models: result.models } : null;
|
return result?.models?.length ? { models: result.models } : null;
|
||||||
},
|
},
|
||||||
"grok-cli": async (conn) => {
|
"grok-cli": async (conn) => {
|
||||||
const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {});
|
const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {}, conn.id);
|
||||||
const result = await resolveGrokCliModels({
|
const result = await resolveGrokCliModels({
|
||||||
...conn,
|
...conn,
|
||||||
connectionId: conn.id,
|
connectionId: conn.id,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const OPTIONAL_FIELDS = [
|
|||||||
"scope", "projectId", "apiKey", "testStatus",
|
"scope", "projectId", "apiKey", "testStatus",
|
||||||
"lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode",
|
"lastTested", "lastError", "lastErrorAt", "rateLimitedUntil", "expiresIn", "errorCode",
|
||||||
"consecutiveUseCount", "idToken", "lastRefreshAt",
|
"consecutiveUseCount", "idToken", "lastRefreshAt",
|
||||||
|
"proxyRotationStrategy", "proxyPoolIds",
|
||||||
];
|
];
|
||||||
|
|
||||||
function rowToConn(row) {
|
function rowToConn(row) {
|
||||||
|
|||||||
@@ -59,75 +59,107 @@ function normalizeLegacyProxy(providerSpecificData = {}) {
|
|||||||
* Resolve final proxy configuration.
|
* Resolve final proxy configuration.
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Proxy Pool
|
* 1. Multi-Proxy Pool (new format with rotation)
|
||||||
* 2. Legacy Proxy
|
* 2. Single Proxy Pool (legacy)
|
||||||
* 3. No Proxy
|
* 3. Legacy Proxy
|
||||||
|
* 4. No Proxy
|
||||||
*/
|
*/
|
||||||
export async function resolveConnectionProxyConfig(
|
export async function resolveConnectionProxyConfig(
|
||||||
providerSpecificData = {}
|
providerSpecificData = {},
|
||||||
|
connectionId = null
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const proxyPoolIdRaw = normalizeString(
|
// Handle new multi-proxy format
|
||||||
providerSpecificData?.proxyPoolId
|
const proxyPoolIds = providerSpecificData?.proxyPoolIds || [];
|
||||||
);
|
const proxyRotationStrategy = providerSpecificData?.proxyRotationStrategy || "none";
|
||||||
|
|
||||||
// "__none__" means explicitly disabled
|
// Handle legacy single-proxy format
|
||||||
const proxyPoolId =
|
const legacyProxyPoolId = normalizeString(providerSpecificData?.proxyPoolId);
|
||||||
proxyPoolIdRaw === "__none__" ? "" : proxyPoolIdRaw;
|
const proxyPoolIdRaw = legacyProxyPoolId === "__none__" ? "" : legacyProxyPoolId;
|
||||||
|
|
||||||
const legacy = normalizeLegacyProxy(providerSpecificData);
|
const legacy = normalizeLegacyProxy(providerSpecificData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* -----------------------------
|
* -----------------------------
|
||||||
* Proxy Pool Resolution
|
* Multi-Proxy Pool Resolution (NEW)
|
||||||
* -----------------------------
|
* -----------------------------
|
||||||
*/
|
*/
|
||||||
if (proxyPoolId) {
|
if (proxyPoolIds.length > 0) {
|
||||||
const proxyPool = await getProxyPoolById(proxyPoolId);
|
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 proxyUrl = normalizeString(proxyPool?.proxyUrl);
|
||||||
const noProxy = normalizeString(proxyPool?.noProxy);
|
const noProxy = normalizeString(proxyPool?.noProxy);
|
||||||
|
const isValidPool = proxyPool && proxyPool.isActive === true && proxyUrl;
|
||||||
const isValidPool =
|
|
||||||
proxyPool &&
|
|
||||||
proxyPool.isActive === true &&
|
|
||||||
proxyUrl;
|
|
||||||
|
|
||||||
if (isValidPool) {
|
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") {
|
if (proxyPool.type === "vercel" || proxyPool.type === "cloudflare" || proxyPool.type === "deno") {
|
||||||
return {
|
return {
|
||||||
source: proxyPool.type,
|
source: proxyPool.type,
|
||||||
|
proxyPoolId: proxyPoolIdRaw,
|
||||||
proxyPoolId,
|
|
||||||
proxyPool,
|
proxyPool,
|
||||||
|
|
||||||
connectionProxyEnabled: false,
|
connectionProxyEnabled: false,
|
||||||
connectionProxyUrl: "",
|
connectionProxyUrl: "",
|
||||||
connectionNoProxy: noProxy,
|
connectionNoProxy: noProxy,
|
||||||
|
|
||||||
strictProxy: proxyPool.strictProxy === true,
|
strictProxy: proxyPool.strictProxy === true,
|
||||||
|
vercelRelayUrl: proxyUrl,
|
||||||
vercelRelayUrl: proxyUrl, // Still mapped to vercelRelayUrl in the unified payload since they use the exact same header spec
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard proxy pool
|
|
||||||
*/
|
|
||||||
return {
|
return {
|
||||||
source: "pool",
|
source: "pool",
|
||||||
|
proxyPoolId: proxyPoolIdRaw,
|
||||||
proxyPoolId,
|
|
||||||
proxyPool,
|
proxyPool,
|
||||||
|
|
||||||
connectionProxyEnabled: true,
|
connectionProxyEnabled: true,
|
||||||
connectionProxyUrl: proxyUrl,
|
connectionProxyUrl: proxyUrl,
|
||||||
connectionNoProxy: noProxy,
|
connectionNoProxy: noProxy,
|
||||||
|
|
||||||
strictProxy: proxyPool.strictProxy === true,
|
strictProxy: proxyPool.strictProxy === true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -145,7 +177,7 @@ export async function resolveConnectionProxyConfig(
|
|||||||
return {
|
return {
|
||||||
source: "legacy",
|
source: "legacy",
|
||||||
|
|
||||||
proxyPoolId: proxyPoolId || null,
|
proxyPoolId: proxyPoolIdRaw || null,
|
||||||
proxyPool: null,
|
proxyPool: null,
|
||||||
|
|
||||||
...legacy,
|
...legacy,
|
||||||
@@ -160,7 +192,7 @@ export async function resolveConnectionProxyConfig(
|
|||||||
return {
|
return {
|
||||||
source: "none",
|
source: "none",
|
||||||
|
|
||||||
proxyPoolId: proxyPoolId || null,
|
proxyPoolId: proxyPoolIdRaw || null,
|
||||||
proxyPool: null,
|
proxyPool: null,
|
||||||
|
|
||||||
...legacy,
|
...legacy,
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
|||||||
"codebuddy-intl",
|
"codebuddy-intl",
|
||||||
"qoder",
|
"qoder",
|
||||||
"grok-cli",
|
"grok-cli",
|
||||||
|
"freebuff",
|
||||||
];
|
];
|
||||||
if (deviceCodeProviders.includes(provider)) {
|
if (deviceCodeProviders.includes(provider)) {
|
||||||
setIsDeviceCode(true);
|
setIsDeviceCode(true);
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
|
|||||||
connection = availableConnections[0];
|
connection = availableConnections[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {});
|
const resolvedProxy = await resolveConnectionProxyConfig(connection.providerSpecificData || {}, connection.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authType: connection.authType,
|
authType: connection.authType,
|
||||||
|
|||||||
Reference in new issue
Block a user