refactor: remove all CLI flags and behavior env vars — single codepath

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.
This commit is contained in:
teamchong
2026-05-19 17:01:05 -04:00
parent c4aa5af342
commit a66a558b55
6 changed files with 100 additions and 204 deletions
+14 -16
View File
@@ -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:
+8 -20
View File
@@ -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
+6 -5
View File
@@ -124,11 +124,12 @@ const DEFAULTS: Required<TransformOptions> = {
// `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
+48 -148
View File
@@ -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:<port>/ (default port 47821)
Options:
-p, --port <N> listen port (default 47821)
--upstream <URL> 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 <system-reminder> blocks
--no-tool-results don't image-compress large tool_result content
--min-chars <N> skip system compression below this many chars (default 2000)
--min-reminder-chars <N> per-block fast-skip threshold for reminders (default 10000)
--min-tool-result-chars <N> per-block fast-skip threshold for tool_results (default 10000)
--cols <N> soft-wrap column count (default 100)
--multi-col <N> 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 <P> 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<void> {
// 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<void> {
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<void> {
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}/`);
});
+11 -7
View File
@@ -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 () => {
+13 -8
View File
@@ -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"