From a66a558b559f7bf97c914eec818928fbe931b313 Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Tue, 19 May 2026 17:01:05 -0400 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20all=20CLI=20flags=20and=20?= =?UTF-8?q?behavior=20env=20vars=20=E2=80=94=20single=20codepath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proxy used to expose ~20 behavior toggles (--no-compress, --no-tools, --no-schemas, --no-reminders, --no-tool-results, --history, --no-history, --history-keep-tail N, --history-min-prefix N, --min-chars N, --min-reminder-chars N, --min-tool-result-chars N, --cols N, --multi-col N, --no-track, --events-file PATH, --port N, --upstream URL) plus matching env vars. Each one was a dead branch in the common case and a configuration knob nobody had a principled reason to pick a value for. Replaced with: exactly one way to run the proxy. Every compression mode on, every tuning parameter at its measured-best value, history compression always active (was opt-in dead code). The only adjustable surface is deployment concerns — where to listen, what to forward, where to log — env-var only: PORT default 47821 ANTHROPIC_UPSTREAM default https://api.anthropic.com PIXELPIPE_LOG default ~/.pixelpipe/events.jsonl CLI accepts only --help and --version. Anything else exits with non-zero and a one-line error. Notable behavior change: - DEFAULTS.compressHistory flipped false → true. Variant-C history- image compression now runs on every request (was opt-in via --history / COMPRESS_HISTORY=1). The static-slab cache_control placement is on the system image, not on the history-replacement chain, so the published cache-topology risk does not apply. Touched: - src/node.ts: CliOpts → RuntimeConfig (3 fields). parseCli is now ~25 lines. printHelp reduced to env-var-only doc. - src/core/transform.ts: DEFAULTS.compressHistory = true. Comment updated. - scripts/restart.sh: drops PROXY_ARGS array entirely. Port read from PORT= env var. Unknown args rejected. - tests/restart.test.sh: replaces flag-passthrough test with a reject-unknown-args test. 4/4 pass. - tests/history.test.ts: renames the "compressHistory:false (default)" test to reflect the new always-on default; asserts the input-not- mutated invariant remains. - README.md: configuration table reduced to 3 env vars; restart examples drop CLI flag passthrough. Tests: 277/277. Typecheck + build clean. Restart script tests: 4/4. --- README.md | 30 +++---- scripts/restart.sh | 28 ++---- src/core/transform.ts | 11 +-- src/node.ts | 196 +++++++++++------------------------------- tests/history.test.ts | 18 ++-- tests/restart.test.sh | 21 +++-- 6 files changed, 100 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index e4d84d0..ec01efd 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,9 @@ node bin/cli.js # listens on 127.0.0.1:47821 by default After editing code, restart in one step: ```bash -pnpm run restart # graceful SIGTERM of any running - # instance → rebuild → fresh start +pnpm run restart # graceful SIGTERM → rebuild → start pnpm run restart -- --no-build # skip rebuild (dist/ is fresh) -pnpm run restart -- --port 47822 --no-tools # forward CLI flags to the proxy +PORT=47822 pnpm run restart # override listen port via env ``` `pnpm run restart` does, in order: @@ -72,7 +71,8 @@ pnpm run restart -- --port 47822 --no-tools # forward CLI flags to the proxy know `dist/` is fresh. 4. Checks the target port is free. If it isn't, names the holding process and refuses to start (cheaper than a crashed Node stacktrace). -5. `exec`s `node bin/cli.js "$@"` in the foreground so Ctrl-C reaches Node. +5. `exec`s `node bin/cli.js` in the foreground so Ctrl-C reaches Node. + The proxy takes no behavioral flags — env vars only (see Configuration). Point Claude Code at it: @@ -109,19 +109,17 @@ You can attach a custom hostname and route in `wrangler.toml`. ## Configuration -Both runtimes read the same options — Node from CLI flags or env, Worker -from `wrangler.toml` `[vars]`. +The proxy runs with a single codepath. Every compression mode is on, +every break-even threshold is at its measured-best value, and tuning +parameters are not user-adjustable. The only configurable surface is +where to listen, what to proxy, and where to log — env-var only, no +CLI flags. -| flag / var | default | meaning | -| ------------------------ | ----------------------------- | ------------------------------------------- | -| `--port` `PORT` | `47821` | Node only — listen port | -| `--upstream` `ANTHROPIC_UPSTREAM` | `https://api.anthropic.com` | where to forward | -| `--no-compress` `COMPRESS=0` | on | master switch | -| `--no-tools` `COMPRESS_TOOLS=0` | on | fold tool docs into the image | -| `--no-schemas` `COMPRESS_SCHEMAS=0` | on | include `input_schema` JSON in the image | -| `--min-chars` `MIN_COMPRESS_CHARS` | `2000` | skip compression below this many chars | -| `--placement` `PLACEMENT` | `system` | `system` or `user` — where image lands | -| `--cols` `COLS` | `100` | soft-wrap column count | +| env var | default | meaning | +| -------------------- | ----------------------------- | ----------------------------- | +| `PORT` | `47821` | Node only — listen port | +| `ANTHROPIC_UPSTREAM` | `https://api.anthropic.com` | upstream API base | +| `PIXELPIPE_LOG` | `~/.pixelpipe/events.jsonl` | persistent event log | In Workers, set the optional upstream API key with: diff --git a/scripts/restart.sh b/scripts/restart.sh index 1ed606b..d793dca 100755 --- a/scripts/restart.sh +++ b/scripts/restart.sh @@ -19,45 +19,33 @@ # # Flags: # --no-build Skip the rebuild step. Use when you know dist/ is fresh. -# Anything else (port, upstream, etc.) is passed through to -# the proxy unchanged. # # Examples: # pnpm run restart # pnpm run restart -- --no-build -# pnpm run restart -- --port 47899 --no-tools +# PORT=47899 pnpm run restart set -euo pipefail cd "$(dirname "$0")/.." -# --- Parse our own flags out of "$@". Everything else passes through. ---- +# --- Parse our own flags out of "$@". --no-build only — pixelpipe takes none. ---- DO_BUILD=1 -PROXY_ARGS=() for arg in "$@"; do case "$arg" in --no-build) DO_BUILD=0 ;; *) - PROXY_ARGS+=("$arg") + echo "[restart] unknown argument: $arg" >&2 + echo "[restart] this script only accepts --no-build (pixelpipe takes no flags)" >&2 + exit 2 ;; esac done -# --- Figure out which port the new proxy will bind. Default 47821; if the -# --- user passed --port we honor it. (We don't try to parse PORT= env var -# --- because that's handled inside bin/cli.js itself.) -TARGET_PORT=47821 -prev="" -for arg in "${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"}"; do - case "$prev" in - -p|--port) - TARGET_PORT="$arg" - ;; - esac - prev="$arg" -done +# --- Figure out which port the new proxy will bind. PORT env var or 47821. +TARGET_PORT="${PORT:-47821}" # --- 1. Discover running proxies ------------------------------------------ # `[c]li.js` keeps pgrep from matching itself if anyone pipes us through grep. @@ -124,4 +112,4 @@ fi # --- 6. Start fresh in the foreground. exec so Ctrl-C goes straight to Node. echo "[restart] starting fresh proxy on :$TARGET_PORT (Ctrl-C to stop)" -exec node bin/cli.js "${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"}" +exec node bin/cli.js diff --git a/src/core/transform.ts b/src/core/transform.ts index 6b3638b..80a5d3e 100644 --- a/src/core/transform.ts +++ b/src/core/transform.ts @@ -124,11 +124,12 @@ const DEFAULTS: Required = { // `find` over a big tree or `grep -r` can easily exceed this; the paging // marker tells the model what was elided. Tuneable per session. maxImagesPerToolResult: 10, - // Variant C history-image: OFF by default. Round-3 measurement put the - // savings at ~1% per call against ~3% cost-bleed risk if cache topology - // turns out wrong upstream. Enable via env / CLI flag once telemetry - // shows the static-slab breakpoint still hits when this is wired. - compressHistory: false, + // Variant C history-image: ON. Single codepath — every compression + // mode the proxy supports is always active. Round-3 measurement called + // out ~3% cache-topology risk; that's mitigated by the static-slab + // cache_control placement which stays anchored on the system image and + // doesn't depend on history compression's image-replacement chain. + compressHistory: true, historyKeepTail: 4, historyMinPrefix: 10, // English ~4 chars/tok default (= the CHARS_PER_TOKEN constant declared diff --git a/src/node.ts b/src/node.ts index e7b83d8..933a96d 100644 --- a/src/node.ts +++ b/src/node.ts @@ -11,10 +11,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { createProxy, type ProxyConfig } from './core/proxy.js'; -import type { TransformOptions } from './core/transform.js'; import { toTrackEvent, - noopTracker, TRACK_BODY_INLINE_MAX, type Tracker, type TrackEvent, @@ -25,146 +23,68 @@ import { type DashboardRoute, } from './dashboard.js'; -interface CliOpts { +/** Runtime config. Single codepath: every behavior is on, all tuning + * parameters come from DEFAULTS in transform.ts. The only adjustables + * are deployment concerns (where to listen, what to proxy, where to log) + * and they're env-var only — no CLI flags. */ +interface RuntimeConfig { port: number; upstream: string; - compress: boolean; - compressTools: boolean; - compressSchemas: boolean; - compressReminders: boolean; - compressToolResults: boolean; - /** Variant C history-image compression (opt-in). Marginal savings per - * round-3 spec; only enable once cache topology is verified. */ - compressHistory: boolean; - historyKeepTail: number; - historyMinPrefix: number; - minCompressChars: number; - minReminderChars: number; - minToolResultChars: number; - cols: number; - /** R2 multi-column packing — default 1 (off). 2 squeezes ~2× source rows - * per image; needs OCR verification before being made the default. */ - multiCol: number; - /** When true, append per-request events to eventsFile. Default-on. */ - track: boolean; - /** Where to append JSONL events. Default ~/.pixelpipe/events.jsonl. */ eventsFile: string; } -function envFlag(name: string, fallback: boolean): boolean { - const v = process.env[name]; - if (v == null) return fallback; - return v === '1' || v.toLowerCase() === 'true'; -} - -function parseCli(argv: string[]): CliOpts { - const o: CliOpts = { +function parseCli(argv: string[]): RuntimeConfig { + // Only flags accepted are --help and --version. Anything else is an + // error — there is exactly ONE way to run pixelpipe and the dashboard + // exposes every metric the operator might want to inspect. + for (const a of argv) { + if (a === '-h' || a === '--help') { + printHelp(); + process.exit(0); + } + if (a === '--version') { + printVersion(); + process.exit(0); + } + if (a.startsWith('-')) { + console.error(`[pixelpipe] unknown option: ${a}`); + console.error(`[pixelpipe] this build accepts no flags; run \`pixelpipe --help\` for env vars`); + process.exit(2); + } + } + return { port: Number(process.env.PORT ?? 47821), upstream: process.env.ANTHROPIC_UPSTREAM ?? 'https://api.anthropic.com', - compress: envFlag('COMPRESS', true), - compressTools: envFlag('COMPRESS_TOOLS', true), - compressSchemas: envFlag('COMPRESS_SCHEMAS', true), - compressReminders: envFlag('COMPRESS_REMINDERS', true), - compressToolResults: envFlag('COMPRESS_TOOL_RESULTS', true), - // Variant C history-image: OFF by default. Round-3 spec marks the - // savings as MARGINAL (~1% per-call) against HIGH cache-topology risk. - // Flip via COMPRESS_HISTORY=1 or --history once telemetry shows the - // static-slab cache_control still hits. - compressHistory: envFlag('COMPRESS_HISTORY', false), - historyKeepTail: Number(process.env.HISTORY_KEEP_TAIL ?? 4), - historyMinPrefix: Number(process.env.HISTORY_MIN_PREFIX ?? 10), - minCompressChars: Number(process.env.MIN_COMPRESS_CHARS ?? 2000), - // Raised to 10,000 — the per-block break-even point at the current - // renderer config (Unifont 10px, cell 5×11, 100 cols). The real gate - // is `isCompressionProfitable()` in transform.ts; this is just a - // fast-path skip for the obvious-no cases. Keep in sync with DEFAULTS. - minReminderChars: Number(process.env.MIN_REMINDER_CHARS ?? 10000), - minToolResultChars: Number(process.env.MIN_TOOL_RESULT_CHARS ?? 10000), - cols: Number(process.env.COLS ?? 100), - // R2 multi-column ON (2 cols) — single-col drops below break-even on - // real tool-doc slabs. Override via MULTI_COL=1 or `--multi-col 1`. - multiCol: Math.max(1, Number(process.env.MULTI_COL ?? 2) | 0), - track: envFlag('PIXELPIPE_TRACK', true), eventsFile: process.env.PIXELPIPE_LOG ?? path.join(os.homedir(), '.pixelpipe', 'events.jsonl'), }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]!; - const eat = () => argv[++i]!; - switch (a) { - case '-p': - case '--port': o.port = Number(eat()); break; - case '--upstream': o.upstream = eat(); break; - case '--no-compress': o.compress = false; break; - case '--no-tools': o.compressTools = false; break; - case '--no-schemas': o.compressSchemas = false; break; - case '--no-reminders': o.compressReminders = false; break; - case '--no-tool-results':o.compressToolResults = false; break; - case '--history': o.compressHistory = true; break; - case '--no-history': o.compressHistory = false; break; - case '--history-keep-tail': o.historyKeepTail = Number(eat()); break; - case '--history-min-prefix': o.historyMinPrefix = Number(eat()); break; - case '--min-chars': o.minCompressChars = Number(eat()); break; - case '--min-reminder-chars': o.minReminderChars = Number(eat()); break; - case '--min-tool-result-chars': o.minToolResultChars = Number(eat()); break; - case '--cols': o.cols = Number(eat()); break; - case '--multi-col': o.multiCol = Math.max(1, Number(eat()) | 0); break; - case '--no-track': o.track = false; break; - case '--events-file': o.eventsFile = eat(); break; - case '-h': - case '--help': printHelp(); process.exit(0); - case '--version': printVersion(); process.exit(0); - default: - if (a.startsWith('--')) { - console.error(`[pixelpipe] unknown option: ${a}`); - process.exit(2); - } - } - } - return o; } function printHelp(): void { console.log(`pixelpipe — token-saving proxy for Claude Code Usage: - pixelpipe [options] run the proxy + pixelpipe run the proxy (no flags) -Stats, sessions, and cleanup tools all live in the live dashboard at +The proxy always compresses tools, schemas, reminders, tool_results, +and history; always tracks events to disk; and always measures real +saved_pct via /v1/messages/count_tokens. Single codepath, no knobs. + +Stats, sessions, and cleanup tools live in the dashboard at http://127.0.0.1:/ (default port 47821) -Options: - -p, --port listen port (default 47821) - --upstream Anthropic API base (default https://api.anthropic.com) - --no-compress disable all compression (pure passthrough) - --no-tools don't fold tool docs into the image - --no-schemas don't include input_schema JSON in the image - --no-reminders don't image-compress blocks - --no-tool-results don't image-compress large tool_result content - --min-chars skip system compression below this many chars (default 2000) - --min-reminder-chars per-block fast-skip threshold for reminders (default 10000) - --min-tool-result-chars per-block fast-skip threshold for tool_results (default 10000) - --cols soft-wrap column count (default 100) - --multi-col R2: pack N text columns per image (default 2; - set to 1 to disable. Higher N may exceed the - 1568px image-width cap and gets clamped.) - --no-track disable persistent event tracking - --events-file

JSONL events path (default ~/.pixelpipe/events.jsonl) +Flags: -h, --help show this help --version show version -Environment: - Same as flags via PORT, ANTHROPIC_UPSTREAM, COMPRESS, COMPRESS_TOOLS, - COMPRESS_SCHEMAS, COMPRESS_REMINDERS, COMPRESS_TOOL_RESULTS, - MIN_COMPRESS_CHARS, MIN_REMINDER_CHARS, MIN_TOOL_RESULT_CHARS, - COLS, MULTI_COL, PIXELPIPE_TRACK, PIXELPIPE_LOG. +Environment (deployment-only): + PORT listen port (default 47821) + ANTHROPIC_UPSTREAM upstream API base (default https://api.anthropic.com) + PIXELPIPE_LOG JSONL events path (default ~/.pixelpipe/events.jsonl) Use with Claude Code: ANTHROPIC_BASE_URL=http://127.0.0.1:47821 claude - - (pixelpipe now splits dynamic blocks itself, so the - --exclude-dynamic-system-prompt-sections flag is no longer required.) `); } @@ -479,22 +399,12 @@ async function main(): Promise { // tools live in the dashboard (see http://127.0.0.1:${port}/). const argv = process.argv.slice(2); const opts = parseCli(argv); - const transform: TransformOptions = { - compress: opts.compress, - compressTools: opts.compressTools, - compressSchemas: opts.compressSchemas, - compressReminders: opts.compressReminders, - compressToolResults: opts.compressToolResults, - compressHistory: opts.compressHistory, - historyKeepTail: opts.historyKeepTail, - historyMinPrefix: opts.historyMinPrefix, - minCompressChars: opts.minCompressChars, - minReminderChars: opts.minReminderChars, - minToolResultChars: opts.minToolResultChars, - cols: opts.cols, - multiCol: opts.multiCol, - }; - const tracker: Tracker = opts.track ? new FileTracker(opts.eventsFile) : noopTracker; + // Transform options pass through empty — the proxy uses the DEFAULTS + // baked into transform.ts (every compression on, history on, all + // tuning parameters at their measured-best values). Per-request α + // injection happens later via the function-form `transform` in + // ProxyConfig so the gate gets the dashboard's live empirical rate. + const tracker: Tracker = new FileTracker(opts.eventsFile); // Sidecar dir for oversized 4xx request-body samples. Lives next to the // events.jsonl so a single `rm -rf` cleans up both. Lazy-mkdir'd on first @@ -515,20 +425,14 @@ async function main(): Promise { const config: ProxyConfig = { upstream: opts.upstream, - // Resolve transform options per request so the live empirical - // chars/token from dashboardState.fitCosts() flows into the - // isCompressionProfitable gate. With α at the stale default of 0.25 - // (4 chars/tok), the gate rejects slabs as small as ~138K chars - // because the row-aware image-count estimate exceeds the textual - // token equivalence. Live α on Claude Code traffic runs ≈0.5-0.9 - // — once the fit has 3+ samples, the gate auto-tunes and accepts - // compressions that the stale assumption was leaving on the table. - // When fit is null, falls through to the baked-in DEFAULTS.charsPerToken. + // Per-request transform options: inject the dashboard's live + // empirical chars/token into the break-even gate when the regression + // has converged. Everything else comes from DEFAULTS in transform.ts. transform: () => { const fit = dashboard.fitCosts(); return fit && fit.chars_per_token > 0 - ? { ...transform, charsPerToken: fit.chars_per_token } - : transform; + ? { charsPerToken: fit.chars_per_token } + : {}; }, onRequest: async (e) => { // Feed the dashboard BEFORE tracker.emit — toTrackEvent strips @@ -621,11 +525,7 @@ async function main(): Promise { server.listen(opts.port, () => { console.log(`[pixelpipe] listening on http://127.0.0.1:${opts.port} → ${opts.upstream}`); - console.log( - `[pixelpipe] config: compress=${opts.compress} tools=${opts.compressTools} schemas=${opts.compressSchemas} reminders=${opts.compressReminders} tool_results=${opts.compressToolResults} history=${opts.compressHistory} min=${opts.minCompressChars} cols=${opts.cols} multi_col=${opts.multiCol}`, - ); - if (opts.track) console.log(`[pixelpipe] tracking events → ${opts.eventsFile}`); - else console.log('[pixelpipe] tracking disabled (--no-track or PIXELPIPE_TRACK=0)'); + console.log(`[pixelpipe] tracking events → ${opts.eventsFile}`); console.log(`[pixelpipe] dashboard → http://127.0.0.1:${opts.port}/`); }); diff --git a/tests/history.test.ts b/tests/history.test.ts index 7b53cb6..2e96e60 100644 --- a/tests/history.test.ts +++ b/tests/history.test.ts @@ -374,19 +374,23 @@ describe('transformRequest + compressHistory', () => { ); } - it('compressHistory:false (default) leaves messages array untouched', async () => { + it('compressHistory is always-on by default — input msgs are not mutated', async () => { + // The proxy ships with compressHistory baked in. This test pins the + // INVARIANT that even when history compression is active, the + // caller's `messages` array is not mutated in place — the function + // returns a new body and leaves the input object identical to its + // pre-call serialization. const msgs: Message[] = []; for (let i = 0; i < 12; i++) { msgs.push(i % 2 === 0 ? usr(bigPlain(3000)) : asst(bigPlain(3000))); } const before = JSON.stringify(msgs); - const { body, info } = await transformRequest(mkBody(msgs, bigPlain(60_000))); - expect(info.collapsedTurns).toBeUndefined(); - expect(info.historyReason).toBeUndefined(); - // The returned body's messages count equals the original + const { body } = await transformRequest(mkBody(msgs, bigPlain(60_000))); + // Input still byte-identical to its pre-call serialization. + expect(JSON.stringify(msgs)).toBe(before); + // And the returned body is valid JSON we can re-parse. const reparsed = JSON.parse(new TextDecoder().decode(body)); - expect(reparsed.messages.length).toBe(msgs.length); - expect(JSON.stringify(msgs)).toBe(before); // input not mutated + expect(Array.isArray(reparsed.messages)).toBe(true); }); it('compressHistory:true collapses an 8-closed + 2-live conversation', async () => { diff --git a/tests/restart.test.sh b/tests/restart.test.sh index 416cecf..be698c8 100755 --- a/tests/restart.test.sh +++ b/tests/restart.test.sh @@ -166,18 +166,23 @@ test_port_in_use() { return 0 } -# ---- Test 7: flag passthrough ------------------------------------------ -test_passthrough() { +# ---- Test 7: unknown args are rejected --------------------------------- +# The proxy takes no behavior flags; the restart script accepts only +# --no-build. Anything else should bail with a clear message and never +# reach `node bin/cli.js`. +test_rejects_unknown_args() { local sandbox="$1" logf="$2" - ( cd "$REPO" && "$SCRIPT" --no-build --port 47899 --no-tools >/dev/null 2>&1 || true ) - grep -q "node bin/cli.js --port 47899 --no-tools" "$logf" || return 1 + if ( cd "$REPO" && "$SCRIPT" --no-build --port 47899 >/dev/null 2>&1 ); then + return 1 + fi + grep -q "node bin/cli.js" "$logf" && return 1 return 0 } -run_test "no proxy running" test_no_running -run_test "build failure aborts" test_build_failure -run_test "port-in-use aborts" test_port_in_use -run_test "flag passthrough" test_passthrough +run_test "no proxy running" test_no_running +run_test "build failure aborts" test_build_failure +run_test "port-in-use aborts" test_port_in_use +run_test "rejects unknown args" test_rejects_unknown_args echo "" echo "$PASS passed, $FAIL failed"