mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
feat(pxpipe): PXPIPE token saver — multimodal prompt compression (#2465)
Add pxpipe as an experimental fifth Token Saver: Claude-format request bodies above a configurable size threshold are rendered as dense PNGs via the pxpipe-proxy library API (transformAnthropicMessages) before dispatch, cutting estimated input tokens by ~35-60% on token-dense contexts. Integration follows the Headroom pattern: applied to the final body in chatCore just before dispatch, fail-open on any error/timeout. Managed npm install into DATA_DIR/pxpipe, dynamic loader with per-version cache-bust, JSONL event log with rotation, /api/pxpipe/* endpoints, Token Saver card (marked experimental) + /dashboard/pxpipe page, and per-request Activated/Skipped annotation in Request Details. Disabled by default.
This commit is contained in:
committed by
decolua
parent
e1f3399b73
commit
dcf1927f22
@@ -98,6 +98,7 @@ async function flushToDatabase() {
|
||||
providerRequest: truncateField(item.providerRequest, config.maxJsonSize),
|
||||
providerResponse: truncateField(item.providerResponse, config.maxJsonSize),
|
||||
response: truncateField(item.response, config.maxJsonSize),
|
||||
pxpipe: item.pxpipe || undefined,
|
||||
};
|
||||
|
||||
db.run(
|
||||
|
||||
@@ -42,6 +42,10 @@ const DEFAULT_SETTINGS = {
|
||||
cavemanLevel: "full",
|
||||
ponytailEnabled: false,
|
||||
ponytailLevel: "full",
|
||||
pxpipeEnabled: false,
|
||||
pxpipeAutoInstall: true,
|
||||
pxpipeMinChars: 25000,
|
||||
pxpipeTimeoutMs: 15000,
|
||||
};
|
||||
|
||||
async function readRaw() {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { PXPIPE_DIR } from "./install.js";
|
||||
|
||||
const EVENTS_FILE = path.join(PXPIPE_DIR, "events.jsonl");
|
||||
const ROTATED_FILE = path.join(PXPIPE_DIR, "events.jsonl.1");
|
||||
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(PXPIPE_DIR)) fs.mkdirSync(PXPIPE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Fire-and-forget: stats must never break the request path.
|
||||
export function appendPxpipeEvent(event) {
|
||||
try {
|
||||
ensureDir();
|
||||
try {
|
||||
const stat = fs.statSync(EVENTS_FILE);
|
||||
if (stat.size > MAX_FILE_BYTES) fs.renameSync(EVENTS_FILE, ROTATED_FILE);
|
||||
} catch { /* no file yet */ }
|
||||
fs.appendFile(EVENTS_FILE, JSON.stringify({ ts: Date.now(), ...event }) + "\n", () => {});
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export function readPxpipeEvents({ sinceMs = null, limit = null } = {}) {
|
||||
const events = [];
|
||||
for (const file of [ROTATED_FILE, EVENTS_FILE]) {
|
||||
try {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
||||
if (!line) continue;
|
||||
try {
|
||||
const ev = JSON.parse(line);
|
||||
if (sinceMs && ev.ts < sinceMs) continue;
|
||||
events.push(ev);
|
||||
} catch { /* skip corrupt line */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
events.sort((a, b) => a.ts - b.ts);
|
||||
return limit ? events.slice(-limit) : events;
|
||||
}
|
||||
|
||||
function emptyTotals() {
|
||||
return {
|
||||
requests: 0, compressed: 0, bypassed: 0, errors: 0,
|
||||
tokensBeforeEst: 0, tokensAfterEst: 0, tokensSavedEst: 0, savedPct: 0,
|
||||
imagesGenerated: 0, compressionTimeMs: 0, avgCompressionMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function accumulate(totals, ev) {
|
||||
totals.requests++;
|
||||
if (ev.applied) {
|
||||
totals.compressed++;
|
||||
totals.tokensBeforeEst += ev.tokensBeforeEst || 0;
|
||||
totals.tokensAfterEst += ev.tokensAfterEst || 0;
|
||||
totals.tokensSavedEst += ev.tokensSavedEst || 0;
|
||||
totals.imagesGenerated += ev.imageCount || 0;
|
||||
totals.compressionTimeMs += ev.durationMs || 0;
|
||||
} else if (ev.reason === "transform_error" || ev.reason === "timeout") {
|
||||
totals.errors++;
|
||||
} else {
|
||||
totals.bypassed++;
|
||||
}
|
||||
}
|
||||
|
||||
function finalize(totals) {
|
||||
totals.savedPct = totals.tokensBeforeEst > 0
|
||||
? +((totals.tokensSavedEst / totals.tokensBeforeEst) * 100).toFixed(2)
|
||||
: 0;
|
||||
totals.avgCompressionMs = totals.compressed > 0
|
||||
? Math.round(totals.compressionTimeMs / totals.compressed)
|
||||
: 0;
|
||||
return totals;
|
||||
}
|
||||
|
||||
// Aggregated stats for the dashboard: all-time + windowed totals, a daily
|
||||
// tokens-saved timeline (last `timelineDays`), and the most recent events.
|
||||
export function getPxpipeStats({ timelineDays = 30, recentLimit = 100 } = {}) {
|
||||
const events = readPxpipeEvents();
|
||||
const now = Date.now();
|
||||
const startOfToday = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
|
||||
|
||||
const windows = {
|
||||
all: emptyTotals(),
|
||||
today: emptyTotals(),
|
||||
yesterday: emptyTotals(),
|
||||
last7d: emptyTotals(),
|
||||
last30d: emptyTotals(),
|
||||
};
|
||||
|
||||
const timeline = new Map();
|
||||
for (let i = timelineDays - 1; i >= 0; i--) {
|
||||
const day = new Date(startOfToday - i * DAY_MS);
|
||||
timeline.set(day.toISOString().slice(0, 10), { date: day.toISOString().slice(0, 10), tokensSavedEst: 0, compressed: 0, requests: 0 });
|
||||
}
|
||||
|
||||
for (const ev of events) {
|
||||
accumulate(windows.all, ev);
|
||||
if (ev.ts >= startOfToday) accumulate(windows.today, ev);
|
||||
else if (ev.ts >= startOfToday - DAY_MS) accumulate(windows.yesterday, ev);
|
||||
if (ev.ts >= now - 7 * DAY_MS) accumulate(windows.last7d, ev);
|
||||
if (ev.ts >= now - 30 * DAY_MS) accumulate(windows.last30d, ev);
|
||||
|
||||
const key = new Date(ev.ts).toISOString().slice(0, 10);
|
||||
const bucket = timeline.get(key);
|
||||
if (bucket) {
|
||||
bucket.requests++;
|
||||
if (ev.applied) {
|
||||
bucket.compressed++;
|
||||
bucket.tokensSavedEst += ev.tokensSavedEst || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const w of Object.values(windows)) finalize(w);
|
||||
|
||||
return {
|
||||
windows,
|
||||
timeline: [...timeline.values()],
|
||||
recent: events.slice(-recentLimit).reverse(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn, execSync } from "child_process";
|
||||
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||
|
||||
export const PXPIPE_DIR = path.join(DATA_DIR, "pxpipe");
|
||||
export const PXPIPE_PACKAGE = "pxpipe-proxy";
|
||||
const INSTALL_LOG = path.join(PXPIPE_DIR, "install.log");
|
||||
const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
const IS_WIN = process.platform === "win32";
|
||||
const NPM_CMD = IS_WIN ? "npm.cmd" : "npm";
|
||||
|
||||
// Same PATH extension trick as headroom/detect.js: packaged/launchd environments
|
||||
// often miss the Node bin dirs.
|
||||
const EXTRA_BINS = IS_WIN
|
||||
? [`${process.env.ProgramFiles || ""}\\nodejs`, `${process.env.APPDATA || ""}\\npm`]
|
||||
: ["/usr/local/bin", "/opt/homebrew/bin", `${process.env.HOME || ""}/.local/bin`, "/usr/bin", "/bin"];
|
||||
const EXTENDED_PATH = [...EXTRA_BINS, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
|
||||
|
||||
let installInFlight = null;
|
||||
|
||||
function ensureDir() {
|
||||
if (!fs.existsSync(PXPIPE_DIR)) fs.mkdirSync(PXPIPE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export function packageRoot() {
|
||||
return path.join(PXPIPE_DIR, "node_modules", PXPIPE_PACKAGE);
|
||||
}
|
||||
|
||||
export function libraryEntry() {
|
||||
return path.join(packageRoot(), "dist", "core", "library.js");
|
||||
}
|
||||
|
||||
export function findNpm() {
|
||||
try {
|
||||
const out = execSync(`${IS_WIN ? "where" : "which"} npm`, {
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PATH: EXTENDED_PATH },
|
||||
}).toString().trim();
|
||||
return out ? out.split(/\r?\n/)[0].trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// { installed, version, path } — installed means the library entry exists on disk.
|
||||
export function getInstallInfo() {
|
||||
try {
|
||||
const pkgJson = path.join(packageRoot(), "package.json");
|
||||
if (!fs.existsSync(pkgJson) || !fs.existsSync(libraryEntry())) {
|
||||
return { installed: false, version: null, path: null };
|
||||
}
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
|
||||
return { installed: true, version: pkg.version || null, path: packageRoot() };
|
||||
} catch {
|
||||
return { installed: false, version: null, path: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function isInstalling() {
|
||||
return installInFlight !== null;
|
||||
}
|
||||
|
||||
// Install (or repair by reinstalling) pxpipe-proxy into DATA_DIR/pxpipe.
|
||||
// Serialized: concurrent calls await the same run.
|
||||
export function installPxpipe() {
|
||||
if (installInFlight) return installInFlight;
|
||||
installInFlight = runInstall().finally(() => { installInFlight = null; });
|
||||
return installInFlight;
|
||||
}
|
||||
|
||||
async function runInstall() {
|
||||
const npm = findNpm();
|
||||
if (!npm) {
|
||||
const err = new Error("npm not found on PATH — Node.js/npm is required to install PXPIPE");
|
||||
err.code = "NPM_NOT_FOUND";
|
||||
throw err;
|
||||
}
|
||||
|
||||
ensureDir();
|
||||
const pkgJson = path.join(PXPIPE_DIR, "package.json");
|
||||
if (!fs.existsSync(pkgJson)) {
|
||||
fs.writeFileSync(pkgJson, JSON.stringify({ name: "9router-pxpipe-host", private: true }, null, 2));
|
||||
}
|
||||
|
||||
const outFd = fs.openSync(INSTALL_LOG, "a");
|
||||
fs.writeSync(outFd, `\n[${new Date().toISOString()}] npm install ${PXPIPE_PACKAGE}@latest\n`);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(npm, ["install", `${PXPIPE_PACKAGE}@latest`, "--no-audit", "--no-fund", "--omit=dev"], {
|
||||
cwd: PXPIPE_DIR,
|
||||
stdio: ["ignore", outFd, outFd],
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PATH: EXTENDED_PATH },
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error("npm install timed out after 5 minutes — see install.log"));
|
||||
}, INSTALL_TIMEOUT_MS);
|
||||
child.once("error", (e) => { clearTimeout(timer); reject(e); });
|
||||
child.once("exit", (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`npm install exited with code ${code} — see install.log`));
|
||||
});
|
||||
}).finally(() => fs.closeSync(outFd));
|
||||
|
||||
const info = getInstallInfo();
|
||||
if (!info.installed) throw new Error("install finished but package is missing — see install.log");
|
||||
return info;
|
||||
}
|
||||
|
||||
export function getInstallLogTail(maxLines = 200) {
|
||||
try {
|
||||
if (!fs.existsSync(INSTALL_LOG)) return "";
|
||||
const lines = fs.readFileSync(INSTALL_LOG, "utf8").split(/\r?\n/).filter(Boolean);
|
||||
return lines.slice(-maxLines).join("\n");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { pathToFileURL } from "url";
|
||||
import { getInstallInfo, libraryEntry } from "./install.js";
|
||||
|
||||
// Module cache: pxpipe is loaded once per process ("started") and dropped on
|
||||
// "stop". In library mode start/stop govern the in-process module, not a daemon.
|
||||
let cached = null; // { module, version, loadedAt }
|
||||
let loadPromise = null;
|
||||
|
||||
export function getLoadedInfo() {
|
||||
return cached ? { loaded: true, version: cached.version, loadedAt: cached.loadedAt } : { loaded: false };
|
||||
}
|
||||
|
||||
export async function loadPxpipe() {
|
||||
if (cached) return cached;
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = doLoad().finally(() => { loadPromise = null; });
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
async function doLoad() {
|
||||
const info = getInstallInfo();
|
||||
if (!info.installed) {
|
||||
const err = new Error("PXPIPE is not installed");
|
||||
err.code = "NOT_INSTALLED";
|
||||
throw err;
|
||||
}
|
||||
// Cache-bust per version so Repair/upgrade takes effect without a server restart.
|
||||
const url = `${pathToFileURL(libraryEntry()).href}?v=${encodeURIComponent(info.version || "0")}`;
|
||||
const mod = await import(/* webpackIgnore: true */ url);
|
||||
if (typeof mod.transformAnthropicMessages !== "function") {
|
||||
throw new Error("installed pxpipe package does not export transformAnthropicMessages");
|
||||
}
|
||||
cached = { module: mod, version: info.version, loadedAt: Date.now() };
|
||||
return cached;
|
||||
}
|
||||
|
||||
export function unloadPxpipe() {
|
||||
const wasLoaded = !!cached;
|
||||
cached = null;
|
||||
return wasLoaded;
|
||||
}
|
||||
|
||||
// Transform function for the request pipeline; null when unavailable (fail-open).
|
||||
// autoLoad controls whether a cold cache triggers a load (first request warms it).
|
||||
export async function getTransform({ autoLoad = true } = {}) {
|
||||
try {
|
||||
if (!cached && !autoLoad) return null;
|
||||
const { module: mod } = await loadPxpipe();
|
||||
return mod.transformAnthropicMessages;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Health self-test: run a tiny synthetic Claude request through the transformer.
|
||||
// A healthy module parses it and answers with a machine-readable reason.
|
||||
export async function selfTest() {
|
||||
const startedAt = Date.now();
|
||||
const { module: mod } = await loadPxpipe();
|
||||
const body = new TextEncoder().encode(JSON.stringify({
|
||||
model: "claude-fable-5",
|
||||
max_tokens: 16,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
}));
|
||||
const result = await mod.transformAnthropicMessages({ body, model: "claude-fable-5" });
|
||||
if (!result || typeof result.applied !== "boolean" || !(result.body instanceof Uint8Array)) {
|
||||
throw new Error("transform returned an unexpected shape");
|
||||
}
|
||||
return { ok: true, reason: result.reason, durationMs: Date.now() - startedAt };
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getInstallInfo, isInstalling, findNpm } from "./install.js";
|
||||
import { getLoadedInfo, loadPxpipe, selfTest } from "./loader.js";
|
||||
|
||||
// Aggregate status for the Token Saver card and /api/pxpipe/status.
|
||||
// "running" in library mode = module loaded into this process.
|
||||
export function getPxpipeStatus() {
|
||||
const install = getInstallInfo();
|
||||
const loaded = getLoadedInfo();
|
||||
return {
|
||||
installed: install.installed,
|
||||
installing: isInstalling(),
|
||||
version: install.version,
|
||||
path: install.path,
|
||||
running: loaded.loaded,
|
||||
loadedAt: loaded.loadedAt || null,
|
||||
uptimeMs: loaded.loaded ? Date.now() - loaded.loadedAt : 0,
|
||||
npmAvailable: !!findNpm(),
|
||||
mode: "library", // in-process transform, not an external proxy
|
||||
};
|
||||
}
|
||||
|
||||
// PRD health checklist, adapted to library mode: installed? → module loads
|
||||
// (the "executable found / port listening" equivalent) → test request transforms.
|
||||
export async function runHealthCheck() {
|
||||
const checks = [];
|
||||
const fail = (error) => ({ healthy: false, checks, error });
|
||||
|
||||
const install = getInstallInfo();
|
||||
checks.push({ id: "installed", label: "PXPIPE installed", ok: install.installed, detail: install.version ? `v${install.version}` : null });
|
||||
if (!install.installed) return fail("pxpipe not installed");
|
||||
|
||||
try {
|
||||
await loadPxpipe();
|
||||
checks.push({ id: "module", label: "Transform module loads", ok: true, detail: `v${install.version}` });
|
||||
} catch (e) {
|
||||
checks.push({ id: "module", label: "Transform module loads", ok: false, detail: e.message });
|
||||
return fail(`Cannot load module: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const test = await selfTest();
|
||||
checks.push({ id: "transform", label: "Test request transforms", ok: true, detail: `${test.durationMs}ms (${test.reason})` });
|
||||
} catch (e) {
|
||||
checks.push({ id: "transform", label: "Test request transforms", ok: false, detail: e.message });
|
||||
return fail(`Self-test failed: ${e.message}`);
|
||||
}
|
||||
|
||||
return { healthy: true, checks, error: null };
|
||||
}
|
||||
Reference in New Issue
Block a user