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
1 change: 1 addition & 0 deletions changelog.d/fixes/14620-applied-proxy-shared-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(sse):** the applied-proxy capture context is shared across duplicated `proxyFetch` module instances, so the proxy journal can tell a received provider status apart from a local failure no matter which server copy serves the dispatch. ([#14620](https://github.com/diegosouzapw/OmniRoute/pull/14620)) — thanks @maxmad64bis
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"_rebaseline_2026_09_23_applied_proxy_shared_context": "Own growth: open-sse/utils/proxyFetch.ts 1287->1294 (+7 = the shared-store key + store type + lazy getter, plus four one-line mechanical swaps at the existing call sites). Irreducible plumbing at the single store-identity chokepoint: every duplicated module copy must resolve the same AsyncLocalStorage instance so the fetch patch installed by one copy reads the capture sink opened by another; the store itself lives in the same file, no new module needed for 7 lines. Covered by tests/unit/proxy-fetch-applied-context.test.ts (3/3: single-copy trap, two-copy capture, sequential isolation) plus the neighbor suites proxyfetch-upstream-status-capture (9/9), proxy-fetch (9/9) and proxy-logs-upstream-status (4/4).",
"_rebaseline_2026_09_22_14069_model_not_in_catalog": "PR #14069 (@RaviTharuma) own growth: a live-catalog miss is now recorded as the model_not_in_catalog skip reason instead of collapsing into the generic availability bucket, so an unknown alias stops being reported as \"no credentials available\" (#14068). Measured on the tree reconciled with release/v3.8.51 @ea3c1226: src/sse/handlers/chat.ts 2559->2560 (+1 = the single modelInfo.errorType === \"model_not_found\" early return inside the existing isModelAvailable callback) and open-sse/services/combo/roundRobinCombo.ts 1261->1263 (+2 = the strict `available !== true` pre-check plus the sticky-target expression, which Prettier printWidth 100 reflows over three lines once it becomes `(await isModelAvailable(...)) === true`). Both files were already frozen exactly at their measured size with zero headroom (chat.ts was tightened to 2559 by #14223 on 2026-09-20), so the growth cannot be absorbed. These are call-site lines threading the new model_not_in_catalog skip reason through the two availability chokepoints and nothing else; the reason itself lives outside the frozen files, all under cap: the ModelAvailabilityResult union and modelAvailabilitySkipReason in open-sse/services/combo/types.ts, the COMBO_SKIP_REASONS entry in decisionTrace.ts and the threading in executeTargetGates.ts. Irreducible. Covered by tests/unit/combo/combo-skipped-targets-summary.test.ts. Structural shrink of both god-files stays tracked in #3501.",
"_rebaseline_2026_09_22_11725_cpa_auth_index": "PR for #11725 own growth: open-sse/handlers/chatCore.ts 6400->6402 (+2). Prettier printWidth 100 keeps the failure-usage cpaAuthIndex property and the readCpaAuthIndex import on their own lines; the streaming and non-streaming call sites stay on the existing endpoint line. The parser, stamp, and label join live in open-sse/handlers/chatCore/cpaTraceAuthIndex.ts (under cap). Covered by tests/unit/cpa-trace-auth-index.test.ts, tests/unit/cpa-auth-index-usage.test.ts, and tests/unit/db/migration-185-cpa-auth-index.test.ts.",
"_rebaseline_2026_09_20_14223_rotation_attribution_growth": "PR #14223 own growth: rotation attribution diagnostics (masked serving-account id + request correlation id on proxy log rows, per-account rotation state, skip lines). Re-measured on the tree reconciled with release/v3.8.51 @59de50e4 (which brought #14149 -- the MuseSpark block and the per-request format/session context moved out of opencode.ts -- #14226 and #14464): open-sse/executors/opencode.ts 1251->1318 gate count (re-measured again after #14353 moved the member list off the instance) (snapshotEntries + logSkippedCooldownAccounts + attribution wiring at the nine existing rotation exits incl. the two park-and-replay returns; logic kept at the rotation seam), src/sse/handlers/chat.ts 2547->2559 (attribution sink type + flag read + two forwarded fields at the existing log call-site), src/sse/handlers/chatHelpers.ts 1253->1257 (+4, two additive optional params forwarded to the journal row), open-sse/utils/proxyFetch.ts 1268->1287 (+19, AppliedProxySink rotationAccount field + noteRotationAccount helper). Irreducible spec wiring at existing chokepoints, additive and flag-off inert. Covered by 6 new test files (17 tests).",
Expand Down Expand Up @@ -495,7 +496,7 @@
"open-sse/services/combo/executeTargetAttempt.ts": 1273,
"open-sse/translator/response/openai-responses.ts": 1518,
"open-sse/utils/cursorAgentProtobuf.ts": 1588,
"open-sse/utils/proxyFetch.ts": 1287,
"open-sse/utils/proxyFetch.ts": 1294,
"open-sse/utils/stream.ts": 3239,
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/browser-worker.ts": 4398,
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1335,
Expand Down
17 changes: 12 additions & 5 deletions open-sse/utils/proxyFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,14 @@ export type AppliedProxySink = {
/** Masked serving-account id (N112) — set by the rotation executor at dispatch. */
rotationAccount?: string | null;
};
const appliedProxyContext = new AsyncLocalStorage<AppliedProxySink>();
const APPLIED_PROXY_CONTEXT_KEY = Symbol.for("omniroute.proxyFetch.applied-context");
type AppliedProxyStore = typeof globalThis & {
[APPLIED_PROXY_CONTEXT_KEY]?: AsyncLocalStorage<AppliedProxySink>;
};
function getAppliedProxyContext(): AsyncLocalStorage<AppliedProxySink> {
return ((globalThis as AppliedProxyStore)[APPLIED_PROXY_CONTEXT_KEY] ??=
new AsyncLocalStorage<AppliedProxySink>());
}

/**
* Run `fn` with an applied-proxy capture sink in context. Any
Expand All @@ -205,7 +212,7 @@ const appliedProxyContext = new AsyncLocalStorage<AppliedProxySink>();
* resolves. Pure plumbing — no behavioral change to the request itself.
*/
export function runWithAppliedProxyCapture<T>(sink: AppliedProxySink, fn: () => T): T {
return appliedProxyContext.run(sink, fn);
return getAppliedProxyContext().run(sink, fn);
}

/**
Expand All @@ -215,7 +222,7 @@ export function runWithAppliedProxyCapture<T>(sink: AppliedProxySink, fn: () =>
*/
export function noteRotationAccount(masked: string): void {
try {
const sink = appliedProxyContext.getStore();
const sink = getAppliedProxyContext().getStore();
if (sink) sink.rotationAccount = masked;
} catch {
/* attribution is best-effort; never break the request path */
Expand Down Expand Up @@ -684,7 +691,7 @@ export async function runWithProxyContext(
// otherwise leave proxyInfo reading "direct"). Innermost runWithProxyContext
// wins, which is exactly the per-account proxy the executor selected.
if (effectiveProxyConfig) {
const sink = appliedProxyContext.getStore();
const sink = getAppliedProxyContext().getStore();
if (sink) sink.proxy = effectiveProxyConfig;
}

Expand Down Expand Up @@ -1192,7 +1199,7 @@ async function patchedFetchUnrecorded(
throw lastProxyError;
}

const getAppliedProxySink = () => appliedProxyContext.getStore();
const getAppliedProxySink = () => getAppliedProxyContext().getStore();
const patchedFetch = withUpstreamStatusCapture(patchedFetchUnrecorded, getAppliedProxySink);

/**
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/proxy-fetch-applied-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import test from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import type { AddressInfo } from "node:net";

// Two module copies share the applied-proxy capture: a fetch installed by copy A
// must see the capture sink opened by copy B. This mirrors production, where the
// module is bundled once per server chunk, so a provider dispatch served by any
// copy records its received status on whichever copy opened the capture.
// Single-copy coverage alone cannot catch this: it passes before and after.
// Note: serverless runtimes skip the global patch, so there is no capture there
// by design; this test only covers the patched path.

const MODULE_PATH = "../../open-sse/utils/proxyFetch.ts";
const { runWithCapture } = await import("../../open-sse/utils/providerRequestLogging.ts");

const capture = { capture: () => {}, body: (fallback: unknown) => fallback };
let server: http.Server;
let baseUrl = "";

test.before(async () => {
server = http.createServer((req, res) => {
const params = new URL(req.url ?? "/", "http://local").searchParams;
res.writeHead(Number(params.get("code") ?? 200), { "content-type": "application/json" });
res.end("{}");
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
});

test.after(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});

type Sink = { proxy: unknown; upstreamStatus?: number };

async function dispatch(fn: () => Promise<Response>) {
await runWithCapture(capture, async () => {
const res = await fn();
await res.text();
});
}

test("a single copy records the received status", async () => {
const solo = await import(`${MODULE_PATH}?graph=solo`);
const sink: Sink = { proxy: null };
await solo.runWithAppliedProxyCapture(sink, () =>
dispatch(() => fetch(`${baseUrl}/v1?code=429`))
);
assert.equal(sink.upstreamStatus, 429);
});

test("a fetch from copy A records the status on a sink opened by copy B", async () => {
// Distinct specifiers → two genuinely separate module records.
const copyA = await import(`${MODULE_PATH}?graph=a`);
const copyB = await import(`${MODULE_PATH}?graph=b`);
const sink: Sink = { proxy: null };
await copyB.runWithAppliedProxyCapture(sink, () =>
dispatch(() => copyA.proxyFetch(`${baseUrl}/v1?code=429`))
);
assert.equal(sink.upstreamStatus, 429);
});

test("sequential captures keep their own status, a failed fetch leaves none", async () => {
const copyA = await import(`${MODULE_PATH}?graph=c`);
const copyB = await import(`${MODULE_PATH}?graph=d`);
const first: Sink = { proxy: null };
await copyB.runWithAppliedProxyCapture(first, () =>
dispatch(() => copyA.proxyFetch(`${baseUrl}/v1?code=429`))
);
assert.equal(first.upstreamStatus, 429);

const second: Sink = { proxy: null };
await copyB.runWithAppliedProxyCapture(second, () =>
dispatch(() => copyA.proxyFetch(`${baseUrl}/v1?code=200`))
);
assert.equal(second.upstreamStatus, 200);

const failed: Sink = { proxy: null };
await copyB
.runWithAppliedProxyCapture(failed, () =>
dispatch(() => copyA.proxyFetch("http://127.0.0.1:1/v1"))
)
.catch(() => {});
assert.equal(failed.upstreamStatus, undefined);
});
Loading