Compare commits

...

5 Commits

Author SHA1 Message Date
diegosouzapw
f6dffb3913 fix(quality): green release/v3.8.50 unit base-reds (#9985)
8 unit-test base-reds reproducing on the pristine release tip, fixed in-source
(fast-gates PR->release do not run the unit suite, so these accrued silently):
- ServiceSupervisor: spawn-failure now resolves with error status (was throwing);
  health-probe-failure path still rejects. Distinct via spawnFailed flag.
- stream + responseSanitizer: numeric passthrough id preserved as string (was
  regenerated chatcmpl-); finish chunk with empty delta no longer swallowed by
  the emptyChoices guard.
- proxyFetch: genuine (non-abort) proxy transport failures keep the underlying
  reason in the surfaced error.
- auto-combo builtinCatalog: advertised undefined-variant auto/* ids (auto/chat,
  auto/best-chat, auto/pro-chat) materialize instead of throwing 'Unknown'.
- getTranslations en.json: add missing providers.iconUrlInvalid.
- optional-transformers-dependency.test: reconcile to #9962's deliberate
  move of @huggingface/transformers to a regular dep (napi onnxruntime).

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>
2026-08-10 16:34:35 -03:00
Diego Rodrigues de Sa e Souza
d6b1405a5c fix(quality): rebaseline CodeQL ratchet 1->2 for #9940 fingerprint alerts (#9985)
Base-red: 2nd js/insufficient-password-hash alert on chatBodyAdmission API-key
fingerprints (sha256->16-hex admission-lane key), not password verification.
Reproduces on release/v3.8.50 tip. Owner-authorized rebaseline (revisit v3.9.0).
2026-08-10 14:03:16 -03:00
Diego Rodrigues de Sa e Souza
940f38545d fix(quality): allowlist onnxruntime-node in dependency allowlist (#9985)
check:deps base-red — onnxruntime-node is a real production dep (transformers
embedding path) landed via the LLMLingua/transformers bump (#9962) without an
allowlist entry. Legit package: microsoft onnxruntime, verified in registry.
2026-08-10 13:57:00 -03:00
Diego Rodrigues de Sa e Souza
65940dde76 fix(quality): green open-sse+dashboard typecheck base-reds (#9985)
Release-equivalent fast-gates surface 5 real TS regressions inherited by the
base from merged Fal/guardrails/cursor work (fast-gates PR->release do not run
these, so they accrued on release/v3.8.50):
- open-sse/handlers/imageGeneration/providers/fal.ts: normalizeProviderImagePayload
  missing 4th 'b64_json' arg (TS2554).
- open-sse/handlers/videoGeneration/falHandler.ts: narrow video to Record before .url.
- src/app/api/v1/images/generations/route.ts: type the toJsonErrorPayload read.
- src/lib/guardrails/visionBridgeHelpers.ts: cast through unknown for UA fetch.
- src/lib/providers/mergeProviderModelListing.ts: drop index-signature requirement
  that made interface RegistryModel[] unassignable (TS2322, from #9911).

All fixed in source (keeps the gates meaningful); each reproduces on the base tip.

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>
2026-08-10 13:25:20 -03:00
Diego Rodrigues de Sa e Souza
f672ddcc2c fix(quality): green release/v3.8.50 base-reds — env-doc sync + file-size freeze (#9985)
Sweep base-reds from issue #9985 on release/v3.8.50:
- env-doc-sync: add COMMANDCODE_API_URL + ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS to
  .env.example and ENVIRONMENT.md (in code, missing from docs); add
  OMNIROUTE_STRICT_SYSTEM_PROVIDERS + TLS_FINGERPRINT_PROVIDERS to ENVIRONMENT.md
  (in .env.example, missing from doc). Restores the 3-way env contract.
- file-size: freeze open-sse/utils/proxyFetch.ts at 1207 (new proxied-TLS fetch
  helper over the 1000 cap). Owner-authorized quick rebaseline; slim for v3.9.0.

Co-authored-by: OmniRoute maintenance <maintainers@omniroute.local>
2026-08-10 12:19:08 -03:00
18 changed files with 112 additions and 32 deletions

View File

@@ -1173,6 +1173,12 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Or enable for all providers at once:
# CLI_COMPAT_ALL=1
# Allow the Antigravity request translator to skip its strict CLI request-signature
# validation when the upstream refuses real signatures (debug/antiquated-CLI mode).
# Default: real signatures enforced (unset) — signature bypass disabled.
# Used by: open-sse/translator/request/openai-to-gemini.ts
# ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS=0
# ── Kimi Coding CLI identity overrides ──
# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers.
# Leave unset to use the captured defaults baked into the OmniRoute build.
@@ -2003,6 +2009,12 @@ APP_LOG_TO_FILE=true
# Default: 0.33.2
# COMMAND_CODE_VERSION=0.33.2
# Base URL for the Command Code usage/quota upstream, used by smartphone
# quota-fetcher telemetry.
# Used by: open-sse/services/usage/command-code.ts
# Default: https://api.commandcode.ai
# COMMANDCODE_API_URL=https://api.commandcode.ai
# ── MITM debug proxy (development only) ──
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
# MITM_LOCAL_PORT=443

View File

@@ -0,0 +1 @@
- fix(quality): green release/v3.8.50 base-reds — sync 4 env vars into .env.example/ENVIRONMENT.md and freeze the new proxied-TLS proxyFetch helper in the file-size baseline (#9985)

View File

@@ -98,6 +98,7 @@
"omniglyph",
"open",
"opencode-ai",
"onnxruntime-node",
"ora",
"parse5",
"pino",

View File

@@ -420,8 +420,10 @@
"open-sse/executors/default.ts": 1355,
"open-sse/executors/kiro.ts": 1390,
"open-sse/translator/request/openai-to-kiro.ts": 1374,
"open-sse/utils/sseHeartbeat.ts": 194
"open-sse/utils/sseHeartbeat.ts": 194,
"open-sse/utils/proxyFetch.ts": 1207
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) \u2014 owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore \u2014 the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) \u2014 owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* \u2261 codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
"_rebaseline_2026_07_28_8842_antigravity_projectid_refresh": "PR #8842 (fix/antigravity-projectid-refresh) own growth: open-sse/executors/antigravity.ts 1493->1528 (+35 = projectId discovery in refreshCredentials: import ensureAntigravityProjectAssigned + trim projectId + call ensureAntigravityProjectAssigned with 8s timeout + persistDiscoveredAntigravityProjectId + log success/failure). Irreducible wiring at the existing credential-refresh chokepoint. Covered by tests/unit/executor-antigravity.test.ts (4 new test cases).",

View File

@@ -149,10 +149,11 @@
"dedicatedGate": true
},
"codeqlAlerts": {
"value": 1,
"value": 2,
"direction": "down",
"dedicatedGate": true,
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match."
"_rebaseline_2026_08_06_base_grew": "Base branch file-size drift: translator-openai-to-gemini.test.ts grew 1619->1622 (test assertions for Gemini translator compatibility). CodeQL alert (js/insufficient-password-hash in raycast.ts) is pre-existing base-red; incremented baseline to match.",
"_rebaseline_2026_08_10_9940_fingerprint": "CodeQL base-red (green-prs sweep, issue #9985): 2nd js/insufficient-password-hash alert at src/shared/middleware/chatBodyAdmission.ts:265,269 introduced by #9940 (per-connection virtual admission lanes). Both are API-key/bearer FINGERPRINTS (createHash('sha256') truncated to 16-hex admission-lane key), not password VERIFICATION — false-positive class for this rule. Reproduces on release/v3.8.50 tip. Owner-authorized rebaseline 1->2; revisit at v3.9.0."
},
"secretFindings": {
"_note": "Zeroed 2026-07-13 (WS6/D3): the 3 frozen generic-api-key FPs are allowlisted with justification in .gitleaks.toml — any NEW finding regresses the ratchet.",

View File

@@ -340,6 +340,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con
| `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. |
| `PROXY_FAIL_OPEN` | `false` | `src/sse/handlers/chatHelpers.ts` | When `false` (default), a request whose assigned proxy fails to resolve is **refused (fail-closed)** rather than falling back to a direct connection — prevents real-IP leaks. Set `true` to restore the legacy DIRECT fallback. |
| `ENABLE_TLS_FINGERPRINT` | `false` | `open-sse/executors` | Spoof TLS fingerprint using wreq-js (mimics Chrome 124). Counters JA3/JA4 blocking. |
| `TLS_FINGERPRINT_PROVIDERS` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Comma-separated provider allowlist for the new proxied TLS routing (`open-sse/utils/proxyFetch.ts`). Direct TLS keeps its legacy behavior when unset; only these providers route through the Chrome-124 fingerprint bridge. |
| `OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS` | `false` | `open-sse/services/claudeTurnstileSolver.ts` | Allow the Claude Turnstile Playwright browser context to ignore HTTPS certificate errors. |
### Scenarios
@@ -468,6 +469,7 @@ detection above).
| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. |
| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. |
| `ANTIGRAVITY_CREDITS` | `off` | `open-sse/services/antigravityCredits.ts` | Google One AI credits policy: `off` never injects credits, `retry` injects once after an eligible quota 429, and `always` injects on the first request. |
| `ANTIGRAVITY_ALLOW_SIGNATURE_BYPASS` | `0` | `open-sse/translator/request/openai-to-gemini.ts` | Allow the Antigravity request translator to skip its strict CLI request-signature validation when the upstream refuses real signatures (debug/antiquated-CLI mode). Non-zero enables the bypass. |
| `AGY_TOKEN_FILE` | `~/.gemini/antigravity-cli/antigravity-oauth-token` | `src/app/api/providers/agy-auth/apply-local/route.ts` | Override the Antigravity CLI (agy) token-file path for the auto-detect local login import. |
### OAuth CLI Bridge (Internal)
@@ -791,6 +793,7 @@ Embedding layer, vector store and reranking knobs for the persistent memory subs
| `MEMORY_TYPED_DECAY_EPISODIC_DAYS` | `30` | TTL (days) after which an unused `episodic` memory decays. `0` makes episodic immune too. Durable types (`factual`/`procedural`/`semantic`) are always immune. The decay clock re-bases on `last_accessed_at`. |
| `MEMORY_TYPED_DECAY_ACCESS_IMMUNITY` | `3` | A memory injected `>=` this many times becomes immune to decay regardless of type. `0` disables access immunity. |
| `MEMORY_TYPED_DECAY_SWEEP_INTERVAL` | `0` (disabled) | Interval (seconds) for the optional periodic decay sweep in `src/lib/memory/typedDecay.ts`. `0`/unset = no periodic sweep. Doubly opt-in: also requires `MEMORY_TYPED_DECAY_ENABLED=true`. |
| `OMNIROUTE_STRICT_SYSTEM_PROVIDERS` | _(unset)_ | Comma-separated provider ids (case-insensitive) that accept a `system` message **only at index 0** (`src/lib/memory/injection.ts`). For these, the cache-safe mid-array memory splice is unsafe in multi-turn conversations, so memory is merged/prepended as the leading system message instead. Defaults to only `xiaomi-mimo`/`mimo`; extend for self-hosted OpenAI-compatible endpoints (e.g. Qwen3.5+/3.6) whose chat template enforces the same single-leading-system-message constraint. |
### Low-RAM Docker Example
@@ -1092,6 +1095,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
| `OMNIROUTE_EMERGENCY_FALLBACK` | enabled | `open-sse/services/emergencyFallback.ts` | Set `false` (or `0`) to disable the emergency budget-exhaustion fallback that reroutes failed requests to the free `nvidia`/`openai/gpt-oss-120b` model. Effective precedence is Feature Flags DB override > env var > default; if unavailable, the service falls back to the raw env value. |
| `COMMAND_CODE_CALLBACK_PORT` | _(unset)_ | `src/app/api/providers/command-code/auth/shared.ts` | Local port used for OAuth-style callbacks from the Command Code CLI helper. |
| `COMMAND_CODE_VERSION` | `0.33.2` | `open-sse/executors/commandCode.ts` | Value sent as the `x-command-code-version` header to the Command Code upstream. Override to bump the CLI version. |
| `COMMANDCODE_API_URL` | `https://api.commandcode.ai` | `open-sse/services/usage/command-code.ts` | Base URL for the Command Code usage/quota upstream used by the smartphone quota-fetcher telemetry. Override for a self-hosted/alternative Command Code API. |
| `MITM_LOCAL_PORT` | `443` | `src/mitm/server.cjs` | Local bind port for the MITM debug proxy. |
| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). |
| `MITM_IDLE_TIMEOUT_MS` | `60000` | `src/mitm/socketTimeouts.ts`, `src/mitm/server.cjs` | Idle socket timeout (ms) for proxied connections; idle sockets past this are torn down to avoid leaking half-open tunnels. |

View File

@@ -89,7 +89,7 @@ export async function handleFalAIImageEdit({
const payload = await response.json();
const normalizedBody =
body.response_format === undefined ? { ...body, response_format: "b64_json" } : body;
const imagesOut = await normalizeProviderImagePayload(payload, normalizedBody, log);
const imagesOut = await normalizeProviderImagePayload(payload, normalizedBody, log, "b64_json");
return saveImageSuccessResult({
provider,
model: editModel,

View File

@@ -576,15 +576,21 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
/**
* Normalize response ID to use chatcmpl- prefix.
* Preserves numeric/short custom ids as their string form rather than
* regenerating them — a passthrough numeric id (e.g. `123`) must stay `"123"`
* so streaming clients can correlate chunks (#3427/#5776). Only a genuinely
* missing/empty id gets a fresh `chatcmpl-` token.
*/
function normalizeResponseId(id: unknown): string {
if (!id || typeof id !== "string") {
if (!id || (typeof id !== "string" && typeof id !== "number")) {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
// Already correct format
if (id.startsWith("chatcmpl-")) return id;
// Keep custom IDs but don't break them
return id;
const str = String(id);
if (str === "") {
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
}
// Already correct format, or a custom/numeric id — keep it.
return str;
}
function normalizeResponsesId(id: unknown): string {

View File

@@ -90,7 +90,10 @@ function absoluteFalUrl(value: unknown, baseUrl: string): string | undefined {
function normalizeFalVideoResponse(payload: unknown) {
const record = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
const video = record.video && typeof record.video === "object" ? record.video : null;
const video =
record.video && typeof record.video === "object"
? (record.video as Record<string, unknown>)
: null;
const url = video && typeof video.url === "string" ? video.url.trim() : "";
if (!url) {

View File

@@ -182,6 +182,16 @@ export async function createBuiltinAutoCombo(modelStr: string, suffix: string) {
return virtualCombo;
}
// Advertised `auto/*` ids whose template maps to no variant (auto/chat,
// auto/best-chat, auto/pro-chat) still materialize via the default
// (unconstrained) virtual combo rather than throwing "Unknown built-in".
if (Object.prototype.hasOwnProperty.call(AUTO_TEMPLATE_VARIANTS, modelStr)) {
const virtualCombo = await createVirtualAutoCombo(undefined);
virtualCombo.name = modelStr;
virtualCombo.id = modelStr;
return virtualCombo;
}
// #4235 Phase B: `auto/<category>[:<tier>]` (e.g. auto/coding:fast, auto/vision).
const parsed = parseAutoSuffix(suffix);
if (parsed.valid) {

View File

@@ -1096,9 +1096,12 @@ async function patchedFetch(
continue;
}
tagProxyUnreachable(error);
const originalMsg = error instanceof Error ? error.message : String(error);
const sanitized = sanitizeTransportError(
error,
"Proxy request failed",
originalMsg
? `Proxy request failed: ${originalMsg}`
: "Proxy request failed",
"PROXY_REQUEST_FAILED"
);
console.error(

View File

@@ -1640,7 +1640,8 @@ export function createSSEStream(options: StreamOptions = {}) {
(parsed.choices.length === 1 &&
parsed.choices[0]?.delta &&
typeof parsed.choices[0].delta === "object" &&
Object.keys(parsed.choices[0].delta).length === 0))
Object.keys(parsed.choices[0].delta).length === 0 &&
!parsed.choices[0]?.finish_reason))
) {
const emptyChoicesUsage = extractUsage(parsed) ?? parsed.usage;
if (hasValidUsage(emptyChoicesUsage)) {
@@ -1850,6 +1851,7 @@ export function createSSEStream(options: StreamOptions = {}) {
passthroughSawFinishReason = true;
}
if (isFinishChunk && passthroughHasToolCalls) {
toolFinishTime = now;
try {

View File

@@ -307,7 +307,9 @@ async function postHandler(request, context) {
});
}
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error");
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error") as {
error?: { message?: string };
};
const message =
typeof errorPayload?.error?.message === "string"
? errorPayload.error.message

View File

@@ -4948,6 +4948,7 @@
"baseUrlHint": "Required. Provider API base URL.",
"iconUrlLabel": "Icon URL",
"iconUrlHint": "Optional. Image URL shown as this provider's icon.",
"iconUrlInvalid": "Invalid icon URL. Use an http(s):// or data:image/*;base64 URL.",
"anthropicPrefixPlaceholder": "ac-prod",
"openaiPrefixPlaceholder": "oc-prod",
"anthropicBaseUrlPlaceholder": "https://api.anthropic.com/v1",

View File

@@ -222,7 +222,7 @@ const VISION_BRIDGE_UA_FETCH: typeof fetch = ((input: RequestInfo | URL, init?:
"user-agent": "omniroute-vision-bridge",
...((init?.headers as Record<string, string> | undefined) ?? {}),
},
})) as typeof fetch;
})) as unknown as typeof fetch;
/**
* Resolve every image part in the body to a base64 data URI when the target

View File

@@ -19,7 +19,7 @@ export type ProviderListingModel = {
export type MergeProviderModelListingInput = {
providerId: string;
registryModels: Array<{ id: string; name?: string; [key: string]: unknown }>;
registryModels: Array<{ id: string; name?: string }>;
syncedModels: Array<{ id: string; name?: string; [key: string]: unknown }>;
customModels: Array<{ id: string; name?: string; source?: string; [key: string]: unknown }>;
usesCuratedModelsOnly?: boolean;

View File

@@ -46,6 +46,7 @@ export class ServiceSupervisor extends EventEmitter {
private lastError: string | null = null;
private childProcess: ChildProcess | null = null;
private adopted: boolean = false;
private spawnFailed: boolean = false;
private readonly buffer: RingBuffer;
private readonly checker: HealthChecker;
private operationLock: Promise<void> = Promise.resolve();
@@ -158,6 +159,7 @@ export class ServiceSupervisor extends EventEmitter {
child = spawn(command, args, buildServiceSpawnOptions(env, cwd));
} catch (err) {
this.checker.stop();
this.spawnFailed = true;
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
this.lastError = msg;
this.setState("error");
@@ -195,6 +197,7 @@ export class ServiceSupervisor extends EventEmitter {
// the health poller hammers the dead port every healthIntervalMs.
child.once("error", (err) => {
this.checker.stop();
this.spawnFailed = true;
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
this.lastError = msg;
this.setState("error");
@@ -206,6 +209,12 @@ export class ServiceSupervisor extends EventEmitter {
await this.waitForHealthy();
// A spawn failure flips state to "error" — surface the explicit error
// status instead of overriding it with "running".
if (this.state === "error") {
return this.getStatus();
}
this.setState("running");
await setToolStatus(this.config.tool, "running", this.pid ?? undefined);
@@ -260,13 +269,21 @@ export class ServiceSupervisor extends EventEmitter {
while (Date.now() < deadline) {
if (this.checker.getHealth() === "healthy") return;
if (this.state === "error") throw new Error(this.lastError ?? "Service failed to start");
// A spawn failure (child 'error' event or sync throw) flips state to
// "error" — stop polling and let start() surface the explicit error
// status as a resolve. A health-probe failure is a different, harder
// condition and must reject (handled below).
if (this.state === "error") {
if (this.spawnFailed) return;
throw new Error(this.lastError ?? "Service failed to start");
}
await new Promise((r) => setTimeout(r, 1_000));
}
// Timeout reached without a healthy probe. The health poller may have
// flipped the state to "error" while we were waiting (FAILURE_THRESHOLD
// consecutive failures) — surface that instead of a degraded marker.
if (this.state === "error") {
if (this.spawnFailed) return;
throw new Error(this.lastError ?? "Service failed to start");
}
this.lastError = sanitizeErrorMessage(

View File

@@ -9,7 +9,14 @@ function readJson<T = Record<string, unknown>>(relPath: string): T {
return JSON.parse(readFileSync(join(repoRoot, relPath), "utf8")) as T;
}
test("@huggingface/transformers is optional so onnxruntime CUDA install failures cannot abort OmniRoute install", () => {
test("@huggingface/transformers is a regular dependency so npm ci never skips it", () => {
// #9962 deliberately moved @huggingface/transformers out of optionalDependencies:
// as an optional dep, npm silently skipped the whole subtree on Node 24/26 (old
// pin dragged onnxruntime-node@1.21.0 whose NAN build no longer compiles), which
// broke `npm ci`/`next build` with "Can't resolve @huggingface/transformers"
// (lazy import in src/lib/memory/embedding/transformersLocal.ts). As a regular
// dep with onnxruntime-node@~1.24.3 (napi prebuilds, no node-gyp) it stays
// installable and the memory embedding path requires() cleanly.
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
@@ -17,29 +24,37 @@ test("@huggingface/transformers is optional so onnxruntime CUDA install failures
assert.equal(
pkg.dependencies?.["@huggingface/transformers"],
undefined,
"transformers must not be a regular dependency because it pulls onnxruntime-node install scripts"
"^4.2.0",
"transformers must be a regular dependency (never optional) so npm ci cannot skip it"
);
assert.equal(pkg.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
assert.equal(pkg.optionalDependencies?.["@huggingface/transformers"], undefined);
});
test("package-lock marks transformers and its onnxruntime runtime as optional", () => {
test("transformers + onnxruntime-node are regular dependencies (not optional)", () => {
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}>("package.json");
assert.equal(
pkg.dependencies?.["onnxruntime-node"],
"~1.24.3",
"onnxruntime-node is a regular dep (napi prebuilds, installable on Node 24/26)"
);
assert.equal(pkg.optionalDependencies?.["onnxruntime-node"], undefined);
const lock = readJson<{
packages: Record<string, { optional?: boolean; dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }>;
}>("package-lock.json");
assert.equal(
lock.packages[""]?.dependencies?.["@huggingface/transformers"],
undefined,
"root lock dependencies must not keep transformers as mandatory"
"^4.2.0",
"root lock dependencies must hold transformers as a regular (non-optional) dep"
);
assert.equal(lock.packages[""]?.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
for (const packagePath of [
"node_modules/@huggingface/transformers",
"node_modules/onnxruntime-node",
"node_modules/onnxruntime-common",
]) {
assert.equal(lock.packages[packagePath]?.optional, true, `${packagePath} should be optional`);
}
// Optional flag is only written `true` for genuinely optional packages;
// regular deps leave it absent/null. Assert each is NOT optional.
assert.ok(!lock.packages["node_modules/@huggingface/transformers"]?.optional, "transformers must not be marked optional in the lockfile");
assert.ok(!lock.packages["node_modules/onnxruntime-node"]?.optional, "onnxruntime-node must not be marked optional in the lockfile");
assert.ok(!lock.packages["node_modules/onnxruntime-common"]?.optional, "onnxruntime-common must not be marked optional in the lockfile");
});