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:
diegosouzapw
2026-06-05 15:38:06 -03:00
parent 4fcc16fc6a
commit 75bccccbef
8 changed files with 124 additions and 97 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]),

View File

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

View File

@@ -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 () => {

View File

@@ -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"), {