mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
committed by
GitHub
parent
c29e83a0ba
commit
01c8f2d3dd
@@ -9,6 +9,7 @@
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(intelligence): run pricing + models.dev sync from the live startup path** — like the Arena ELO sync (v3.8.24), the external **pricing sync** (`PRICING_SYNC_ENABLED`) and the **models.dev capability sync** (Settings → AI toggle) were only initialized from `server-init.ts`, which the Next standalone runtime never executes — and models.dev had no caller at all. Their toggles were inert in production. Both are now initialized from `instrumentation-node.ts` (self-gated, opt-in preserved, non-blocking, never fatal). (thanks @diegosouzapw)
|
||||
- **test(proxy): cover the Vercel-relay `proxyFetch` path** — net-new tests for `buildVercelRelayHeaders` and the `vercel`-type relay short-circuit (`x-relay-target`/`-path`/`-auth`, TCP-skip, missing-auth fail-closed), closing one of the deferred test gaps tracked in [#2743](https://github.com/diegosouzapw/OmniRoute/issues/2743). (thanks @diegosouzapw)
|
||||
- **fix(cli): surface `omniroute runtime repair` in the native-module error messages** — after a Node major upgrade, `better-sqlite3`'s prebuilt binary mismatches the ABI and the service can crash-loop; the error only mentioned `npm rebuild better-sqlite3` (which fails for global / no-toolchain installs). The startup + SQLite error hints now also point to the existing self-heal command `omniroute runtime repair` (rebuilds into a user-writable runtime), and a top-level `omniroute repair` alias was added. ([#3476](https://github.com/diegosouzapw/OmniRoute/issues/3476) — thanks @Rahulsharma0810)
|
||||
- **fix(antigravity): per-request Pro-family upstream-id fallback chain (`gemini-3.1-pro-high` 400)** — Antigravity silently renamed the Gemini 3.1 Pro-high upstream id, so `gemini-3.1-pro-high` started returning HTTP 400 (while `-low` still worked) and the live id can't be determined statically (competitor proxies disagree). The executor now retries alternative ids on a 400 (`gemini-3.1-pro-high` → `gemini-pro-agent` → `gemini-3-pro-high`, analogous for pro-low), bounded and only on a 400, with zero extra cost on the happy path; the 1:1 tier-passthrough invariant is preserved (the chain is request-time, not a static alias remap). ([#3786](https://github.com/diegosouzapw/OmniRoute/issues/3786) — thanks @aliaksandrsen)
|
||||
- **fix(sse): retry once on an early stream close (`STREAM_EARLY_EOF`) for single-model requests** — flaky OpenAI-compatible upstreams (e.g. NVIDIA NIM with minimax-m3 / qwen3.5 / glm-5.1) intermittently send HTTP 200 then close the SSE with zero useful frames, surfacing as a 502 "Stream ended before producing useful content". Only Antigravity got an early-close retry; every other provider returned the 502 immediately on the non-combo single-model path. A bounded one-retry (early-close only — not readiness-timeout — and without marking the account unavailable) now generalizes it. (The separate qwen-web validation SSRF part of the same report was already fixed in v3.8.24, [#3767](https://github.com/diegosouzapw/OmniRoute/pull/3767).) ([#3758](https://github.com/diegosouzapw/OmniRoute/issues/3758) — thanks @Svatosalav)
|
||||
|
||||
182
tests/unit/proxyfetch-vercel-relay-2743.test.ts
Normal file
182
tests/unit/proxyfetch-vercel-relay-2743.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// #2743 (gap c — deferred test debt): the Vercel-relay dispatch path had no direct
|
||||
// coverage. `tests/unit/proxy-fetch.test.ts` exercises the HTTP/SOCKS/TLS-context
|
||||
// branches but never the relay short-circuit, and `proxy-registry.test.ts` only
|
||||
// asserts the DB schema for `type:vercel`. This file covers:
|
||||
// 1. `buildVercelRelayHeaders` as a pure function (header shape + values).
|
||||
// 2. The relay short-circuit in `proxyFetch` — that a `vercel`-type proxy context
|
||||
// rewrites the request to the relay endpoint with the relay headers and never
|
||||
// touches the TCP/proxy-dispatcher path.
|
||||
//
|
||||
// IMPORTANT: the relay branch fires `originalFetch(...)`, which is captured from
|
||||
// `globalThis.fetch` at module-load time (proxyFetch.ts line ~54/63). To intercept it
|
||||
// deterministically we install a fetch spy on `globalThis.fetch` BEFORE the first
|
||||
// import of the proxyFetch module, then dynamically import it so `originalFetch`
|
||||
// resolves to our spy. The module also reassigns `globalThis.fetch = patchedFetch`
|
||||
// during init; we capture `proxyFetch` (the named export) which calls `patchedFetch`
|
||||
// directly, so the spy stays the relay-branch sink and is not shadowed.
|
||||
|
||||
// --- Install the relay sink BEFORE importing the module under test. ---
|
||||
type FetchCall = { input: unknown; init: RequestInit & { headers?: HeadersInit } };
|
||||
const relayCalls: FetchCall[] = [];
|
||||
const realGlobalFetch = globalThis.fetch;
|
||||
|
||||
// The spy stands in for the native (pre-patch) fetch that the relay branch calls.
|
||||
// It records the call and returns a canned Response so no network I/O happens.
|
||||
const relaySink = (async (input: unknown, init: RequestInit = {}) => {
|
||||
relayCalls.push({ input, init });
|
||||
return Response.json({ via: "vercel-relay" });
|
||||
}) as unknown as typeof globalThis.fetch;
|
||||
|
||||
globalThis.fetch = relaySink;
|
||||
|
||||
// Dynamic import AFTER the stub so `originalFetch` (captured at module init) === relaySink.
|
||||
const proxyDispatcher = await import("../../open-sse/utils/proxyDispatcher.ts");
|
||||
const { buildVercelRelayHeaders } = proxyDispatcher;
|
||||
const proxyFetchMod = await import("../../open-sse/utils/proxyFetch.ts");
|
||||
const { proxyFetch, runWithProxyContext } = proxyFetchMod;
|
||||
|
||||
test.after(() => {
|
||||
// Restore whatever the module installed (or the real native fetch) so we never
|
||||
// leave a global spy behind for sibling tests sharing the process.
|
||||
globalThis.fetch = realGlobalFetch;
|
||||
});
|
||||
|
||||
test.beforeEach(() => {
|
||||
relayCalls.length = 0;
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 1. buildVercelRelayHeaders — pure function
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
test("buildVercelRelayHeaders splits target into origin + path/query and forwards auth", () => {
|
||||
const headers = buildVercelRelayHeaders(
|
||||
"https://api.anthropic.com/v1/messages?beta=true",
|
||||
"secret-relay-token"
|
||||
);
|
||||
|
||||
assert.deepEqual(headers, {
|
||||
"x-relay-target": "https://api.anthropic.com",
|
||||
"x-relay-path": "/v1/messages?beta=true",
|
||||
"x-relay-auth": "secret-relay-token",
|
||||
});
|
||||
});
|
||||
|
||||
test("buildVercelRelayHeaders preserves a non-default port in the target origin", () => {
|
||||
const headers = buildVercelRelayHeaders("https://upstream.example.com:8443/v1/chat", "tok");
|
||||
|
||||
// URL.host includes the port (URL.hostname would drop it), so the relay edge
|
||||
// function reconstructs the exact origin including the explicit port.
|
||||
assert.equal(headers["x-relay-target"], "https://upstream.example.com:8443");
|
||||
assert.equal(headers["x-relay-path"], "/v1/chat");
|
||||
assert.equal(headers["x-relay-auth"], "tok");
|
||||
});
|
||||
|
||||
test("buildVercelRelayHeaders yields a bare '/' path when the target has no path", () => {
|
||||
const headers = buildVercelRelayHeaders("https://api.openai.com", "tok2");
|
||||
|
||||
// new URL("https://host").pathname === "/" and .search === "".
|
||||
assert.equal(headers["x-relay-target"], "https://api.openai.com");
|
||||
assert.equal(headers["x-relay-path"], "/");
|
||||
assert.equal(headers["x-relay-auth"], "tok2");
|
||||
});
|
||||
|
||||
test("buildVercelRelayHeaders carries the relayAuth verbatim (empty string allowed at this layer)", () => {
|
||||
// The helper itself does not validate relayAuth — it copies it verbatim.
|
||||
// The missing-relayAuth GUARD lives in proxyFetch's relay branch (tested below),
|
||||
// which throws before this helper is ever called. This asserts the helper's
|
||||
// actual contract rather than a behavior it does not own.
|
||||
const headers = buildVercelRelayHeaders("https://api.example.com/x", "");
|
||||
assert.equal(headers["x-relay-auth"], "");
|
||||
assert.equal(headers["x-relay-target"], "https://api.example.com");
|
||||
assert.equal(headers["x-relay-path"], "/x");
|
||||
});
|
||||
|
||||
test("buildVercelRelayHeaders throws on an unparsable target URL", () => {
|
||||
// It delegates to `new URL(targetUrl)`, which throws on garbage input.
|
||||
assert.throws(() => buildVercelRelayHeaders("not-a-url", "tok"), /Invalid URL/);
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 2. proxyFetch relay short-circuit (dispatch decision + header rewrite)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
const VERCEL_CTX = {
|
||||
type: "vercel" as const,
|
||||
host: "omniroute-relay-abc123.vercel.app",
|
||||
relayAuth: "live-relay-secret",
|
||||
};
|
||||
|
||||
test("proxyFetch routes a vercel-type context through the relay endpoint with relay headers", async () => {
|
||||
const response = await runWithProxyContext(VERCEL_CTX, () =>
|
||||
proxyFetch("https://api.anthropic.com/v1/messages?x=1", {
|
||||
method: "POST",
|
||||
headers: { "x-existing": "keep-me" },
|
||||
})
|
||||
);
|
||||
|
||||
// The canned relay response proves the relay sink (originalFetch) was hit and the
|
||||
// TCP/proxy-dispatcher path was skipped (a real dispatcher would have failed to
|
||||
// connect to the fake host).
|
||||
assert.deepEqual(await response.json(), { via: "vercel-relay" });
|
||||
|
||||
assert.equal(relayCalls.length, 1, "exactly one relay dispatch");
|
||||
const call = relayCalls[0];
|
||||
|
||||
// The request is rewritten to the relay edge endpoint, NOT the upstream target.
|
||||
assert.equal(call.input, "https://omniroute-relay-abc123.vercel.app");
|
||||
|
||||
const sentHeaders = new Headers(call.init.headers);
|
||||
// Relay headers carry the real upstream routing.
|
||||
assert.equal(sentHeaders.get("x-relay-target"), "https://api.anthropic.com");
|
||||
assert.equal(sentHeaders.get("x-relay-path"), "/v1/messages?x=1");
|
||||
assert.equal(sentHeaders.get("x-relay-auth"), "live-relay-secret");
|
||||
// Pre-existing caller headers are preserved (merged, not dropped).
|
||||
assert.equal(sentHeaders.get("x-existing"), "keep-me");
|
||||
// The original request method survives the rewrite.
|
||||
assert.equal(call.init.method, "POST");
|
||||
// duplex:"half" is set so a streamed request body is allowed by undici/native fetch.
|
||||
assert.equal((call.init as { duplex?: string }).duplex, "half");
|
||||
});
|
||||
|
||||
test("proxyFetch throws (without dispatching) when a vercel context is missing relayAuth", async () => {
|
||||
await assert.rejects(
|
||||
runWithProxyContext({ type: "vercel", host: "relay.vercel.app" }, () =>
|
||||
proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" })
|
||||
),
|
||||
/Vercel relay configuration error: missing relayAuth/
|
||||
);
|
||||
|
||||
// Fail-closed: no request was sent to the relay endpoint.
|
||||
assert.equal(relayCalls.length, 0, "no relay dispatch when relayAuth is missing");
|
||||
});
|
||||
|
||||
test("the missing-relayAuth error message does not leak internal [ProxyFetch] diagnostics", async () => {
|
||||
// Guards the comment at proxyFetch.ts: the throw can bubble into response bodies,
|
||||
// so it must stay free of internal labels and stack-trace markers.
|
||||
await runWithProxyContext({ type: "vercel", host: "relay.vercel.app" }, async () => {
|
||||
try {
|
||||
await proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" });
|
||||
assert.fail("expected the relay branch to throw on missing relayAuth");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
assert.ok(!message.includes("[ProxyFetch]"), "no internal [ProxyFetch] label");
|
||||
assert.ok(!message.includes("at /"), "no stack-trace path leaked");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("a non-vercel proxy context never takes the relay branch", async () => {
|
||||
// Sanity guard on the dispatch decision: a plain http proxy context must NOT be
|
||||
// routed through the relay sink. runWithProxyContext fast-fails the unreachable
|
||||
// bogus proxy (PROXY_UNREACHABLE) before fn() runs — that throw is expected and
|
||||
// swallowed here; the point is that the relay sink stays untouched either way.
|
||||
await runWithProxyContext({ type: "http", host: "127.0.0.1", port: "9" }, async () => {
|
||||
await proxyFetch("https://api.anthropic.com/v1/messages", { method: "POST" });
|
||||
}).catch(() => undefined);
|
||||
|
||||
assert.equal(relayCalls.length, 0, "http proxy context must not hit the vercel relay sink");
|
||||
});
|
||||
Reference in New Issue
Block a user