mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Honor provider proxies for The Old LLM Vercel blocks (#7380)
* fix(theoldllm): honor provider proxy for Vercel blocks * fix(theoldllm): fail closed when assigned proxy is unavailable * refactor(theoldllm): extract proxy guards into dedicated module
This commit is contained in:
@@ -108,6 +108,23 @@ export function mapModel(model: string): string {
|
||||
const TOKEN_SEED = "oldllm-client-2026";
|
||||
const UA_PREFIX = CHROME_UA.slice(0, 20); // "Mozilla/5.0 (Windows"
|
||||
|
||||
type TheOldLlmProxy = {
|
||||
type?: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string | null;
|
||||
password?: string | null;
|
||||
} | null;
|
||||
|
||||
interface TheOldLlmFetchDependencies {
|
||||
resolveProxy: () => Promise<TheOldLlmProxy>;
|
||||
runWithProxy: <T>(proxy: TheOldLlmProxy, request: () => Promise<T>) => Promise<T>;
|
||||
fetch: typeof fetch;
|
||||
hasBlockingProxyAssignment?: () => boolean;
|
||||
}
|
||||
|
||||
class TheOldLlmProxyUnavailableError extends Error {}
|
||||
|
||||
export function generateRequestToken(): string {
|
||||
const n = Date.now();
|
||||
const e = `${n}-${TOKEN_SEED}-${UA_PREFIX}`;
|
||||
@@ -127,6 +144,44 @@ export const tokenCache: { value: string; expiresAt: number } = { value: "", exp
|
||||
|
||||
// ── Direct Node.js fetch ──────────────────────────────────────────────────
|
||||
|
||||
export async function fetchTheOldLlmWithProviderProxy(
|
||||
reqBody: Record<string, unknown>,
|
||||
signal: AbortSignal,
|
||||
dependencies?: TheOldLlmFetchDependencies
|
||||
): Promise<Response> {
|
||||
let deps = dependencies;
|
||||
if (!deps) {
|
||||
const [
|
||||
{ resolveProxyForProvider, hasBlockingProxyAssignmentForProvider },
|
||||
{ runWithProxyContext },
|
||||
] = await Promise.all([import("../../src/lib/db/proxies"), import("../utils/proxyFetch.ts")]);
|
||||
deps = {
|
||||
resolveProxy: () => resolveProxyForProvider("theoldllm"),
|
||||
runWithProxy: runWithProxyContext,
|
||||
fetch: globalThis.fetch,
|
||||
hasBlockingProxyAssignment: () => hasBlockingProxyAssignmentForProvider("theoldllm"),
|
||||
};
|
||||
}
|
||||
|
||||
const proxy = await deps.resolveProxy();
|
||||
if (!proxy && deps.hasBlockingProxyAssignment?.()) {
|
||||
throw new TheOldLlmProxyUnavailableError("No active proxy is available for The Old LLM");
|
||||
}
|
||||
return deps.runWithProxy(proxy, () =>
|
||||
deps.fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Version": "3.8.4",
|
||||
"X-Request-Token": generateRequestToken(),
|
||||
"User-Agent": CHROME_UA,
|
||||
},
|
||||
body: JSON.stringify(reqBody),
|
||||
signal,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function directFetch(
|
||||
reqBody: Record<string, unknown>,
|
||||
signal?: AbortSignal | null
|
||||
@@ -137,23 +192,26 @@ async function directFetch(
|
||||
signal?.addEventListener("abort", onSignal!, { once: true });
|
||||
|
||||
try {
|
||||
return await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Version": "3.8.4",
|
||||
"X-Request-Token": generateRequestToken(),
|
||||
"User-Agent": CHROME_UA,
|
||||
},
|
||||
body: JSON.stringify(reqBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
// No-auth providers do not have a connection row, so chatCore cannot apply
|
||||
// a connection-scoped proxy context for them. Resolve the provider/global
|
||||
// assignment explicitly; otherwise The Old LLM always leaks out through the
|
||||
// VPS address and Vercel's bot protection denies every model.
|
||||
return await fetchTheOldLlmWithProviderProxy(reqBody, controller.signal);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
if (onSignal) signal?.removeEventListener("abort", onSignal);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVercelMitigationResponse(response: Response, body: string): boolean {
|
||||
const mitigation = response.headers.get("x-vercel-mitigated")?.toLowerCase();
|
||||
if (mitigation === "deny" || mitigation === "challenge") return true;
|
||||
return (
|
||||
(response.status === 403 || response.status === 429) &&
|
||||
/vercel security checkpoint|"message"\s*:\s*"forbidden"/i.test(body)
|
||||
);
|
||||
}
|
||||
|
||||
function isTokenRejected(status: number, body: string): boolean {
|
||||
if (status === 401 || status === 403) return true;
|
||||
try {
|
||||
@@ -211,6 +269,45 @@ function buildErrorResponse(status: number, body: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function buildVercelMitigationError(): string {
|
||||
return JSON.stringify({
|
||||
error: {
|
||||
message:
|
||||
"The Old LLM is blocked by Vercel for this server egress IP. Configure a residential provider or global proxy for 'theoldllm' and retry.",
|
||||
type: "upstream_access_denied",
|
||||
code: "THEOLDLLM_VERCEL_MITIGATED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildProxyUnavailableError(): string {
|
||||
return JSON.stringify({
|
||||
error: {
|
||||
message:
|
||||
"The Old LLM proxy assignment has no active proxies. Configure or enable a proxy and retry.",
|
||||
type: "proxy_unavailable",
|
||||
code: "THEOLDLLM_PROXY_UNAVAILABLE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUpstreamWithRetry(
|
||||
reqBody: Record<string, unknown>,
|
||||
signal: AbortSignal | null | undefined,
|
||||
log: ExecuteInput["log"]
|
||||
): Promise<{ response: Response; body: string; vercelMitigated: boolean }> {
|
||||
let response = await directFetch(reqBody, signal);
|
||||
let body = await response.text();
|
||||
let vercelMitigated = isVercelMitigationResponse(response, body);
|
||||
if (!vercelMitigated && isTokenRejected(response.status, body)) {
|
||||
log?.warn?.("THEOLDLLM", `Token rejected (${response.status}), retrying with fresh token…`);
|
||||
response = await directFetch(reqBody, signal);
|
||||
body = await response.text();
|
||||
vercelMitigated = isVercelMitigationResponse(response, body);
|
||||
}
|
||||
return { response, body, vercelMitigated };
|
||||
}
|
||||
|
||||
// ── Executor ──────────────────────────────────────────────────────────────
|
||||
|
||||
export class TheOldLlmExecutor extends BaseExecutor {
|
||||
@@ -237,27 +334,37 @@ export class TheOldLlmExecutor extends BaseExecutor {
|
||||
return body;
|
||||
}
|
||||
|
||||
private executionResult(input: ExecuteInput, response: Response, body: unknown) {
|
||||
return {
|
||||
response,
|
||||
url: API_URL,
|
||||
headers: this.buildHeaders(input.credentials),
|
||||
transformedBody: body,
|
||||
};
|
||||
}
|
||||
|
||||
async testConnection(
|
||||
_credentials: ProviderCredentials,
|
||||
_signal?: AbortSignal | null,
|
||||
log?: ExecuteInput["log"]
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Version": "3.8.4",
|
||||
"X-Request-Token": generateRequestToken(),
|
||||
"User-Agent": CHROME_UA,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const resp = await directFetch(
|
||||
{
|
||||
model: "GPT_5_4",
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
stream: false,
|
||||
}),
|
||||
signal: _signal ?? undefined,
|
||||
});
|
||||
},
|
||||
_signal
|
||||
);
|
||||
const body = await resp.text();
|
||||
if (!resp.ok && isVercelMitigationResponse(resp, body)) {
|
||||
log?.warn?.(
|
||||
"THEOLDLLM",
|
||||
"Vercel blocked this egress IP; configure a residential provider proxy"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return resp.status === 200;
|
||||
} catch {
|
||||
log?.warn?.("THEOLDLLM", "testConnection network error");
|
||||
@@ -297,56 +404,55 @@ export class TheOldLlmExecutor extends BaseExecutor {
|
||||
stream: true,
|
||||
};
|
||||
|
||||
let upstream = await directFetch(reqBody, signal);
|
||||
let finalBody = await upstream.text();
|
||||
|
||||
if (isTokenRejected(upstream.status, finalBody)) {
|
||||
log?.warn?.("THEOLDLLM", `Token rejected (${upstream.status}), retrying with fresh token…`);
|
||||
upstream = await directFetch(reqBody, signal);
|
||||
finalBody = await upstream.text();
|
||||
}
|
||||
const {
|
||||
response: upstream,
|
||||
body: finalBody,
|
||||
vercelMitigated,
|
||||
} = await fetchUpstreamWithRetry(reqBody, signal, log);
|
||||
|
||||
if (upstream.status === 200 && finalBody) {
|
||||
const payload = stream ? finalBody : buildChatCompletion(parseSseContent(finalBody), model);
|
||||
return {
|
||||
response: new Response(encoder.encode(payload), {
|
||||
return this.executionResult(
|
||||
input,
|
||||
new Response(encoder.encode(payload), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": stream ? "text/event-stream" : "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
}),
|
||||
url: API_URL,
|
||||
headers: this.buildHeaders(input.credentials),
|
||||
transformedBody: body,
|
||||
};
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(encoder.encode(buildErrorResponse(upstream.status, finalBody)), {
|
||||
const errorPayload = vercelMitigated
|
||||
? buildVercelMitigationError()
|
||||
: buildErrorResponse(upstream.status, finalBody);
|
||||
return this.executionResult(
|
||||
input,
|
||||
new Response(encoder.encode(errorPayload), {
|
||||
status: upstream.status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url: API_URL,
|
||||
headers: this.buildHeaders(input.credentials),
|
||||
transformedBody: body,
|
||||
};
|
||||
body
|
||||
);
|
||||
} catch (err) {
|
||||
const proxyUnavailable = err instanceof TheOldLlmProxyUnavailableError;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
log?.error?.("THEOLDLLM", `Executor error: ${msg}`);
|
||||
return {
|
||||
response: new Response(
|
||||
encoder.encode(
|
||||
JSON.stringify({
|
||||
error: { message: msg, type: "upstream_error", code: "EXECUTOR_ERROR" },
|
||||
})
|
||||
),
|
||||
{ status: 502, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: API_URL,
|
||||
headers: this.buildHeaders(input.credentials),
|
||||
transformedBody: body,
|
||||
};
|
||||
const errorPayload = proxyUnavailable
|
||||
? buildProxyUnavailableError()
|
||||
: JSON.stringify({
|
||||
error: { message: msg, type: "upstream_error", code: "EXECUTOR_ERROR" },
|
||||
});
|
||||
return this.executionResult(
|
||||
input,
|
||||
new Response(encoder.encode(errorPayload), {
|
||||
status: proxyUnavailable ? 503 : 502,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
coerceProxyPayload,
|
||||
redactProxySecrets,
|
||||
} from "./proxies/mappers";
|
||||
import { isGlobalProxyEnabled, PROXY_ALIVE_PREDICATE } from "./proxies/guards";
|
||||
export {
|
||||
hasBlockingProxyAssignment,
|
||||
hasBlockingProxyAssignmentForProvider,
|
||||
} from "./proxies/guards";
|
||||
export { extractRelayAuth, redactProxySecrets } from "./proxies/mappers";
|
||||
|
||||
let proxyRegistryGeneration = 0;
|
||||
@@ -822,9 +827,6 @@ export async function deleteProxyById(id: string, options?: { force?: boolean })
|
||||
// usable so a working proxy is never stranded; only known-dead states are
|
||||
// excluded so a dead proxy stops being handed out (every request would
|
||||
// otherwise pay the timeout or leak out the host IP).
|
||||
const PROXY_ALIVE_PREDICATE =
|
||||
"(p.status IS NULL OR LOWER(p.status) NOT IN ('inactive','error','disabled','dead','down'))";
|
||||
|
||||
// Resolve one scope's alive pool to a single proxy via its rotation strategy.
|
||||
// Returns the standard registry resolution shape, or null when the pool is empty
|
||||
// or every member is dead (preserving the #6246 fail-closed contract — a dead
|
||||
@@ -908,59 +910,6 @@ export async function resolveProxyForScopeFromRegistry(scope: string, scopeId?:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #6246 fail-closed guard. Returns true when a connection would egress DIRECTLY
|
||||
* ONLY because its ASSIGNED proxy (account/provider/global scope) is dead/inactive
|
||||
* — i.e. the request must be BLOCKED, not silently sent on the real IP.
|
||||
*
|
||||
* Callers use this after `resolveProxyForConnection` returns a direct result: if
|
||||
* the operator assigned a proxy but every assigned proxy is dead, leaking the IP
|
||||
* is worse than failing the request. An explicit "proxy off" (global or per
|
||||
* connection) is a deliberate direct choice and is NOT treated as a leak. Read-only
|
||||
* and best-effort: any DB error fails OPEN (returns false) so a guard never breaks
|
||||
* the request path.
|
||||
*/
|
||||
export function hasBlockingProxyAssignment(connectionId: string): boolean {
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
|
||||
// Explicit global "proxy off" → direct is intended, never a leak.
|
||||
const globalRow = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'proxyEnabled'")
|
||||
.get() as { value?: string } | undefined;
|
||||
if (globalRow?.value) {
|
||||
try {
|
||||
if (JSON.parse(globalRow.value) === false) return false;
|
||||
} catch {
|
||||
/* malformed → treat as enabled */
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit per-connection "proxy off" → direct is intended.
|
||||
const conn = db
|
||||
.prepare("SELECT provider, proxy_enabled FROM provider_connections WHERE id = ?")
|
||||
.get(connectionId) as { provider?: string | null; proxy_enabled?: number } | undefined;
|
||||
if (conn && conn.proxy_enabled === 0) return false;
|
||||
const provider = conn?.provider ?? null;
|
||||
|
||||
// A proxy is assigned to this connection at some scope, but every assigned
|
||||
// proxy is dead (the alive filter would have resolved a live one already).
|
||||
const dead = db
|
||||
.prepare(
|
||||
`SELECT 1 FROM proxy_assignments a JOIN proxy_registry p ON p.id = a.proxy_id
|
||||
WHERE ((a.scope = 'account' AND a.scope_id = ?)
|
||||
OR (a.scope = 'provider' AND a.scope_id = ?)
|
||||
OR (a.scope = 'global'))
|
||||
AND NOT ${PROXY_ALIVE_PREDICATE}
|
||||
LIMIT 1`
|
||||
)
|
||||
.get(connectionId, provider);
|
||||
return !!dead;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateLegacyProxyConfigToRegistry(options?: { force?: boolean }) {
|
||||
const force = options?.force === true;
|
||||
const db = getDbInstance();
|
||||
@@ -1127,6 +1076,9 @@ export async function bulkAssignProxyToScope(
|
||||
*/
|
||||
export async function resolveProxyForProvider(providerId: string) {
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
if (!isGlobalProxyEnabled(db)) return null;
|
||||
|
||||
// Resolve by specificity across both storage backends. The GUI Custom tab
|
||||
// still writes provider/global proxies to the legacy config, while Saved
|
||||
// Proxy uses the registry. A registry-global fallback must not shadow a
|
||||
|
||||
81
src/lib/db/proxies/guards.ts
Normal file
81
src/lib/db/proxies/guards.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { getDbInstance } from "../core";
|
||||
|
||||
export const PROXY_ALIVE_PREDICATE =
|
||||
"(p.status IS NULL OR LOWER(p.status) NOT IN ('inactive','error','disabled','dead','down'))";
|
||||
|
||||
export function isGlobalProxyEnabled(db: ReturnType<typeof getDbInstance>): boolean {
|
||||
try {
|
||||
const row = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'proxyEnabled'")
|
||||
.get() as { value?: string } | undefined;
|
||||
if (!row?.value) return true;
|
||||
try {
|
||||
return JSON.parse(row.value) !== false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #6246 fail-closed guard for a connection with an assigned dead proxy pool.
|
||||
* Explicitly disabling proxying globally or for the connection allows direct egress.
|
||||
*/
|
||||
export function hasBlockingProxyAssignment(connectionId: string): boolean {
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
if (!isGlobalProxyEnabled(db)) return false;
|
||||
|
||||
const conn = db
|
||||
.prepare("SELECT provider, proxy_enabled FROM provider_connections WHERE id = ?")
|
||||
.get(connectionId) as { provider?: string | null; proxy_enabled?: number } | undefined;
|
||||
if (conn && conn.proxy_enabled === 0) return false;
|
||||
const provider = conn?.provider ?? null;
|
||||
const dead = db
|
||||
.prepare(
|
||||
`SELECT 1 FROM proxy_assignments a JOIN proxy_registry p ON p.id = a.proxy_id
|
||||
WHERE ((a.scope = 'account' AND a.scope_id = ?)
|
||||
OR (a.scope = 'provider' AND a.scope_id = ?)
|
||||
OR (a.scope = 'global'))
|
||||
AND NOT ${PROXY_ALIVE_PREDICATE}
|
||||
LIMIT 1`
|
||||
)
|
||||
.get(connectionId, provider);
|
||||
return !!dead;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #7380 fail-closed guard for providers without a connection row. Returns true
|
||||
* when a provider/global proxy assignment exists but all assigned proxies are known dead.
|
||||
*/
|
||||
export function hasBlockingProxyAssignmentForProvider(providerId: string): boolean {
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
if (!isGlobalProxyEnabled(db)) return false;
|
||||
|
||||
const assignments = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
EXISTS(
|
||||
SELECT 1 FROM proxy_assignments a
|
||||
WHERE ((a.scope = 'provider' AND a.scope_id = ?)
|
||||
OR a.scope = 'global')
|
||||
) AS assigned,
|
||||
EXISTS(
|
||||
SELECT 1 FROM proxy_assignments a JOIN proxy_registry p ON p.id = a.proxy_id
|
||||
WHERE ((a.scope = 'provider' AND a.scope_id = ?)
|
||||
OR a.scope = 'global')
|
||||
AND ${PROXY_ALIVE_PREDICATE}
|
||||
) AS alive`
|
||||
)
|
||||
.get(providerId, providerId) as { assigned?: number; alive?: number } | undefined;
|
||||
return assignments?.assigned === 1 && assignments.alive === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -40,32 +40,30 @@ describe("TheOldLlmExecutor", () => {
|
||||
assert.strictEqual(headers["Content-Type"], "application/json");
|
||||
assert.ok(
|
||||
headers["User-Agent"].includes("Chrome/"),
|
||||
`expected Chrome UA, got ${headers["User-Agent"]}`,
|
||||
`expected Chrome UA, got ${headers["User-Agent"]}`
|
||||
);
|
||||
assert.ok(
|
||||
headers["User-Agent"].includes("Mozilla/5.0"),
|
||||
`expected Mozilla UA, got ${headers["User-Agent"]}`,
|
||||
`expected Mozilla UA, got ${headers["User-Agent"]}`
|
||||
);
|
||||
});
|
||||
|
||||
it("maps model aliases to upstream slugs", () => {
|
||||
const cases: Record<string, string> = {
|
||||
"gpt-5.4": "GPT_5_4",
|
||||
"GPT_5_3": "GPT_5_3",
|
||||
"gpt_5_2": "GPT_5_2",
|
||||
GPT_5_3: "GPT_5_3",
|
||||
gpt_5_2: "GPT_5_2",
|
||||
"gpt-4o": "GPT_4O",
|
||||
"claude-4.6-opus": "CLAUDE_4_6_OPUS",
|
||||
"claude sonnet 4": "CLAUDE_4_6_SONNET",
|
||||
"claude_haiku_3_5": "CLAUDE_4_5_HAIKU",
|
||||
claude_haiku_3_5: "CLAUDE_4_5_HAIKU",
|
||||
"weird-model": "GPT_5_4",
|
||||
};
|
||||
|
||||
const transformRequest = (executor as any).transformRequest.bind(
|
||||
executor,
|
||||
) as (
|
||||
const transformRequest = (executor as any).transformRequest.bind(executor) as (
|
||||
model: string,
|
||||
body: Record<string, unknown>,
|
||||
stream: boolean,
|
||||
stream: boolean
|
||||
) => Record<string, unknown>;
|
||||
|
||||
for (const [model, expected] of Object.entries(cases)) {
|
||||
@@ -73,7 +71,7 @@ describe("TheOldLlmExecutor", () => {
|
||||
assert.strictEqual(
|
||||
updated.model,
|
||||
expected,
|
||||
`model ${model} mapped to ${expected}, got ${updated.model}`,
|
||||
`model ${model} mapped to ${expected}, got ${updated.model}`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -89,7 +87,7 @@ describe("TheOldLlmExecutor", () => {
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
}),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
globalThis.fetch = async () => makeResponse(401) as any;
|
||||
@@ -100,7 +98,7 @@ describe("TheOldLlmExecutor", () => {
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
}),
|
||||
false,
|
||||
false
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
@@ -112,15 +110,10 @@ describe("TheOldLlmExecutor", () => {
|
||||
warmTokenCache();
|
||||
try {
|
||||
let calls = 0;
|
||||
const responses = [
|
||||
() => makeResponse(401, MOCK_ERR),
|
||||
() => makeResponse(200, MOCK_SSE),
|
||||
];
|
||||
const responses = [() => makeResponse(401, MOCK_ERR), () => makeResponse(200, MOCK_SSE)];
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
responses[
|
||||
calls++ < responses.length ? calls - 1 : responses.length - 1
|
||||
]() as any;
|
||||
responses[calls++ < responses.length ? calls - 1 : responses.length - 1]() as any;
|
||||
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.4",
|
||||
@@ -144,6 +137,45 @@ describe("TheOldLlmExecutor", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not retry a Vercel egress denial as a stale request token", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
try {
|
||||
let calls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
calls++;
|
||||
return new Response(
|
||||
JSON.stringify({ error: { code: "403", message: "Forbidden", id: "fra1::test" } }),
|
||||
{
|
||||
status: 403,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-vercel-mitigated": "deny",
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.4",
|
||||
body: { messages: [{ role: "user", content: "ping" }] },
|
||||
stream: true,
|
||||
signal: null,
|
||||
credentials: {},
|
||||
log: { debug() {}, info() {}, warn() {}, error() {} },
|
||||
});
|
||||
|
||||
assert.strictEqual(calls, 1);
|
||||
assert.strictEqual(result.response.status, 403);
|
||||
const json = (await result.response.json()) as {
|
||||
error?: { code?: string; message?: string };
|
||||
};
|
||||
assert.strictEqual(json.error?.code, "THEOLDLLM_VERCEL_MITIGATED");
|
||||
assert.match(json.error?.message || "", /residential.*proxy/i);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("lets cancellation abort before upstream work", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error("cancelled"));
|
||||
@@ -202,7 +234,7 @@ describe("TheOldLlmExecutor", () => {
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(requests);
|
||||
|
||||
60
tests/unit/theoldllm-provider-proxy.test.ts
Normal file
60
tests/unit/theoldllm-provider-proxy.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { fetchTheOldLlmWithProviderProxy } from "../../open-sse/executors/theoldllm.ts";
|
||||
|
||||
test("theoldllm dispatches through its provider proxy assignment", async () => {
|
||||
const assignedProxy = {
|
||||
type: "http",
|
||||
host: "residential.example",
|
||||
port: 8080,
|
||||
username: "user",
|
||||
password: "secret",
|
||||
};
|
||||
let observedProxy: unknown = null;
|
||||
let fetchCalls = 0;
|
||||
|
||||
const response = await fetchTheOldLlmWithProviderProxy(
|
||||
{ model: "GPT_5_4", messages: [], stream: true },
|
||||
new AbortController().signal,
|
||||
{
|
||||
resolveProxy: async () => assignedProxy,
|
||||
runWithProxy: async (proxy, request) => {
|
||||
observedProxy = proxy;
|
||||
return request();
|
||||
},
|
||||
fetch: (async () => {
|
||||
fetchCalls++;
|
||||
return new Response("ok", { status: 200 });
|
||||
}) as typeof fetch,
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(fetchCalls, 1);
|
||||
assert.deepEqual(observedProxy, assignedProxy);
|
||||
});
|
||||
|
||||
test("theoldllm fails closed when an assigned proxy pool has no active proxy", async () => {
|
||||
let fetchCalls = 0;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
fetchTheOldLlmWithProviderProxy(
|
||||
{ model: "GPT_5_4", messages: [], stream: true },
|
||||
new AbortController().signal,
|
||||
{
|
||||
resolveProxy: async () => null,
|
||||
hasBlockingProxyAssignment: () => true,
|
||||
runWithProxy: async (_proxy, request) => request(),
|
||||
fetch: (async () => {
|
||||
fetchCalls++;
|
||||
return new Response("unexpected", { status: 200 });
|
||||
}) as typeof fetch,
|
||||
}
|
||||
),
|
||||
/No active proxy is available/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls, 0, "a dead assigned proxy pool must never fall back to direct egress");
|
||||
});
|
||||
Reference in New Issue
Block a user