mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
chore(release): finalize v3.8.11 changelog + repair release-gate test drift
Finalize the 3.8.11 cycle CHANGELOG and clear the failures the full test:unit gate surfaced (release-branch drift — PR merges bypassed pre-push): - CHANGELOG: date the [3.8.11] section (2026-06-05) + repo-housekeeping roll-up - docs(env): document THEOLDLLM_NAV_TIMEOUT_MS in .env.example + ENVIRONMENT.md (env-doc-sync gate; #3217 added the var without docs) - test(nvidia): exercise the #3226 bypass-fetch path via a local HTTP server and hoist the validator import (patching globalThis.fetch no longer intercepts the un-patched native fetch captured by proxyFetch) - test(i18n): import the shipped normalizeComplianceEventTypes helper (#3185) - test(model-caps): save synced metadata under the canonical gemini-3.1-pro key now that #3229 aliases gemini-3.1-pro-high/-low to it - test(web-session): expect the grok-web "sso + sso-rw" credential hint (#3180) - test(synced-models): isolate DATA_DIR so the #3199 hidden-override stops bleeding into the shared DB and breaking the re-run precondition
This commit is contained in:
@@ -585,6 +585,11 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
# Trae OAuth token override. Used by: open-sse/executors/trae.ts.
|
||||
# TRAE_TOKEN=
|
||||
|
||||
# ── The Old LLM (theoldllm) ──
|
||||
# Playwright navigation timeout (ms) for the browser-backed token capture.
|
||||
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
|
||||
# THEOLDLLM_NAV_TIMEOUT_MS=30000
|
||||
|
||||
# ── Gemini / Gemini CLI / Antigravity / Windsurf (all Google-based) ──
|
||||
# These providers ship public OAuth client_id/secret values (or Firebase Web
|
||||
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.11] — Unreleased
|
||||
|
||||
_Development cycle in progress — entries are added as work merges into `release/v3.8.11` and finalized by the release flow._
|
||||
## [3.8.11] — 2026-06-05
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -44,6 +42,7 @@ _Development cycle in progress — entries are added as work merges into `releas
|
||||
|
||||
- **build:** finish the build-output-isolation cleanup — `assembleStandalone.mjs` now derives both its async (`syncStandalone*`) and sync copy paths from a single `NATIVE_ASSET_ENTRIES`/`EXTRA_MODULE_ENTRIES` source of truth (previously two hand-maintained lists that could silently drift), guarded by a new parity test; and the `Dockerfile` drops 5 redundant per-module `COPY` overrides (`@swc/helpers`, `pino-abstract-transport`, `pino-pretty`, `split2`, `migrations`) now that `assembleStandalone` bundles them into the standalone regardless of NFT/Turbopack tracing (validated with a real Turbopack `docker build` + boot → `/api/monitoring/health` 200; `better-sqlite3` stays explicit since only its native `build/` is synced) (#3187 — thanks @diegosouzapw)
|
||||
- **combo:** add a regression guard asserting the same-provider cascade is short-circuited by the connection-cooldown layer (#3200 — thanks @diegosouzapw)
|
||||
- **repo:** housekeeping — ignore the generated `coverage/` output dir and prune deprecated `.agents/skills/*` SKILL definitions superseded by the current workflow skills (thanks @diegosouzapw)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | `open-sse/executors/chatgpt-web.ts` | Browser-facing OmniRoute origin used for image URLs in API responses (e.g., `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/<id>`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. |
|
||||
| `THEOLDLLM_NAV_TIMEOUT_MS` | `30000` (30s) | `open-sse/executors/theoldllm.ts` | Playwright navigation timeout (ms) for the browser-backed token capture used by the The Old LLM (theoldllm) free provider. Raise on slow networks if the relay page is slow to settle. |
|
||||
| `KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public callback URL for asynchronous kie.ai jobs. Highest-priority override before `OMNIROUTE_KIE_CALLBACK_URL` and `OMNIROUTE_PUBLIC_URL`. |
|
||||
| `OMNIROUTE_KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Alternate spelling of `KIE_CALLBACK_URL`. Falls back when the primary variable is unset. |
|
||||
| `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. |
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { nestDottedKeys } from "@/i18n/request";
|
||||
// The shipped helper is `normalizeComplianceEventTypes` (#3185); it nests dotted
|
||||
// keys under `compliance.eventTypes` and is a no-op for messages without that path.
|
||||
import { normalizeComplianceEventTypes as nestDottedKeys } from "@/i18n/request";
|
||||
|
||||
test("nestDottedKeys expands flat compliance.eventTypes keys into nested objects", () => {
|
||||
const input = {
|
||||
|
||||
@@ -68,9 +68,10 @@ test("canonical model capability resolver lets exact synced metadata override gl
|
||||
}),
|
||||
},
|
||||
antigravity: {
|
||||
// The resolver returns "gemini-3.1-pro-high" unchanged (ANTIGRAVITY_MODEL_ALIASES only maps
|
||||
// the public-facing alias → internal, not the reverse). Save under the canonical resolved key.
|
||||
"gemini-3.1-pro-high": buildCapability({
|
||||
// Since #3229, ANTIGRAVITY_MODEL_ALIASES maps both "gemini-3.1-pro-high" and
|
||||
// "gemini-3.1-pro-low" → "gemini-3.1-pro", so the capability resolver looks
|
||||
// synced metadata up under the canonical "gemini-3.1-pro" key. Save it there.
|
||||
"gemini-3.1-pro": buildCapability({
|
||||
tool_call: false,
|
||||
reasoning: false,
|
||||
modalities_input: JSON.stringify(["text"]),
|
||||
|
||||
@@ -1,99 +1,107 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import http from "node:http";
|
||||
|
||||
// #2463 — NVIDIA NIM validation must not crash with `e.startsWith is not a function`
|
||||
// when providerSpecificData has malformed shapes; and the validation must use a
|
||||
// direct chat probe instead of the /models probe.
|
||||
//
|
||||
// #3226 — the validator now probes via `directHttpsRequest` → `safeOutboundFetch`
|
||||
// with `bypassProxyPatch: true`, which uses the ORIGINAL (un-patched) native fetch
|
||||
// captured at module load. Patching `globalThis.fetch` in the test no longer
|
||||
// intercepts it, so we point `baseUrl` at a real local HTTP server instead (the
|
||||
// outbound guard is "none" for validation, so 127.0.0.1 is reachable). This
|
||||
// exercises the true code path end-to-end without a real upstream call.
|
||||
//
|
||||
// IMPORTANT: import the validator at FILE LOAD (top-level), not lazily inside a
|
||||
// test. `proxyFetch` captures the un-patched `globalThis.fetch` on its first
|
||||
// import; if the first import happened inside a test that had already patched
|
||||
// `globalThis.fetch`, `getOriginalFetch()` would forever return that test's mock
|
||||
// and poison every later bypass call. Loading here pins the real native fetch.
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
|
||||
async function withMockServer(
|
||||
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
||||
fn: (baseUrl: string) => Promise<void>
|
||||
): Promise<void> {
|
||||
const server = http.createServer(handler);
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address ? address.port : 0;
|
||||
const baseUrl = `http://127.0.0.1:${port}/v1`;
|
||||
try {
|
||||
await fn(baseUrl);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
test("normalizeBaseUrl tolerates non-string baseUrl without throwing", async () => {
|
||||
// Indirect probe — call validation entrypoint with a non-string baseUrl in PSD;
|
||||
// the function should return a normal Validation result (not throw a TypeError
|
||||
// such as `e.startsWith is not a function` after minification — see #2463).
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({}), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-test-key",
|
||||
// Malformed: baseUrl is an object instead of a string. Pre-fix this would
|
||||
// crash inside normalizeBaseUrl with `.trim is not a function`.
|
||||
providerSpecificData: { baseUrl: { not: "a string" } as any },
|
||||
});
|
||||
assert.equal(typeof result, "object");
|
||||
assert.equal(typeof result.valid, "boolean");
|
||||
// Whether the call succeeds (with default baseUrl) or fails gracefully with
|
||||
// an outbound URL guard error is fine — the contract is "no TypeError leak".
|
||||
if (!result.valid && typeof result.error === "string") {
|
||||
assert.ok(
|
||||
!result.error.includes("startsWith"),
|
||||
`error must not mention startsWith TypeError, got: ${result.error}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.error.includes("is not a function"),
|
||||
`error must not mention TypeError, got: ${result.error}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
// Call validation entrypoint with a non-string baseUrl in PSD; the function
|
||||
// should return a normal Validation result (not throw a TypeError such as
|
||||
// `e.startsWith is not a function` after minification — see #2463). A malformed
|
||||
// baseUrl normalizes to "" and yields an invalid relative probe URL, which the
|
||||
// outbound layer rejects gracefully — so no real upstream call is made.
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-test-key",
|
||||
providerSpecificData: { baseUrl: { not: "a string" } as any },
|
||||
});
|
||||
assert.equal(typeof result, "object");
|
||||
assert.equal(typeof result.valid, "boolean");
|
||||
if (!result.valid && typeof result.error === "string") {
|
||||
assert.ok(
|
||||
!result.error.includes("startsWith"),
|
||||
`error must not mention startsWith TypeError, got: ${result.error}`
|
||||
);
|
||||
assert.ok(
|
||||
!result.error.includes("is not a function"),
|
||||
`error must not mention TypeError, got: ${result.error}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("nvidia specialty validator returns Invalid API key on 401", async () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ error: "unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-badkey",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
await withMockServer(
|
||||
(_req, res) => {
|
||||
res.writeHead(401, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "unauthorized" }));
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-badkey",
|
||||
providerSpecificData: { baseUrl },
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("nvidia specialty validator skips /models probe entirely", async () => {
|
||||
const calls: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (url: any) => {
|
||||
calls.push(String(url));
|
||||
return new Response(JSON.stringify({}), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-key",
|
||||
providerSpecificData: {},
|
||||
});
|
||||
assert.equal(result.valid, true);
|
||||
assert.ok(
|
||||
calls.every((u) => !u.endsWith("/v1/models")),
|
||||
`should not call /v1/models, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
assert.ok(
|
||||
calls.some((u) => u.endsWith("/chat/completions")),
|
||||
`should call /chat/completions, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
await withMockServer(
|
||||
(req, res) => {
|
||||
calls.push(String(req.url));
|
||||
res.writeHead(200, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({}));
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: "nv-key",
|
||||
providerSpecificData: { baseUrl },
|
||||
});
|
||||
assert.equal(result.valid, true);
|
||||
assert.ok(
|
||||
calls.every((u) => !u.endsWith("/v1/models")),
|
||||
`should not call /v1/models, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
assert.ok(
|
||||
calls.some((u) => u.endsWith("/chat/completions")),
|
||||
`should call /chat/completions, called: ${JSON.stringify(calls)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
*/
|
||||
import test, { before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import {
|
||||
replaceSyncedAvailableModelsForConnection,
|
||||
getSyncedAvailableModels,
|
||||
mergeModelCompatOverride,
|
||||
} from "../../src/lib/localDb.ts";
|
||||
import { resetDbInstance } from "../../src/lib/db/core.ts";
|
||||
// Hermetic DB: this test marks a model hidden (an override persisted in the
|
||||
// `modelCompatOverrides` key_value namespace). Without an isolated DATA_DIR it
|
||||
// would write that override into the shared dev/CI database and never clean it
|
||||
// up, so the SECOND run would see `model-del` already hidden and the first-sync
|
||||
// precondition would fail. Point DATA_DIR at a throwaway dir before any import
|
||||
// that opens the SQLite connection.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-synced-del-"));
|
||||
process.env.DATA_DIR = tmpDir;
|
||||
|
||||
const { replaceSyncedAvailableModelsForConnection, getSyncedAvailableModels, mergeModelCompatOverride } =
|
||||
await import("../../src/lib/localDb.ts");
|
||||
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
|
||||
|
||||
before(() => {
|
||||
resetDbInstance();
|
||||
@@ -23,6 +32,7 @@ before(() => {
|
||||
|
||||
after(() => {
|
||||
resetDbInstance();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("a hidden (deleted) synced model is not re-added on re-import", async () => {
|
||||
|
||||
@@ -15,10 +15,11 @@ test("web session credential metadata covers every web-cookie provider", () => {
|
||||
});
|
||||
|
||||
test("web session credential metadata identifies cookie, token, and no-auth providers", () => {
|
||||
// Grok needs BOTH sso and sso-rw cookies (#3180)
|
||||
assert.deepEqual(webSessionCredentials.getWebSessionCredentialRequirement("grok-web"), {
|
||||
kind: "cookie",
|
||||
credentialName: "sso",
|
||||
placeholder: "sso=...",
|
||||
credentialName: "sso + sso-rw",
|
||||
placeholder: "sso=...; sso-rw=...",
|
||||
acceptsFullCookieHeader: true,
|
||||
});
|
||||
assert.deepEqual(webSessionCredentials.getWebSessionCredentialRequirement("copilot-web"), {
|
||||
|
||||
Reference in New Issue
Block a user