Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"_rebaseline_2026_06_15_3885_glm_5_2": "PR #3885 own growth: pricing.ts 1508->1529 (+21 = GLM-5.2 pricing rows for glm-5.2 + effort aliases glm-5.2-high/-max, same $1.2/$5 schedule as glm-5.1; pure data). Also adds glm-5.2 specs to glmProvider.ts/modelSpecs.ts (modelSpecs.ts stays under cap). Cohesive model registration; not extractable.",
"_rebaseline_2026_06_15_3870_alias_lookup": "PR #3870 own growth: providerRegistry.ts 4703->4708 (+5 = generateModels() also stores each provider's models under its raw id, not only its alias, so getProviderModels(rawId) works when alias != id e.g. github->gh; preserves the existing first-wins guard). Cohesive registry fix; not extractable.",
"_rebaseline_2026_06_15_3846_sticky_combo_rr": "PR #3846 own growth: combo.ts 5204->5277 (+73 = combo-level sticky round-robin reusing the existing global stickyRoundRobinLimit knob #3847 added for account fallback: rrStickyTargets map + clampStickyRoundRobinTargetLimit + getStickyRoundRobinStartIndex/recordStickyRoundRobinSuccess helpers wired into handleRoundRobinCombo, with sticky-eviction tied to rrCounters eviction). Cohesive routing logic in the combo handler; not a movable block. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_06_15_3906_proxy_direct_fallback": "PR #3906 own growth: oauth/[provider]/[action]/route.ts 916->918 (+2 = swap runWithProxyContext -> runWithProxyContextOrDirect at the control-plane callsites so a dead pinned proxy degrades to a direct connection instead of a 500; the longer call name + a prettier reflow add the lines). Cohesive control-plane change; not extractable.",
"_rebaseline_2026_06_15_3871_empty_pool": "PR #3871 own growth: combo.ts 5203->5204 (+1 = guard expandAutoComboCandidatePool against an empty candidatePool array — Array.isArray(pool) && pool.length > 0 so [] falls through to active-connection expansion instead of early-returning). One-line correctness fix; not extractable.",
"cap": 800,
"frozen": {
Expand Down Expand Up @@ -77,7 +78,7 @@
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069,
"src/app/api/oauth/[provider]/[action]/route.ts": 916,
"src/app/api/oauth/[provider]/[action]/route.ts": 918,
"src/app/api/providers/[id]/models/route.ts": 2489,
"src/app/api/providers/[id]/test/route.ts": 842,
"src/app/api/usage/analytics/route.ts": 941,
Expand Down
40 changes: 39 additions & 1 deletion open-sse/utils/proxyFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,11 @@ function getTargetUrl(input) {
return String(input);
}

export async function runWithProxyContext(proxyConfig, fn) {
export async function runWithProxyContext(
proxyConfig,
fn,
opts?: { directFallbackOnUnreachable?: boolean }
) {
if (typeof fn !== "function") {
throw new TypeError("runWithProxyContext requires a callback function");
}
Expand All @@ -212,6 +216,15 @@ export async function runWithProxyContext(proxyConfig, fn) {

const resolvedProxyUrl = effectiveProxyConfig ? proxyConfigToUrl(effectiveProxyConfig) : null;

// When set, a proxy that fails the reachability/family pre-checks degrades to a
// DIRECT connection instead of throwing. Use for control-plane operations (OAuth,
// connection tests, token refresh) where reaching the upstream matters more than
// egress-IP pinning — a dead pinned proxy must not surface as a generic 500. Data
// plane (chat) keeps the strict behaviour so per-account IP isolation is preserved.
const directFallbackOnUnreachable = opts?.directFallbackOnUnreachable === true;
// Run fn with the proxy context cleared so the request egresses directly.
const runDirect = () => proxyContext.run(null, fn);

// T14: Proxy Fast-Fail
// Perform a short TCP reachability check before issuing upstream requests.
// Skip for vercel-relay type: proxyConfigToUrl returns "https://<host>" which is the
Expand All @@ -221,6 +234,12 @@ export async function runWithProxyContext(proxyConfig, fn) {
const reachable = await isProxyReachable(resolvedProxyUrl);
if (!reachable) {
const proxyLabel = proxyUrlForLogs(resolvedProxyUrl);
if (directFallbackOnUnreachable) {
console.warn(
`[ProxyFetch] Proxy unreachable (${proxyLabel}); using a direct connection for this request.`
);
return runDirect();
}
const err = new Error(`[Proxy Fast-Fail] Proxy unreachable: ${proxyLabel}`) as Error & {
code?: string;
statusCode?: number;
Expand All @@ -244,6 +263,12 @@ export async function runWithProxyContext(proxyConfig, fn) {
await assertHostnameSupportsFamily(u.hostname, fam === "ipv6" ? 6 : 4);
}
} catch (familyErr) {
if (directFallbackOnUnreachable) {
console.warn(
`[ProxyFetch] Proxy family pre-check failed (${proxyUrlForLogs(resolvedProxyUrl)}); using a direct connection for this request.`
);
return runDirect();
}
const e = familyErr as Error & { code?: string; statusCode?: number };
e.code = e.code || "PROXY_FAMILY_UNAVAILABLE";
e.statusCode = e.statusCode || 503;
Expand All @@ -261,6 +286,19 @@ export async function runWithProxyContext(proxyConfig, fn) {
});
}

/**
* Like {@link runWithProxyContext}, but if the assigned proxy is unreachable or fails
* its pre-checks the request degrades to a DIRECT connection instead of throwing.
*
* For control-plane flows — OAuth code/token exchange, connection tests, token refresh —
* where a dead pinned proxy must not block reaching the upstream (it otherwise surfaces
* as a generic "Internal server error"). Data-plane chat keeps strict pinning via
* runWithProxyContext so per-account egress-IP isolation is preserved.
*/
export async function runWithProxyContextOrDirect(proxyConfig, fn) {
return runWithProxyContext(proxyConfig, fn, { directFallbackOnUnreachable: true });
}

async function patchedFetch(
input: RequestInfo | URL,
options: FetchWithDispatcherOptions = {},
Expand Down
22 changes: 12 additions & 10 deletions src/app/api/oauth/[provider]/[action]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/lib/cloudSync";
import { startLocalServer } from "@/lib/oauth/utils/server";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts";
import {
jsonObjectSchema,
oauthDeviceCompleteSchema,
Expand Down Expand Up @@ -166,7 +166,7 @@ export async function GET(
error:
"GitLab Duo OAuth is not configured. Register an OAuth application at " +
"https://gitlab.com/-/profile/applications with redirect URI " +
"http://localhost:20128/callback and scopes \"ai_features read_user\", then set " +
'http://localhost:20128/callback and scopes "ai_features read_user", then set ' +
"GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.",
});
}
Expand Down Expand Up @@ -211,15 +211,17 @@ export async function GET(
ssoOidcEndpoint: `https://oidc.${region}.amazonaws.com`,
};

deviceData = await runWithProxyContext(proxy, () =>
deviceData = await runWithProxyContextOrDirect(proxy, () =>
(requestDeviceCode as any)(provider, null, providerOverrideConfig)
);
} else {
deviceData = await runWithProxyContext(proxy, () => (requestDeviceCode as any)(provider));
deviceData = await runWithProxyContextOrDirect(proxy, () =>
(requestDeviceCode as any)(provider)
);
}
} else {
// Qwen and other providers use PKCE
deviceData = await runWithProxyContext(proxy, () =>
deviceData = await runWithProxyContextOrDirect(proxy, () =>
requestDeviceCode(provider, authData.codeChallenge)
);
}
Expand Down Expand Up @@ -454,7 +456,7 @@ export async function POST(
const proxy = await resolveProxyForProvider(provider);

// Exchange code for tokens (through proxy if configured)
const tokenData = await runWithProxyContext(proxy, () =>
const tokenData = await runWithProxyContextOrDirect(proxy, () =>
exchangeTokens(provider, code, redirectUri, codeVerifier, normalizedState)
);

Expand Down Expand Up @@ -527,20 +529,20 @@ export async function POST(
let result;
if (provider === "github" || provider === "kimi-coding" || provider === "kilocode") {
// For providers that don't use PKCE (GitHub, Kimi Coding, KiloCode), don't pass codeVerifier
result = await runWithProxyContext(proxy, () =>
result = await runWithProxyContextOrDirect(proxy, () =>
(pollForToken as any)(provider, deviceCode)
);
} else if (provider === "kiro" || provider === "amazon-q") {
// Kiro needs extraData (clientId, clientSecret) from device code response
result = await runWithProxyContext(proxy, () =>
result = await runWithProxyContextOrDirect(proxy, () =>
(pollForToken as any)(provider, deviceCode, null, extraData)
);
} else {
// Qwen and other providers use PKCE
if (!codeVerifier) {
return NextResponse.json({ error: "Missing code verifier" }, { status: 400 });
}
result = await runWithProxyContext(proxy, () =>
result = await runWithProxyContextOrDirect(proxy, () =>
(pollForToken as any)(provider, deviceCode, codeVerifier)
);
}
Expand Down Expand Up @@ -675,7 +677,7 @@ export async function POST(
const proxy = await resolveProxyForProvider(provider);

// Exchange code for tokens (through proxy if configured)
const tokenData = await runWithProxyContext(proxy, () =>
const tokenData = await runWithProxyContextOrDirect(proxy, () =>
exchangeTokens(provider, params.code, redirectUri, codeVerifier, params.state)
);

Expand Down
4 changes: 2 additions & 2 deletions src/app/api/providers/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { validateProviderApiKey } from "@/lib/providers/validation";
import { getProxyForLevel, resolveProxyForProvider } from "@/lib/localDb";
import { validateProviderApiKeySchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts";

function sanitizeAuditUrl(url: string | null | undefined) {
if (!url) return null;
Expand Down Expand Up @@ -104,7 +104,7 @@ export async function POST(request) {
proxyToUse = providerProxy || globalProxy || null;
}

const result = await runWithProxyContext(proxyToUse || null, () =>
const result = await runWithProxyContextOrDirect(proxyToUse || null, () =>
validateProviderApiKey({
provider,
apiKey,
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/proxy-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import http from "node:http";

import proxyFetch, {
runWithProxyContext,
runWithProxyContextOrDirect,
runWithTlsTracking,
isTlsFingerprintActive,
} from "../../open-sse/utils/proxyFetch.ts";
Expand Down Expand Up @@ -165,3 +166,40 @@ test("runWithProxyContext accepts reachable HTTP proxy endpoints and returns cal
}
);
});

test("runWithProxyContext throws PROXY_UNREACHABLE for an unreachable proxy by default", async () => {
// 127.0.0.1:9 (discard) refuses connections — the proxy is unreachable.
await assert.rejects(
runWithProxyContext({ type: "http", host: "127.0.0.1", port: "9" }, async () => "unreachable"),
/Proxy unreachable/
);
});

test("runWithProxyContext degrades to a direct connection when directFallbackOnUnreachable is set", async () => {
let ran = false;
const result = await runWithProxyContext(
{ type: "http", host: "127.0.0.1", port: "9" },
async () => {
ran = true;
return "direct-ok";
},
{ directFallbackOnUnreachable: true }
);

assert.equal(ran, true, "callback must still run via a direct connection");
assert.equal(result, "direct-ok");
});

test("runWithProxyContextOrDirect runs the callback directly when the proxy is unreachable", async () => {
let ran = false;
const result = await runWithProxyContextOrDirect(
{ type: "http", host: "127.0.0.1", port: "9" },
async () => {
ran = true;
return "ok";
}
);

assert.equal(ran, true);
assert.equal(result, "ok");
});