fix(docker): complete partially traced packages in standalone co-location (#9615)

* 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>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-08 09:41:46 -03:00
committed by GitHub
parent 9e5fca685c
commit 63cf354129
4 changed files with 168 additions and 12 deletions

View File

@@ -0,0 +1 @@
- **fix(docker):** standalone co-location now completes packages Next's file tracing materialized partially (package.json without its `main` payload) — unblocks the Docker Hub publish that failed on every v3.8.50 push with `Cannot find module '@atjsh/llmlingua-2/dist/index.js'` ([#9615](https://github.com/diegosouzapw/OmniRoute/pull/9615))

View File

@@ -47,7 +47,8 @@
*/
import { cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
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
@@ -96,6 +97,33 @@ export function computeDependencyClosure(nodeModulesDir, seeds = SEED_PACKAGES)
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`.
@@ -142,11 +170,12 @@ export function colocateLlmlinguaOptionals({
const closure = computeDependencyClosure(rootNm, seeds);
// Check the complete closure rather than only the entry package. A partially
// populated bundle must still receive any missing transitive dependencies.
// 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) => existsSync(join(targetNm, name)))
closure.every((name) => isPackageIntact(targetNm, name))
) {
return { skipped: true, reason: "already co-located" };
}
@@ -155,11 +184,18 @@ export function colocateLlmlinguaOptionals({
for (const name of closure) {
const dest = join(targetNm, name);
if (existsSync(dest)) continue;
if (isPackageIntact(targetNm, name)) continue;
try {
mkdirSync(dirname(dest), { recursive: true });
cpSync(join(rootNm, name), 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(

View File

@@ -33,6 +33,11 @@ function mkPkg(
* @tensorflow/tfjs → dep @tensorflow/tfjs-core → dep long
* js-tiktoken → dep base64-js
* @huggingface/transformers present at root as a (stale) 4.2.0
*
* Each mock package gets a resolvable entrypoint so that isPackageIntact (which
* checks entrypoint integrity via require.resolve) can validate the co-located
* copy. The `main` field and corresponding index.js mirror what real npm
* packages ship.
*/
function buildRoot(rootDir: string): void {
const rootNm = join(rootDir, "node_modules");
@@ -40,6 +45,7 @@ function buildRoot(rootDir: string): void {
rootNm,
"@atjsh/llmlingua-2",
{
main: "dist/index.js",
dependencies: { "es-toolkit": "^1.38.0" },
peerDependencies: {
"@huggingface/transformers": "*",
@@ -49,12 +55,27 @@ function buildRoot(rootDir: string): void {
},
{ "dist/index.js": "export const llmlingua = true;\n" }
);
mkPkg(rootNm, "es-toolkit", {});
mkPkg(rootNm, "@tensorflow/tfjs", { dependencies: { "@tensorflow/tfjs-core": "4.22.0" } });
mkPkg(rootNm, "@tensorflow/tfjs-core", { dependencies: { long: "^5.0.0" } });
mkPkg(rootNm, "long", {});
mkPkg(rootNm, "js-tiktoken", { dependencies: { "base64-js": "^1.5.1" } });
mkPkg(rootNm, "base64-js", {});
mkPkg(rootNm, "es-toolkit", { main: "index.js" }, { "index.js": "export const esToolkit = true;\n" });
mkPkg(
rootNm,
"@tensorflow/tfjs",
{ main: "index.js", dependencies: { "@tensorflow/tfjs-core": "4.22.0" } },
{ "index.js": "export const tfjs = true;\n" }
);
mkPkg(
rootNm,
"@tensorflow/tfjs-core",
{ main: "index.js", dependencies: { long: "^5.0.0" } },
{ "index.js": "export const tfjsCore = true;\n" }
);
mkPkg(rootNm, "long", { main: "index.js" }, { "index.js": "export const long = true;\n" });
mkPkg(
rootNm,
"js-tiktoken",
{ main: "index.js", dependencies: { "base64-js": "^1.5.1" } },
{ "index.js": "export const tiktoken = true;\n" }
);
mkPkg(rootNm, "base64-js", { main: "index.js" }, { "index.js": "export const base64 = true;\n" });
// Root transformers is the STALE 4.x line — the bug we must not propagate into dist.
mkPkg(rootNm, "@huggingface/transformers", { version: "4.2.0" });
}

View File

@@ -55,6 +55,7 @@ function buildLlmlinguaRoot(
rootNm,
"@atjsh/llmlingua-2",
{
main: "dist/index.js",
dependencies: {
"es-toolkit": "^1.38.0",
},
@@ -227,6 +228,103 @@ test("#9166 standalone assembly never overwrites an already pinned transformers
}
});
test("#9166 co-location completes a partially traced package (package.json without its main)", () => {
const root = mkdtempSync(
join(tmpdir(), "omniroute-docker-llmlingua-partial-9166-")
);
try {
buildLlmlinguaRoot(root);
const { distDir, standaloneDir } = createStandalone(root);
// Next's file tracing materializes @atjsh/llmlingua-2 PARTIALLY in the
// standalone: the package.json lands (its "main" points at dist/index.js)
// but the dist/ payload does not — the exact state the Docker guard hits
// ("Cannot find module .../dist/index.js"). A directory-level no-clobber
// sees the dir and skips the package forever.
mkPkg(join(standaloneDir, "node_modules"), "@atjsh/llmlingua-2", {
main: "dist/index.js",
});
assembleStandalone({
distDir,
outDir: standaloneDir,
projectRoot: root,
copyNatives: true,
});
assert.ok(
existsSync(
join(
standaloneDir,
"node_modules",
"@atjsh",
"llmlingua-2",
"dist",
"index.js"
)
),
"a partially traced package must be completed, not skipped as already present"
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("#9166 co-location is not skipped when every closure dir exists but one is partial", () => {
const root = mkdtempSync(
join(tmpdir(), "omniroute-docker-llmlingua-partial-all-9166-")
);
try {
buildLlmlinguaRoot(root);
const { distDir, standaloneDir } = createStandalone(root);
const standaloneNm = join(standaloneDir, "node_modules");
// Every closure package already has a directory in the standalone (so a
// directory-level "already co-located" early-exit would fire), but the
// llmlingua-2 one is the partial NFT-trace shell without its main.
for (const packageName of [
"es-toolkit",
"@tensorflow/tfjs",
"@tensorflow/tfjs-core",
"long",
"js-tiktoken",
"base64-js",
"@huggingface/transformers",
"onnxruntime-node",
]) {
mkPkg(standaloneNm, packageName, { main: "index.js" }, {
"index.js": "export {};\n",
});
}
mkPkg(standaloneNm, "@atjsh/llmlingua-2", { main: "dist/index.js" });
assembleStandalone({
distDir,
outDir: standaloneDir,
projectRoot: root,
copyNatives: true,
});
assert.ok(
existsSync(
join(
standaloneDir,
"node_modules",
"@atjsh",
"llmlingua-2",
"dist",
"index.js"
)
),
"the closure-wide early-exit must not fire while any member is partial"
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("#9166 Docker explicitly installs and validates LLMLingua optionals", () => {
const dockerfile = readFileSync(
new URL("../../Dockerfile", import.meta.url),