mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-06 23:12:09 +03:00
Rebased onto the current release/v3.8.51 tip as part of a combined provider-retirement/provenance merge batch (Designer Web, Felo Web, Runtime, GPL-derived removal, Qwen Web already landed). Large conflict set (this is the biggest PR in the batch — the common ChatGPT Web provider touches chat, images, count-tokens, session leases, and combos). Conflicts resolved: - `open-sse/config/providers/registry/chatgpt-web/*`, `open-sse/executors/chatgpt-web*`, `open-sse/handlers/imageGeneration/providers/chatgptWeb.ts`, and their tests: kept deleted, matching the PR's stated scope. - `open-sse/config/providers/registry/minimax/web/index.ts`, `open-sse/handlers/imageGeneration/providers/geminiWeb.ts`, `open-sse/executors/gemini-web.ts`'s stale image-mode branch: base-drift collisions against already-merged sibling retirements (#11691, #11708) — kept deleted / dropped the dead code, since this PR's own branch forked before those merged. - `src/shared/constants/reservedProviderPrefixes.ts`, `open-sse/executors/index.ts`, `executorProxy.ts`, `virtualFactory.ts`, `autoStrategy.ts`, `src/lib/db/providers.ts`, `src/sse/handlers/chat.ts`: combined the Designer + Runtime (Felo/Qwen) + common-ChatGPT-Web retirement guard calls at each shared chokepoint — compute-once-then-OR pattern, consistent with prior combinations in this batch. - `src/sse/services/model.ts` / `src/sse/handlers/chatHelpers.ts`: adopted this PR's new `getModelInfoOrRetirementResponse()` central wrapper (a real improvement over ad-hoc try/catch), and extended it to also catch the Designer + Runtime retirement errors it didn't originally cover, so the consolidation doesn't regress the other two mechanisms. - `src/app/api/v1/images/edits/route.ts`: this PR moved the retirement check earlier (before `enforceApiKeyPolicy`) but left the old later call+catch block in place from base drift — removed the now-redundant duplicate `resolveImageRouteModel()` call and merged the Designer catch into the earlier one. - `open-sse/config/imageRegistry.ts`, `tests/snapshots/executors/executor-map.json` (`keyCount` recomputed to 133), `tests/snapshots/provider/translate-path.json`: same "both sides inserted a different retired provider at the same slot" pattern — resolved by dropping both. - `tests/unit/chatcore-executor-proxy.test.ts`, `provider-node-reserved-prefix.test.ts`, `combo-auto-candidate-expansion.test.ts`, `messages-count-tokens-route.test.ts`, `virtual-auto-combo.test.ts`: split into independent per-mechanism test blocks (established pattern); `virtual-auto-combo.test.ts`'s old "includes cookie web-session providers" positive-inclusion test (which used chatgpt-web as its example) was retired along with the provider and replaced by this PR's negative-exclusion test for the same slot. - `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` (+ 4 i18n mirrors), `README.md`, `FREE-TIERS-GUIDE.md`, `docs/diagrams/free-tier-budget.svg`, `docs/screenshots/free-tier-budget-card.svg`, `docs/reference/PROVIDER_REFERENCE.md`: recomputed every stale count from the real merged state — 104 executors (`countFiles` gate logic), 351 providers (regenerated via `gen:provider-reference`), 152/351 `hasFree` entries, 445/438/7 free-tier catalog rows, 13 ToS-avoid providers, budget-card regenerated via its real generator script. One doc conflict (`oauth/` module list) needed picking HEAD's side specifically — theirs still listed the already-removed `raycast` module instead of the real `openference`. - `config/quality/test-masking-allowlist.json`: additive merge of the PR's 17 `_deletedWithReplacement` entries alongside the batch's existing ones (one real duplicate-key mistake in my first pass, caught and fixed via a `object_pairs_hook` duplicate-key check before finalizing). Also fixed two real, unrelated-to-my-merge issues surfaced by the focused suite: - `tests/unit/resolve-web-provider-host.test.ts`: the PR's own test had a typo — it asserted `perplexity-web`'s resolved host as `"perplexity.ai"`, but the provider's registered `website` is `"https://www.perplexity.ai"` and the resolver returns the URL's `host` verbatim (no www-stripping), so the correct value is `"www.perplexity.ai"` (consistent with the same test's own `url` assertion). - `tests/unit/hard-session-lease-bypass-inventory.test.ts`: this golden call-site inventory was already stale on the pristine post-#11713 tip (confirmed via a throwaway probe worktree) — `src/lib/db/providers.ts`'s 3 connection-fallback sites and a third `src/app/api/providers/route.ts` site were never added to the golden list by the earlier-merged #11698/#11720 PRs. Updated it to the real current inventory (dated inline comments explain each delta and which PR introduced it), plus this PR's own legitimate deltas (image-edits duplicate-call removal, `ChatGptWebExecutor.execute()` site removed). Focused suite green (433/433 across executor-proxy, reserved-prefix, hard-session-lease-bypass-inventory, resolve-web-provider-host, retirement/runtime-block/source-retirement/management-retirement/image-handler-retirement, migration-168, combo-auto-candidate-expansion, virtual-auto-combo, executor-map-golden and siblings), plus `typecheck:core`, `check-file-size`, and `check-changelog-integrity` clean. Thanks for the thorough provenance-hold retirement work — appreciated.
229 lines
8.5 KiB
JavaScript
229 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* OmniRoute — Optional runtime pack manifest + integrity core.
|
|
*
|
|
* Stage 7 of the Electron efficiency roadmap (issue #10321): the heavy optional
|
|
* ML / browser automation dependency closure is excluded from the packaged
|
|
* desktop app and shipped as versioned, checksummed packs that install on first
|
|
* use into `DATA_DIR/packs/<name>/node_modules`.
|
|
*
|
|
* This module owns the *contract* shared by three consumers:
|
|
* - `scripts/build/optionalPackStaging.mjs` (build): checksums the staged
|
|
* closure, emits `optional-packs.index.json`, removes pack members from the
|
|
* Electron staging tree, optionally tars the packs for release assets.
|
|
* - `scripts/packs/optionalPackInstaller.mjs` (first use): installs/verifies/
|
|
* removes packs in DATA_DIR against the shipped index.
|
|
* - `bin/cli/commands/packs.mjs` (UX): `omniroute packs …`.
|
|
*
|
|
* The runtime *resolution* side (making an installed pack light up the SLM /
|
|
* embeddings / browser features) lives in `open-sse/utils/optionalPacks.ts` and
|
|
* intentionally does NOT import this file — it embeds only the pack names.
|
|
*
|
|
* Fail-open philosophy: every consumer of a pack degrades gracefully when the
|
|
* pack is absent; nothing here may throw into a code path that works today.
|
|
*/
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { createReadStream } from "node:fs";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* The optional runtime packs. Membership changes require bumping `packVersion`.
|
|
*
|
|
* `os`/`cpu` use Node `process.platform`/`process.arch` values and exist so the
|
|
* installer can refuse (with a clear error) a pack whose native payloads do not
|
|
* match the machine — e.g. a future pack that only ships darwin/win prebuilds.
|
|
*/
|
|
export const OPTIONAL_PACKS = [
|
|
{
|
|
name: "ml-runtime",
|
|
packVersion: 1,
|
|
description:
|
|
"Local ML inference closure: LLMLingua-2 SLM prompt compression and transformers.js memory embeddings",
|
|
packages: [
|
|
// NOTE: exact versions are resolved at packaging time from the staged
|
|
// tree and recorded in optional-packs.index.json — the manifest defines
|
|
// MEMBERSHIP only, so member bumps don't need a manifest edit unless the
|
|
// set of packages changes.
|
|
{ name: "@huggingface/transformers" },
|
|
{ name: "onnxruntime-node" },
|
|
{ name: "@atjsh/llmlingua-2" },
|
|
{ name: "js-tiktoken" },
|
|
],
|
|
},
|
|
{
|
|
name: "browser-runtime",
|
|
packVersion: 1,
|
|
description:
|
|
"Browser automation closure: Claude Turnstile solver and ChatGPT Web (Codex)/Gemini web executors",
|
|
packages: [{ name: "playwright" }, { name: "playwright-core" }],
|
|
},
|
|
];
|
|
|
|
/** Index file emitted at the standalone bundle root (same walk-up anchor style as llmlingua's GATE_DEP_REL). */
|
|
export const PACK_INDEX_FILENAME = "optional-packs.index.json";
|
|
|
|
/** Look up a pack definition by name. */
|
|
export function findPack(name) {
|
|
return OPTIONAL_PACKS.find((pack) => pack.name === name) ?? null;
|
|
}
|
|
|
|
/** Flatten every package name across all packs (sorted, deduped). */
|
|
export function allPackPackageNames() {
|
|
return [...new Set(OPTIONAL_PACKS.flatMap((pack) => pack.packages.map((p) => p.name)))].sort();
|
|
}
|
|
|
|
/** Whether `platform`/`arch` satisfy a package's optional os/cpu filters. */
|
|
export function packageMatchesPlatform(pkg, platform = process.platform, arch = process.arch) {
|
|
if (Array.isArray(pkg.os) && !pkg.os.includes(platform)) return false;
|
|
if (Array.isArray(pkg.cpu) && !pkg.cpu.includes(arch)) return false;
|
|
return true;
|
|
}
|
|
|
|
/** Whether every package of `pack` matches the platform (compat gate for installs). */
|
|
export function packMatchesPlatform(pack, platform = process.platform, arch = process.arch) {
|
|
return pack.packages.every((pkg) => packageMatchesPlatform(pkg, platform, arch));
|
|
}
|
|
|
|
// ─── deterministic directory checksum ───────────────────────────────────────────
|
|
|
|
/**
|
|
* Recursively collect sorted relative POSIX paths of regular files under `dir`.
|
|
* Symlinks are included as their own entries (link target hashed) — npm trees can
|
|
* contain them and silently skipping them would weaken tamper detection.
|
|
*
|
|
* @param {string} dir
|
|
* @returns {{rel: string, absolute: string, symlink: boolean}[]}
|
|
*/
|
|
export function listDirFiles(dir) {
|
|
const out = [];
|
|
const walk = (current, prefix) => {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
} catch {
|
|
return;
|
|
}
|
|
// Sort for determinism across platforms/FS orderings.
|
|
const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
for (const entry of sorted) {
|
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
const absolute = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walk(absolute, rel);
|
|
} else {
|
|
out.push({ rel, absolute, symlink: entry.isSymbolicLink() });
|
|
}
|
|
}
|
|
};
|
|
walk(dir, "");
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Deterministic sha256 over a directory tree: sorted relative path + per-file
|
|
* content (or link target). Byte-stable across platforms (POSIX separators).
|
|
*
|
|
* @param {string} dir
|
|
* @returns {Promise<{sha256: string, files: number, bytes: number}>}
|
|
*/
|
|
export async function dirChecksum(dir) {
|
|
const hash = createHash("sha256");
|
|
let files = 0;
|
|
let bytes = 0;
|
|
for (const { rel, absolute, symlink } of listDirFiles(dir)) {
|
|
hash.update(rel);
|
|
hash.update("\0");
|
|
if (symlink) {
|
|
let target = "";
|
|
try {
|
|
target = fs.readlinkSync(absolute);
|
|
} catch {
|
|
/* unreadable link — hash as empty target */
|
|
}
|
|
hash.update(`link:${target}`);
|
|
} else {
|
|
let size = 0;
|
|
try {
|
|
size = fs.statSync(absolute).size;
|
|
} catch {
|
|
/* stat race — hash content stream anyway */
|
|
}
|
|
bytes += size;
|
|
hash.update(String(size));
|
|
hash.update("\0");
|
|
try {
|
|
// Stream to keep memory bounded on multi-hundred-MB packages (onnxruntime-node).
|
|
for await (const chunk of createReadStream(absolute)) hash.update(chunk);
|
|
} catch {
|
|
hash.update("<unreadable>");
|
|
}
|
|
}
|
|
hash.update("\0");
|
|
files++;
|
|
}
|
|
return { sha256: hash.digest("hex"), files, bytes };
|
|
}
|
|
|
|
// ─── index build / verify ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Build the pack index entry for one pack from a populated `node_modules` dir.
|
|
* Records resolved versions + deterministic checksums so installs and `verify`
|
|
* can prove integrity without network access.
|
|
*
|
|
* @param {{name: string, packVersion: number, description?: string, packages: {name: string}[]}} pack
|
|
* @param {string} nodeModulesDir tree containing the pack members
|
|
* @returns {Promise<{name: string, packVersion: number, description: string, tarball: string, packages: object[]}>}
|
|
*/
|
|
export async function buildPackIndexEntry(pack, nodeModulesDir) {
|
|
const packages = [];
|
|
for (const pkg of pack.packages) {
|
|
const pkgDir = path.join(nodeModulesDir, ...pkg.name.split("/"));
|
|
if (!fs.existsSync(path.join(pkgDir, "package.json"))) {
|
|
throw new Error(`pack member missing from staging tree: ${pkg.name}`);
|
|
}
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
const checksum = await dirChecksum(pkgDir);
|
|
packages.push({
|
|
name: pkg.name,
|
|
version: manifest.version ?? null,
|
|
sha256: checksum.sha256,
|
|
files: checksum.files,
|
|
bytes: checksum.bytes,
|
|
});
|
|
}
|
|
return {
|
|
name: pack.name,
|
|
packVersion: pack.packVersion,
|
|
description: pack.description,
|
|
tarball: `optional-pack-${pack.name}.tar.gz`,
|
|
packages,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify a directory tree against an index entry (every member checksum).
|
|
*
|
|
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
*/
|
|
export async function verifyAgainstIndexEntry(entry, nodeModulesDir) {
|
|
const errors = [];
|
|
for (const pkg of entry.packages) {
|
|
const pkgDir = path.join(nodeModulesDir, ...pkg.name.split("/"));
|
|
if (!fs.existsSync(pkgDir)) {
|
|
errors.push(`${pkg.name}: missing`);
|
|
continue;
|
|
}
|
|
const checksum = await dirChecksum(pkgDir);
|
|
if (checksum.sha256 !== pkg.sha256) {
|
|
errors.push(
|
|
`${pkg.name}: checksum mismatch (expected ${pkg.sha256.slice(0, 12)}, got ${checksum.sha256.slice(0, 12)})`
|
|
);
|
|
}
|
|
}
|
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
}
|