mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
* fix(docker): complete partially traced packages in standalone co-location Publish-to-Docker-Hub has failed on every release/v3.8.50 push since #9151 enabled publishing from active release branches: the post-build guard dies with "Cannot find module .../@atjsh/llmlingua-2/dist/index.js" while the co-location step right above it reports 100 packages copied. Root cause: Next's file tracing materializes @atjsh/llmlingua-2 PARTIALLY in the standalone (package.json lands, the dist/ payload its main points at does not). colocateOptionals' no-clobber checked existsSync on the package DIRECTORY, so the partial shell counted as present and the one package that mattered was skipped forever (#9185 added the closure walk but kept the directory-level check). Fix: presence is now judged by entrypoint integrity — the package resolves from inside the target tree (same contract as the Dockerfile guard). Partial directories are completed with a file-level no-clobber merge (cpSync force:false), so files the trace did materialize are never overwritten and pinned instances (dist transformers 3.5.2) keep their protection. Validation (TDD): 2 new tests in docker-llmlingua-optionals-9166.test.ts reproduce the CI failure (partial package skipped; closure-wide early-exit firing while a member is partial) — red on the old code, 5/5 green after. * fix: update colocate test mock packages to match isPackageIntact entrypoint resolution The PR's isPackageIntact check uses require.resolve to validate that co-located packages have a usable entrypoint inside the target tree. The pre-existing test's mock packages lacked main fields and index files, so require.resolve failed and the idempotency assertion broke. Update buildRoot() to give every closure package a resolvable entry (main + index.js), mirroring what real npm packages ship. Refs #9615 * docs(changelog): fragment for #9615 * fix(yuanbao-web): accept content field in SSE text events (upstream format change) (#8739) Closes #8739 * fix(errorClassifier): classify ChatGPT Web SENTINEL_BLOCKED 403 as FORBIDDEN, enabling combo fallback (#8813) Closes #8813 * fix(vertex): route Claude models to native rawPredict and respect targetFormat overrides (#8994) Closes #8994 * fix(cursor): preserve tool context across multi-turn conversations when client lacks conversation_id (#9029) Closes #9029 * fix(sse): move Antigravity client system content to first user message to avoid upstream 429 (#9030) Closes #9030 * fix(combo): distinguish pre-dispatch skips from genuine failures to prevent false 503 ALL_ACCOUNTS_INACTIVE (#9630) Closes #9630 * fix: repair stray brace in combo.ts and fix no-explicit-any types in repro-9630 test --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
215 lines
8.4 KiB
JavaScript
215 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* OmniRoute — Co-locate the LLMLingua-2 optional dependency closure into the standalone bundle.
|
|
*
|
|
* The compression "ultra" SLM tier (PR #4257) runs `@atjsh/llmlingua-2` +
|
|
* `@huggingface/transformers` + `@tensorflow/tfjs` + `js-tiktoken` inside a worker thread
|
|
* (`open-sse/services/compression/engines/llmlingua/onnxWorker.js`, shipped under `dist/`). These
|
|
* are `optionalDependencies`: npm installs them into the ROOT `node_modules` on
|
|
* `--include=optional`, but the Next.js standalone trace bundles ONLY `@huggingface/transformers`
|
|
* (3.5.2, pinned) into `dist/node_modules` — it does NOT trace the optional, dynamically-imported
|
|
* SLM packages.
|
|
*
|
|
* ## Why this matters (the instance-split bug)
|
|
*
|
|
* The worker lives under `dist/`, so its `import("@huggingface/transformers")` resolves
|
|
* `dist/node_modules/@huggingface/transformers` (3.5.2) and the worker sets the model `cacheDir`
|
|
* on THAT instance's `env`. But its `import("@atjsh/llmlingua-2")` walks past `dist/node_modules`
|
|
* (no `@atjsh` there) up to the ROOT `node_modules`, and llmlingua-2's own
|
|
* `import("@huggingface/transformers")` then resolves the ROOT transformers — a DIFFERENT instance.
|
|
* The `cacheDir`/`localModelPath` config the worker set never reaches the instance llmlingua-2
|
|
* actually uses, so the local model under `DATA_DIR/models/llmlingua` is never found and the SLM
|
|
* tier silently fails-open (no compression). Worse, if the root transformers is a 4.x line,
|
|
* llmlingua-2 throws on a tokenizer-API change (`decoder.decode` is undefined).
|
|
*
|
|
* ## The fix
|
|
*
|
|
* Co-locate the SLM optional dependency CLOSURE from the root `node_modules` into
|
|
* `dist/node_modules` (NO-CLOBBER, so the pinned `dist` transformers 3.5.2 / onnxruntime / sharp
|
|
* stay). Then the worker resolves `@atjsh/llmlingua-2` AND `@huggingface/transformers` from the
|
|
* SAME `dist/node_modules` — a single 3.5.2 instance — so the env config applies and the local
|
|
* model loads.
|
|
*
|
|
* `@huggingface/transformers` is intentionally NOT a closure seed: it is a PEER of
|
|
* `@atjsh/llmlingua-2` (not a regular dependency) and is already bundled in `dist/node_modules`,
|
|
* so the closure walk never reaches it and the no-clobber guard would skip it anyway.
|
|
*
|
|
* ## Validation (Hard Rule #18)
|
|
*
|
|
* Manual co-location of this exact closure on the production VPS produced real 54.8% compression
|
|
* (11520 → 5203 chars) via real ONNX inference — both the default and the `modelPath` (PR #4257)
|
|
* code paths. See the unit test for the closure-walk + no-clobber contract.
|
|
*
|
|
* Idempotent + fail-soft: skips when the optionals are absent (the common case — they are OPTIONAL)
|
|
* or already co-located; a per-package copy failure only disables the SLM tier, which is itself
|
|
* fail-open, so this never throws into the install.
|
|
*/
|
|
|
|
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, sep } from "node:path";
|
|
|
|
/**
|
|
* Entry packages of the SLM optional stack (the closure roots). `@huggingface/transformers` is
|
|
* deliberately absent — it is the pinned instance already present in `dist/node_modules`.
|
|
*/
|
|
export const SEED_PACKAGES = ["@atjsh/llmlingua-2", "@tensorflow/tfjs", "js-tiktoken"];
|
|
|
|
/**
|
|
* Compute the transitive dependency closure of `seeds` by walking each package's `dependencies` +
|
|
* `optionalDependencies` from a `node_modules` directory. Packages that are not present in that
|
|
* tree (e.g. peers provided elsewhere, like `@huggingface/transformers` in `dist`) are skipped —
|
|
* the closure only contains packages that actually exist in `nodeModulesDir`.
|
|
*
|
|
* @param {string} nodeModulesDir absolute path to the source `node_modules`
|
|
* @param {string[]} [seeds] closure roots (defaults to {@link SEED_PACKAGES})
|
|
* @returns {string[]} package names in discovery order, seeds first
|
|
*/
|
|
export function computeDependencyClosure(nodeModulesDir, seeds = SEED_PACKAGES) {
|
|
const closure = [];
|
|
const seen = new Set();
|
|
const stack = [...seeds];
|
|
|
|
while (stack.length) {
|
|
const name = stack.shift();
|
|
if (seen.has(name)) continue;
|
|
seen.add(name);
|
|
|
|
const pkgDir = join(nodeModulesDir, name);
|
|
if (!existsSync(pkgDir)) continue; // absent in this tree (peer provided elsewhere) — skip
|
|
|
|
closure.push(name);
|
|
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8"));
|
|
} catch {
|
|
continue; // unreadable/absent manifest — copy the dir but do not recurse
|
|
}
|
|
|
|
const deps = { ...manifest.dependencies, ...manifest.optionalDependencies };
|
|
for (const dep of Object.keys(deps)) {
|
|
if (!seen.has(dep)) stack.push(dep);
|
|
}
|
|
}
|
|
|
|
return closure;
|
|
}
|
|
|
|
/**
|
|
* A package in the target tree counts as PRESENT only when its entrypoint
|
|
* resolves from inside that tree — the same contract the Dockerfile's
|
|
* post-build guard enforces. Next's file tracing can materialize a package
|
|
* PARTIALLY (the package.json lands, the files its `main` points at do not),
|
|
* and a directory-level `existsSync` check then skips the package forever
|
|
* while the runtime dies with "Cannot find module <pkg>/dist/index.js".
|
|
*
|
|
* @param {string} targetNodeModulesDir
|
|
* @param {string} name
|
|
* @returns {boolean}
|
|
*/
|
|
function isPackageIntact(targetNodeModulesDir, name) {
|
|
if (!existsSync(join(targetNodeModulesDir, name))) return false;
|
|
try {
|
|
const probe = createRequire(
|
|
join(targetNodeModulesDir, "__colocate_probe__.js")
|
|
);
|
|
const resolved = probe.resolve(name);
|
|
// A resolution that walked past the target into an ancestor tree does not
|
|
// prove the target copy is usable.
|
|
return resolved.startsWith(targetNodeModulesDir + sep);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Co-locate the SLM optional dependency closure from `<rootDir>/node_modules`
|
|
* into a standalone bundle's `node_modules`.
|
|
*
|
|
* The default destination remains `<rootDir>/dist/node_modules` for the npm
|
|
* postinstall path. Standalone builders, including Docker, may provide
|
|
* `targetNodeModulesDir`.
|
|
*
|
|
* Packages already present in the destination are never overwritten. This
|
|
* preserves the standalone bundle's pinned dependency instances while filling
|
|
* dynamically imported packages that Next.js did not trace.
|
|
*
|
|
* @param {{
|
|
* rootDir: string,
|
|
* targetNodeModulesDir?: string,
|
|
* seeds?: string[],
|
|
* log?: (message: string) => void
|
|
* }} opts
|
|
* @returns {{ skipped: true, reason: string }
|
|
* | { skipped: false, copied: number, closure: number }}
|
|
*/
|
|
export function colocateLlmlinguaOptionals({
|
|
rootDir,
|
|
targetNodeModulesDir,
|
|
seeds = SEED_PACKAGES,
|
|
log = () => {},
|
|
}) {
|
|
const rootNm = join(rootDir, "node_modules");
|
|
const targetNm = targetNodeModulesDir ?? join(rootDir, "dist", "node_modules");
|
|
|
|
if (!existsSync(targetNm)) {
|
|
return {
|
|
skipped: true,
|
|
reason: targetNodeModulesDir
|
|
? "no target node_modules"
|
|
: "no standalone dist/node_modules",
|
|
};
|
|
}
|
|
|
|
// Only run when every requested closure root was installed.
|
|
if (!seeds.every((seed) => existsSync(join(rootNm, seed)))) {
|
|
return { skipped: true, reason: "SLM optionals not installed at root" };
|
|
}
|
|
|
|
const closure = computeDependencyClosure(rootNm, seeds);
|
|
|
|
// Check the complete closure rather than only the entry package, and judge
|
|
// presence by entrypoint integrity — a partially traced directory (see
|
|
// isPackageIntact) must still receive its missing files.
|
|
if (
|
|
closure.length > 0 &&
|
|
closure.every((name) => isPackageIntact(targetNm, name))
|
|
) {
|
|
return { skipped: true, reason: "already co-located" };
|
|
}
|
|
|
|
let copied = 0;
|
|
|
|
for (const name of closure) {
|
|
const dest = join(targetNm, name);
|
|
if (isPackageIntact(targetNm, name)) continue;
|
|
|
|
try {
|
|
mkdirSync(dirname(dest), { recursive: true });
|
|
// force:false merges into a partially traced directory: files the trace
|
|
// already materialized are kept, missing ones (the package payload) are
|
|
// filled in from the root tree.
|
|
cpSync(join(rootNm, name), dest, {
|
|
recursive: true,
|
|
force: false,
|
|
errorOnExist: false,
|
|
});
|
|
copied++;
|
|
} catch (err) {
|
|
log(
|
|
` ⚠️ LLMLingua optional co-location failed for ${name}: ${err.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (copied > 0) {
|
|
log(
|
|
` ✅ Co-located ${copied} LLMLingua SLM optional package(s) into standalone node_modules.\n`
|
|
);
|
|
}
|
|
|
|
return { skipped: false, copied, closure: closure.length };
|
|
}
|