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:
Jan Leon
2026-07-18 20:13:13 +02:00
committed by GitHub
parent cb594ae370
commit d470526031
5 changed files with 363 additions and 132 deletions

View File

@@ -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
);
}
}
}

View File

@@ -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

View 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;
}
}

View File

@@ -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);

View 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");
});