mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
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:
committed by
GitHub
parent
9e5fca685c
commit
63cf354129
1
changelog.d/fixes/9615-docker-colocate-partial-trace.md
Normal file
1
changelog.d/fixes/9615-docker-colocate-partial-trace.md
Normal 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))
|
||||
@@ -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(
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user