diff --git a/src/app/api/pxpipe/health/route.js b/src/app/api/pxpipe/health/route.js
new file mode 100644
index 00000000..f974342b
--- /dev/null
+++ b/src/app/api/pxpipe/health/route.js
@@ -0,0 +1,16 @@
+import { NextResponse } from "next/server";
+import { runHealthCheck } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+
+export async function POST() {
+ try {
+ const result = await runHealthCheck();
+ return NextResponse.json(result);
+ } catch (error) {
+ return NextResponse.json({ healthy: false, checks: [], error: error.message }, { status: 500 });
+ }
+}
+
+// GET mirrors POST so the card can probe on page load without a mutation call.
+export const GET = POST;
diff --git a/src/app/api/pxpipe/install/route.js b/src/app/api/pxpipe/install/route.js
new file mode 100644
index 00000000..c5eedcc0
--- /dev/null
+++ b/src/app/api/pxpipe/install/route.js
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import { installPxpipe } from "@/lib/pxpipe/install.js";
+import { unloadPxpipe } from "@/lib/pxpipe/loader.js";
+import { runHealthCheck } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+// npm install can legitimately take minutes on a cold cache.
+export const maxDuration = 300;
+
+// Install (or repair — same operation, reinstalls @latest) then re-run the health check.
+export async function POST() {
+ try {
+ const info = await installPxpipe();
+ unloadPxpipe(); // drop any previously-loaded version so health loads the fresh one
+ const health = await runHealthCheck();
+ return NextResponse.json({ ...info, health });
+ } catch (error) {
+ return NextResponse.json({ error: error.message, code: error.code || null }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/logs/route.js b/src/app/api/pxpipe/logs/route.js
new file mode 100644
index 00000000..051db386
--- /dev/null
+++ b/src/app/api/pxpipe/logs/route.js
@@ -0,0 +1,18 @@
+import { NextResponse } from "next/server";
+import { getInstallLogTail } from "@/lib/pxpipe/install.js";
+import { readPxpipeEvents } from "@/lib/pxpipe/events.js";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const limit = Math.min(Number(searchParams.get("limit")) || 100, 500);
+ return NextResponse.json({
+ installLog: getInstallLogTail(),
+ events: readPxpipeEvents({ limit }).reverse(),
+ });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/restart/route.js b/src/app/api/pxpipe/restart/route.js
new file mode 100644
index 00000000..1aaab39a
--- /dev/null
+++ b/src/app/api/pxpipe/restart/route.js
@@ -0,0 +1,16 @@
+import { NextResponse } from "next/server";
+import { unloadPxpipe, loadPxpipe } from "@/lib/pxpipe/loader.js";
+import { getPxpipeStatus } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+
+// Reload the in-process module (picks up an upgraded install without a server restart).
+export async function POST() {
+ try {
+ unloadPxpipe();
+ await loadPxpipe();
+ return NextResponse.json(getPxpipeStatus());
+ } catch (error) {
+ return NextResponse.json({ error: error.message, code: error.code || null }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/start/route.js b/src/app/api/pxpipe/start/route.js
new file mode 100644
index 00000000..5dad8b2b
--- /dev/null
+++ b/src/app/api/pxpipe/start/route.js
@@ -0,0 +1,26 @@
+import { NextResponse } from "next/server";
+import { getSettings } from "@/lib/localDb";
+import { getInstallInfo, installPxpipe } from "@/lib/pxpipe/install.js";
+import { loadPxpipe } from "@/lib/pxpipe/loader.js";
+import { getPxpipeStatus } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+export const maxDuration = 300;
+
+// "Start" in library mode = warm the in-process transform module.
+// Auto-installs first when the package is missing and pxpipeAutoInstall is on.
+export async function POST() {
+ try {
+ if (!getInstallInfo().installed) {
+ const settings = await getSettings();
+ if (!settings.pxpipeAutoInstall) {
+ return NextResponse.json({ error: "PXPIPE is not installed", code: "NOT_INSTALLED" }, { status: 409 });
+ }
+ await installPxpipe();
+ }
+ await loadPxpipe();
+ return NextResponse.json(getPxpipeStatus());
+ } catch (error) {
+ return NextResponse.json({ error: error.message, code: error.code || null }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/stats/route.js b/src/app/api/pxpipe/stats/route.js
new file mode 100644
index 00000000..c5860b6e
--- /dev/null
+++ b/src/app/api/pxpipe/stats/route.js
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+import { getPxpipeStats } from "@/lib/pxpipe/events.js";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const recentLimit = Math.min(Number(searchParams.get("limit")) || 100, 500);
+ return NextResponse.json(getPxpipeStats({ recentLimit }));
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/status/route.js b/src/app/api/pxpipe/status/route.js
new file mode 100644
index 00000000..663c8398
--- /dev/null
+++ b/src/app/api/pxpipe/status/route.js
@@ -0,0 +1,21 @@
+import { NextResponse } from "next/server";
+import { getSettings } from "@/lib/localDb";
+import { getPxpipeStatus } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ try {
+ const settings = await getSettings();
+ const status = getPxpipeStatus();
+ return NextResponse.json({
+ ...status,
+ enabled: !!settings.pxpipeEnabled,
+ autoInstall: !!settings.pxpipeAutoInstall,
+ minChars: settings.pxpipeMinChars,
+ timeoutMs: settings.pxpipeTimeoutMs,
+ });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/app/api/pxpipe/stop/route.js b/src/app/api/pxpipe/stop/route.js
new file mode 100644
index 00000000..cc675c52
--- /dev/null
+++ b/src/app/api/pxpipe/stop/route.js
@@ -0,0 +1,16 @@
+import { NextResponse } from "next/server";
+import { unloadPxpipe } from "@/lib/pxpipe/loader.js";
+import { getPxpipeStatus } from "@/lib/pxpipe/service.js";
+
+export const dynamic = "force-dynamic";
+
+// "Stop" in library mode = drop the in-process module; requests fail open to
+// uncompressed passthrough until it is started again.
+export async function POST() {
+ try {
+ const wasLoaded = unloadPxpipe();
+ return NextResponse.json({ stopped: wasLoaded, ...getPxpipeStatus() });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/lib/db/repos/requestDetailsRepo.js b/src/lib/db/repos/requestDetailsRepo.js
index 2f308015..defd294b 100644
--- a/src/lib/db/repos/requestDetailsRepo.js
+++ b/src/lib/db/repos/requestDetailsRepo.js
@@ -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(
diff --git a/src/lib/db/repos/settingsRepo.js b/src/lib/db/repos/settingsRepo.js
index 0057cc1c..7b147e1e 100644
--- a/src/lib/db/repos/settingsRepo.js
+++ b/src/lib/db/repos/settingsRepo.js
@@ -42,6 +42,10 @@ const DEFAULT_SETTINGS = {
cavemanLevel: "full",
ponytailEnabled: false,
ponytailLevel: "full",
+ pxpipeEnabled: false,
+ pxpipeAutoInstall: true,
+ pxpipeMinChars: 25000,
+ pxpipeTimeoutMs: 15000,
};
async function readRaw() {
diff --git a/src/lib/pxpipe/events.js b/src/lib/pxpipe/events.js
new file mode 100644
index 00000000..b1bb880d
--- /dev/null
+++ b/src/lib/pxpipe/events.js
@@ -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(),
+ };
+}
diff --git a/src/lib/pxpipe/install.js b/src/lib/pxpipe/install.js
new file mode 100644
index 00000000..cdf5a8ce
--- /dev/null
+++ b/src/lib/pxpipe/install.js
@@ -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 "";
+ }
+}
diff --git a/src/lib/pxpipe/loader.js b/src/lib/pxpipe/loader.js
new file mode 100644
index 00000000..fbd418f5
--- /dev/null
+++ b/src/lib/pxpipe/loader.js
@@ -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 };
+}
diff --git a/src/lib/pxpipe/service.js b/src/lib/pxpipe/service.js
new file mode 100644
index 00000000..d117c82b
--- /dev/null
+++ b/src/lib/pxpipe/service.js
@@ -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 };
+}
diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js
index 2c1206df..c8cf8161 100644
--- a/src/shared/components/Sidebar.js
+++ b/src/shared/components/Sidebar.js
@@ -25,6 +25,7 @@ const navItems = [
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" },
+ { href: "/dashboard/pxpipe", label: "PXPIPE", icon: "image" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
];
diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js
index 571930e0..a0c187f5 100644
--- a/src/sse/handlers/chat.js
+++ b/src/sse/handlers/chat.js
@@ -12,6 +12,8 @@ import { getSettings } from "@/lib/localDb";
import { getModelInfo, getComboModels } from "../services/model.js";
import { handleChatCore } from "open-sse/handlers/chatCore.js";
import { DEFAULT_HEADROOM_URL } from "@/lib/headroom/detect";
+import { getTransform as getPxpipeTransform } from "@/lib/pxpipe/loader.js";
+import { appendPxpipeEvent } from "@/lib/pxpipe/events.js";
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
import { handleComboChat, handleFusionChat } from "open-sse/services/combo.js";
import { handleBypassRequest } from "open-sse/utils/bypassHandler.js";
@@ -259,6 +261,12 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
cavemanLevel: chatSettings.cavemanLevel || "full",
ponytailEnabled: !!chatSettings.ponytailEnabled,
ponytailLevel: chatSettings.ponytailLevel || "full",
+ pxpipeEnabled: !!chatSettings.pxpipeEnabled,
+ pxpipeMinChars: chatSettings.pxpipeMinChars,
+ pxpipeTimeoutMs: chatSettings.pxpipeTimeoutMs,
+ // Lazily warms the in-process module on first use; null when not installed (fail-open)
+ pxpipeTransform: chatSettings.pxpipeEnabled ? await getPxpipeTransform() : null,
+ onPxpipeEvent: appendPxpipeEvent,
providerThinking,
// Detect source format by endpoint + body
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
diff --git a/tests/unit/pxpipe.test.js b/tests/unit/pxpipe.test.js
new file mode 100644
index 00000000..5c49f126
--- /dev/null
+++ b/tests/unit/pxpipe.test.js
@@ -0,0 +1,95 @@
+import { describe, expect, it, vi } from "vitest";
+import { compressWithPxpipe, formatPxpipeLog } from "../../open-sse/rtk/pxpipe.js";
+
+const bigText = "x".repeat(30000);
+const claudeBody = () => ({
+ model: "claude-fable-5",
+ max_tokens: 100,
+ messages: [{ role: "user", content: bigText }],
+});
+
+// A transform double mimicking pxpipe-proxy/transform's contract.
+const appliedTransform = (outBody) => async () => ({
+ applied: true,
+ reason: "applied",
+ body: new TextEncoder().encode(JSON.stringify(outBody)),
+ info: { compressedChars: 25000, imageCount: 2, imageBytes: 5000, imagePixels: 1500000 },
+ cache: { ownsCacheControl: true, markerCount: 1 },
+});
+
+describe("compressWithPxpipe gates", () => {
+ it("skips when disabled", async () => {
+ const { body, summary } = await compressWithPxpipe(claudeBody(), { enabled: false });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("disabled");
+ });
+
+ it("skips when transform is unavailable (not installed)", async () => {
+ const { body, summary } = await compressWithPxpipe(claudeBody(), { enabled: true, format: "claude", transform: null });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("not_installed");
+ });
+
+ it("skips non-Claude formats", async () => {
+ const transform = vi.fn();
+ const { body, summary } = await compressWithPxpipe(claudeBody(), { enabled: true, format: "openai", transform });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("unsupported_format");
+ expect(transform).not.toHaveBeenCalled();
+ });
+
+ it("bypasses small prompts below minChars", async () => {
+ const transform = vi.fn();
+ const small = { model: "claude-fable-5", messages: [{ role: "user", content: "hi" }] };
+ const { body, summary } = await compressWithPxpipe(small, { enabled: true, format: "claude", minChars: 25000, transform });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("below_threshold");
+ expect(transform).not.toHaveBeenCalled();
+ });
+
+ it("applies the transform and reports savings", async () => {
+ const compressed = { model: "claude-fable-5", messages: [{ role: "user", content: "imaged" }] };
+ const { body, summary } = await compressWithPxpipe(claudeBody(), {
+ enabled: true, format: "claude", minChars: 1000, transform: appliedTransform(compressed),
+ });
+ expect(body).toEqual(compressed);
+ expect(summary.applied).toBe(true);
+ expect(summary.imageCount).toBe(2);
+ expect(summary.tokensBeforeEst).toBeGreaterThan(summary.tokensAfterEst);
+ expect(summary.savedPct).toBeGreaterThan(0);
+ expect(formatPxpipeLog(summary)).toContain("2 image(s)");
+ });
+
+ it("passes through when the transform declines (not_profitable)", async () => {
+ const transform = async () => ({ applied: false, reason: "not_profitable", body: new Uint8Array(), info: {} });
+ const { body, summary } = await compressWithPxpipe(claudeBody(), {
+ enabled: true, format: "claude", minChars: 1000, transform,
+ });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("not_profitable");
+ });
+
+ it("fails open when the transform throws", async () => {
+ const transform = async () => { throw new Error("boom"); };
+ const { body, summary } = await compressWithPxpipe(claudeBody(), {
+ enabled: true, format: "claude", minChars: 1000, transform,
+ });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("transform_error");
+ expect(summary.detail).toBe("boom");
+ });
+
+ it("fails open on timeout", async () => {
+ const transform = () => new Promise(() => {}); // never resolves
+ const { body, summary } = await compressWithPxpipe(claudeBody(), {
+ enabled: true, format: "claude", minChars: 1000, timeoutMs: 50, transform,
+ });
+ expect(body).toBeNull();
+ expect(summary.reason).toBe("timeout");
+ });
+
+ it("does not log skipped requests as savings", () => {
+ expect(formatPxpipeLog({ applied: false, reason: "below_threshold" })).toBeNull();
+ expect(formatPxpipeLog(null)).toBeNull();
+ });
+});
From 74d5fedf7950d40202591aed1dbc6927254f4ff9 Mon Sep 17 00:00:00 2001
From: decolua
Date: Fri, 10 Jul 2026 17:32:45 +0700
Subject: [PATCH 43/73] feat(headroom): activate/uninstall extras + fix
interpreter detection
- find interpreter next to headroom binary so extras/version read correctly
- add on/off toggle to activate [code]/[ml] via proxy restart
- add uninstall action + live install log progress + ~1GB confirm modal
---
.../dashboard/token-saver/TokenSaverClient.js | 207 ++++++++++++++++--
src/app/api/headroom/extras/route.js | 20 +-
src/app/api/headroom/restart/route.js | 35 +++
src/app/api/headroom/start/route.js | 6 +-
src/lib/headroom/detect.js | 36 ++-
src/lib/headroom/process.js | 91 +++++++-
tests/unit/headroom-detect.test.js | 19 +-
7 files changed, 367 insertions(+), 47 deletions(-)
create mode 100644 src/app/api/headroom/restart/route.js
diff --git a/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js b/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
index fc1f9152..4e641f3d 100644
--- a/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
+++ b/src/app/(dashboard)/dashboard/token-saver/TokenSaverClient.js
@@ -1,7 +1,7 @@
"use client";
-import { useState, useEffect, useCallback } from "react";
-import { Card, Button, Input, Modal, Toggle } from "@/shared/components";
+import { useState, useEffect, useCallback, useRef } from "react";
+import { Card, Button, Input, Modal, Toggle, ConfirmModal } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
import {
@@ -33,6 +33,13 @@ export default function TokenSaverClient() {
const [pendingExtras, setPendingExtras] = useState([]);
const [extrasActionLoading, setExtrasActionLoading] = useState(false);
const [extrasActionError, setExtrasActionError] = useState("");
+ const [removingExtra, setRemovingExtra] = useState(null);
+ const [installLog, setInstallLog] = useState("");
+ const [extrasConfirm, setExtrasConfirm] = useState(null);
+ const [codeAware, setCodeAware] = useState(false);
+ const [kompress, setKompress] = useState(true);
+ const [restartingProxy, setRestartingProxy] = useState(false);
+ const logPollRef = useRef(null);
const [cavemanEnabled, setCavemanEnabled] = useState(false);
const [cavemanLevel, setCavemanLevel] = useState("full");
const [ponytailEnabled, setPonytailEnabled] = useState(false);
@@ -205,10 +212,37 @@ export default function TokenSaverClient() {
);
};
- const handleInstallExtras = useCallback(async () => {
+ // Poll the install log tail while a pip install/uninstall is running.
+ const startLogPolling = useCallback(() => {
+ setInstallLog("");
+ if (logPollRef.current) clearInterval(logPollRef.current);
+ const tick = async () => {
+ try {
+ const r = await fetch("/api/headroom/extras?log=1", {
+ headers: { "Cache-Control": "no-store" },
+ });
+ const d = await r.json().catch(() => ({}));
+ if (typeof d.log === "string") setInstallLog(d.log);
+ } catch { /* ignore transient poll errors */ }
+ };
+ tick();
+ logPollRef.current = setInterval(tick, 1500);
+ }, []);
+
+ const stopLogPolling = useCallback(() => {
+ if (logPollRef.current) {
+ clearInterval(logPollRef.current);
+ logPollRef.current = null;
+ }
+ }, []);
+
+ useEffect(() => () => stopLogPolling(), [stopLogPolling]);
+
+ const installExtrasConfirmed = useCallback(async () => {
if (pendingExtras.length === 0) return;
setExtrasActionLoading(true);
setExtrasActionError("");
+ startLogPolling();
try {
const res = await fetch("/api/headroom/extras", {
method: "POST",
@@ -226,9 +260,83 @@ export default function TokenSaverClient() {
} catch (e) {
setExtrasActionError(e.message);
} finally {
+ stopLogPolling();
setExtrasActionLoading(false);
}
- }, [pendingExtras]);
+ }, [pendingExtras, startLogPolling, stopLogPolling]);
+
+ const removeExtraConfirmed = useCallback(async (extra) => {
+ setRemovingExtra(extra);
+ setExtrasActionError("");
+ startLogPolling();
+ try {
+ const res = await fetch("/api/headroom/extras", {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ extras: [extra] }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(data.error || "Remove failed");
+ setHeadroomExtras((s) => ({
+ ...s,
+ version: data.version ?? s.version,
+ extras: data.extras || s.extras,
+ }));
+ } catch (e) {
+ setExtrasActionError(e.message);
+ } finally {
+ stopLogPolling();
+ setRemovingExtra(null);
+ }
+ }, [startLogPolling, stopLogPolling]);
+
+ const handleInstallExtras = useCallback(() => {
+ if (pendingExtras.length === 0) return;
+ // Warn about the heavy ~1GB torch download before installing [ml].
+ if (pendingExtras.includes("ml")) {
+ setExtrasConfirm({
+ title: "Install [ml]",
+ message: "[ml] downloads ~1 GB (torch + huggingface-hub). Continue?",
+ confirmText: "Install",
+ variant: "primary",
+ onConfirm: installExtrasConfirmed,
+ });
+ return;
+ }
+ installExtrasConfirmed();
+ }, [pendingExtras, installExtrasConfirmed]);
+
+ const handleRemoveExtra = useCallback((extra) => {
+ setExtrasConfirm({
+ title: `Remove [${extra}]`,
+ message: `Remove [${extra}] and its packages?`,
+ confirmText: "Remove",
+ variant: "danger",
+ onConfirm: () => removeExtraConfirmed(extra),
+ });
+ }, [removeExtraConfirmed]);
+
+ // Toggle an extra's active state (persist setting), then restart the proxy so
+ // the new --code-aware / --disable-kompress flags take effect.
+ const toggleExtraActive = useCallback(async (extra, value) => {
+ setExtrasActionError("");
+ if (extra === "code") setCodeAware(value);
+ if (extra === "ml") setKompress(value);
+ const key = extra === "code" ? "headroomCodeAware" : "headroomKompress";
+ await patchSetting({ [key]: value });
+ if (!headroomStatus.running) return;
+ setRestartingProxy(true);
+ try {
+ const res = await fetch("/api/headroom/restart", { method: "POST" });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(data.error || "Restart failed");
+ await refreshHeadroomStatus();
+ } catch (e) {
+ setExtrasActionError(e.message);
+ } finally {
+ setRestartingProxy(false);
+ }
+ }, [headroomStatus.running, refreshHeadroomStatus]);
const handleCavemanLevel = (level) => {
setCavemanLevel(level);
@@ -307,6 +415,8 @@ export default function TokenSaverClient() {
setRtkEnabledState(data.rtkEnabled !== false);
setHeadroomEnabled(!!data.headroomEnabled);
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
+ setCodeAware(data.headroomCodeAware === true);
+ setKompress(data.headroomKompress !== false);
setCavemanEnabled(!!data.cavemanEnabled);
setCavemanLevel(data.cavemanLevel || "full");
setPonytailEnabled(!!data.ponytailEnabled);
@@ -387,7 +497,7 @@ export default function TokenSaverClient() {
onChange={() => handleRtkEnabled(!rtkEnabled)}
/>