Skip to content

teamchong/pxpipe

Repository files navigation

pxpipe

Cut Claude Code's input tokens by rendering bulky context as images — the same system prompt, tool docs, and history, in a fraction of the tokens.

An image's token cost is fixed by its pixel dimensions, not by how much text is inside it. Dense content (code, JSON, tool output) packs ~3.1 chars per image-token vs ~1 char per text-token on real Claude Code traffic. The reader is the same vision channel that Anthropic's computer use already relies on for screenshots. pxpipe is a local proxy that uses that channel for context: it rewrites the bulky parts of each request into compact PNGs before it leaves your machine. At current Fable list prices that lands as a ~59–70% lower end-to-end bill — but prices move and workloads differ, so the durable number is the token cut itself, measured per-request against a free count_tokens counterfactual in ~/.pxpipe/events.jsonl.

This is what the model sees instead of text:

example: a real transformRequest output: system prompt + tool docs reflowed into one dense page, instruction banner on top, ↵ marking original newlines

~48k chars of system prompt + tool docs: ≈25k tokens as text, ≈2.7k image tokens as this page. Real pipeline output; the model reads renders like this at 100/100 (see benchmarks).

chart: characters a frontier context window holds, 2018–2026 — vendor text series including Grok 4.5; orange measured overlay is Fable 5 [1m] + pxpipe ~18M (4.6×)

Eight years of context growth, in characters. Every text line tops out near ~4M chars (a 1M-token window at ~4 chars/token); Grok 4.5 is shown as a text-window point only (500K). The orange overlay is the same Fable 5 1M window read through pxpipe images — ~18M chars at the measured Anthropic density (4.6× the text ceiling). Density is measured from a live render at generation time, not hand-typed: regenerate with npx tsx scripts/gen-context-chart.ts (source).

Demo

Fable 5 (the default, 100/100 reader) — plain left, pxpipe right:

Fable-AB-Demo.mp4

pxpipe counts an exact token 10/10 across 39 imaged filler files (matches grep line-for-line), gets the multi-step ledger arithmetic right, and ends the session at $6.06 with context to spare (73.5k/1M) vs $42.21 at 96% full. One caveat visible in the clip: the pxpipe arm needed a nudge to match the requested one-line output format.

Opus 4.8 (disabled by default) — same layout:

Opus-AB-Demo.mp4

Text needles read fine on both arms; the imaged phrase-count doesn't read on Opus — and pxpipe says so instead of fabricating a number. That misread rate is why Opus is opt-in.

Try it (30 seconds)

npx pxpipe-proxy                                  # proxy on 127.0.0.1:47821
ANTHROPIC_BASE_URL=http://127.0.0.1:47821 claude  # point Claude Code at it

Dashboard at http://127.0.0.1:47821/: tokens saved, every text→image conversion side by side, kill switch, live model chips. Responses stream normally — pxpipe compresses the request only, never the model's output. Recent turns stay text; the system prompt, tool docs, and older bulk history are imaged.

The honest part

  • It is lossy. Exact 12-char hex strings in dense imaged content: 13/15 on Fable 5, 0/15 on Opus, and 0/15 on Sol — misses are silent confabulations, not errors. Byte-exact values (IDs, hashes, secrets) must stay text; recent turns do. A dedicated verbatim-risk guard is not built yet.
  • Escape hatch: subagents on non-allowlisted models pass through as text — route byte-exact work there (CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6, or model: sonnet in agent frontmatter).
  • Real work: SWE-bench Lite pilot 10/10 both arms at −65% request size; SWE-bench Pro 14/19 ON vs 15/19 OFF at −60%, verdicts agree 18/19, and the single split re-resolved 3/3 on replication — run-to-run variance, not compression. Small n; receipts in eval/.
  • Workload-dependent. Wins on token-dense content (~1 char/token), loses money on sparse prose (~3.5 chars/token); a profitability gate (calibrated on N=391 production rows) images only where the math wins.
  • Client-dependent, not product-name-dependent. Savings track uncached bulk the client still re-sends as text. Claude Code re-sends system + tools
    • history on /anthropic/messages and typically lands ~60–70%. Codex on /v1/responses is supported; when the prompt is already ~98% cached_tokens, only the static slab (and rare history collapses) remain to image, so Saved can honestly sit near 1%. The same Responses path saves tens of percent when history collapse fires, and an OpenAI client that re-sends the full transcript as plain text each turn is in the same high- savings class as Claude Code. Details and measured splits: docs/CACHING_AND_SAVINGS.md.
  • Model scope: default PXPIPE_MODELS=claude-fable-5. Sol, Opus 4.7/4.8, GPT 5.5, and Grok are opt-in only (dashboard chips or PXPIPE_MODELS) — not good enough as silent defaults for imaged context. Grok packing + factsheet helps exact IDs, but quality remains below Fable: 82/100 arithmetic, 83/98 gist, and 13/18 state tracking. The exact Sol id still matters. Sibling variants such as gpt-5.6-terra do not inherit Sol's allowlist or render profile. PXPIPE_MODELS=off disables imaging. Everything else passes through byte-identical. On the GPT path, tool definitions stay native JSON and no Anthropic cache_control markers are used. Responses history compression recognizes adjacent completed function_call/function_call_output pairs: only old closed pairs are imaged; the newest six completed pairs, every open call, and malformed/orphan state remain native. The default history budget is 32 images; opt-in long-session coverage can be raised (defensive cap 100) with PXPIPE_GPT_HISTORY_MAX_IMAGES=48 after validating the provider's request cap.
  • Per-model rendering: opt-in gpt-5.6-sol uses a 152-column, 5×8 Spleen profile; Claude keeps its 312-column 5×8 Spleen profile. These are selected by exact model id, including history pages and profitability math. Sol quality: production 5×8 scored 98/100 arithmetic and 79/93 completed gist, 18/18 state, 4/15 completed never-stated confabulations, and 0/15 dense hex. Exact IDs therefore use the verbatim factsheet, and recent/open tool state stays native. Sol receipts and profile evidence.
  • Grok 4.5 (opt-in): same production recipe as Sol (5×8 Spleen, IDS, text factsheet; Grok strip maxH 512). Off by default (not Fable-level pure-image). Measured production results are 82/100 arithmetic, 83/98 gist, and 13/18 state tracking. Enable with PXPIPE_MODELS=claude-fable-5,grok-4.5 or the dashboard chip. eval/grok-density/QUALITY_RESULTS.md.

Benchmarks (reproducible)

Model quality (does the model read the images?)

Every model row below uses the same production recipe unless a pure-image research arm is called out: 5×8 Spleen + IDS block + adjacent text factsheet. Claude numbers are novel problems the model cannot have memorized. Sol and Grok quality use Codex’s Responses provider; Fable/Opus use Claude. Token deltas compare matched input arms: negative saves tokens; positive costs more. The historical GSM8K run measured −38%, but it is a different corpus and is not used for these novel-arithmetic rows.

test model N text pxpipe (image) tokens
novel arithmetic claude-fable-5 100 100% 100% not measured
novel arithmetic gpt-5.6-sol 100 100% 98% +32%
novel arithmetic claude-opus-4-8 100 100% 93% not measured
novel arithmetic grok-4.5 100 100% 82% +7%
gist recall A/B (decisions, values, paths, names, negations; distractors; 15k–45k char sessions) Fable 5 98/arm 98/98 98/98 not measured
same gist corpus, production images + factsheet gpt-5.6-sol 98 not measured 79/93 completed; 1 session error not measured
same gist corpus, production images + factsheet grok-4.5 98 98/98 83/98 not measured
state tracking (value mutated 3×, final/first/count) Fable 5 18/arm 18/18 18/18 not measured
same state-tracking corpus gpt-5.6-sol 18 not measured 18/18 latest not measured
same state-tracking corpus grok-4.5 18 18/18 13/18 not measured
confabulation on never-stated facts (lower is better) Fable 5 16/arm 0/16 0/16 not measured
same never-stated probes (lower is better) gpt-5.6-sol 16 not measured 4/15 completed; 1 session error not measured
same never-stated probes (lower is better) grok-4.5 16 0/16 0/16 not measured
verbatim 12-char hex, dense render Opus 15 15/15 0/15 not measured
verbatim 12-char hex, dense render Fable 5 15 not measured 13/15 not measured
verbatim 12-char hex, same dense pages gpt-5.6-sol 15 not measured 0/15 not measured
verbatim 12-char hex, same dense pages grok-4.5 15 15/15 0/6 completed; 9 transport errors not measured

Harness split: Fable/Opus quality and SWE-bench rows use Claude; Sol and Grok quality use Codex’s Responses provider (OPENAI_BASE_URL, typically ocproxy) — see eval/grok-density/QUALITY_SUITE.md.

Sol receipts: eval/sol-profile/QUALITY_RESULTS.md. Grok receipts: eval/grok-density/QUALITY_RESULTS.md. SWE-bench is not copied to Sol: its runner is Claude Code/Fable-specific (ANTHROPIC_BASE_URL, Claude CLI, official Docker grading), and no Sol ON/OFF run exists yet. Pure-image-only is not Fable-grade on live Grok.

Capacity / density (how many chars per vision-token?)

Measured by rendering this repo’s dense fixture through the real pipeline and pricing pixels at each family’s vision rate. Multiplier = measured chars/vision-token ÷ 4 (prose text baseline). Not a model-quality score.

family window as text (@4 c/tok) as pxpipe images density multiplier
claude-fable-5[1m] (default) 1M ~4.0M ~18.3M ~18.3 c/vt (px÷750) ~4.6×

Regenerate: npx tsx scripts/gen-context-chart.ts · chart PNG docs/assets/context-window-chars.png.

SWE-bench run totals, receipts, and caveats: eval/swe-bench/ · eval/swe-bench-pro/ · eval/needle-haystack/ · eval/gist-recall/ · eval/grok-density/ · analysis in FINDINGS.md. (GSM8K scored 96% imaged, but it's in training data — memorized answers survive misreads — so we lead with the novel-number evals.)

How it works

model id ──► render profile ──► wrap/reflow bulk context ──► PNG[] + exact-token factsheet

The proxy intercepts /v1/messages, rewrites eligible bulk into image blocks, splices them back cache-friendly (static prefix preserved, prompt caching keeps working), and forwards. Every enabled model gets the same production stack: 5×8 Spleen pages, in-image IDS block, and adjacent text factsheet. Claude uses 1568×728 pages; GPT 5.6 Sol uses 768px-wide portrait strips; opt-in Grok 4.5 uses 152-col strips (maxH 512). A per-request estimator uses that same resolved profile, so sparse prose stays text. Events log to ~/.pxpipe/events.jsonl.

Library use (no proxy)

import { renderTextToImages, transformAnthropicMessages } from "pxpipe-proxy";

const { pages } = await renderTextToImages(toolResultText);     // pages[i].png: Uint8Array
const { body, applied, info } = await transformAnthropicMessages({
  body: requestBytes,
  model: "claude-fable-5",
});

options.keepSharp(block) pins blocks as text; options.emitRecoverable returns the originals of imaged blocks. Pure-JS runtime (Node and edge/Workers); @napi-rs/canvas is build-time only. Full API: src/core/index.ts.

Development

pnpm install && pnpm test
pnpm run build                # regenerates dist/

FAQ

Is the headline end-to-end, or only on the requests you touched? End-to-end, the whole bill. Most compression tools report savings only on the input slice they touched, which flatters the number. The end-to-end denominator is every production request: the small ones pxpipe correctly left untouched, all cache writes and reads, and all output tokens (which the proxy never compresses). On a 13,709-request snapshot that was 59% ($100 → ~$41); a later 8,904-compressed-request trace measured ~70%. Compressed-only runs higher (~72–74%) and is quoted separately, never as the headline. The exact figure is workload-dependent — reproduce it on your own log.

How is the math measured? Both sides of the same request, at the same moment. For every /v1/messages POST the proxy fires a free count_tokens probe on the original uncompressed body (the counterfactual) in parallel with the real forward, and reads Anthropic's actually-billed usage block off the response. Both land in the same row of ~/.pxpipe/events.jsonl, so there is no turn-count or run-to-run confound. Dollar conversion uses Fable 5 list ratios: input ×1.0, cache write ×1.25, cache read ×0.1, output ×5. Cache pricing is applied identically to both sides, so the caching discount cancels and cannot be double-counted as "savings". Re-derive it yourself from the events log: the formula and field names are documented in src/core/baseline.ts.

What does it actually compress? Three kinds of input blocks, each behind a profitability gate:

  1. large tool_result bodies (file reads, command output, logs) above ~6k chars of token-dense content
  2. older collapsed history: turns behind the live tail get re-rendered as image pages, recent turns always stay text
  3. the static system prompt + tool docs slab

Everything else passes through byte-identical: your messages, recent turns, the model's output (it is the response, the proxy never touches it), sparse prose, and anything too small to win. Fable 5 is the only built-in default. Sol, Opus, GPT 5.5, and Grok remain explicit opt-ins. Sol's production 5×8 run scored 98/100 arithmetic, 79/93 completed gist, 18/18 state, 4/15 completed never-stated confabulations, and 0/15 dense hex. Grok scored 82/100 arithmetic, 83/98 gist, and 13/18 state.

Has it ever failed for real, outside the benchmarks? Yes, once in weeks of daily use: the model recalled a person's name from imaged chat history and got it confidently wrong. No error, just a plausible wrong name. That is the documented failure mode: exact strings in imaged content are not byte-safe. Coding sessions tolerate this because the agent re-reads files before editing; pure chat recall has no such check. This failure mode is measured, not anecdotal: the legibility audit quantifies exact-string recall off rendered pages (blind reads top out at 63% on dense identifiers, with every miss predicted by a glyph-confusability matrix) and documents the shipped mitigations — page geometry clamped to the API's resample cap so billed pixels actually reach the vision encoder, and exact identifiers (SHAs, numbers) riding alongside as text.

Why are misses silent confabulations instead of read errors? Because model vision is not OCR: the image becomes patch embeddings, never discrete characters, so there is no per-glyph confidence to fail loudly on. When pixels underdetermine a glyph, the language prior fills the gap with something plausible. Mechanism and receipts: docs/NOT-OCR.md.

Didn't DeepSeek-OCR show this doesn't hold up in practice? No: it proved the channel works, using an encoder/decoder pair trained for the job. The skepticism dates from October 2025, when no stock production model could read dense renders; that changed with Fable 5 (0/15 verbatim hex on Opus 4.8 vs 13/15 on Fable 5, same pages). Timeline and per-model numbers: docs/NOT-OCR.md.

Why does the README read like an AI wrote it? Because one did. Most of this repo's commits — the code and the docs — were authored by Opus/Fable agent sessions running behind pxpipe itself, reading their own collapsed history as image pages while they worked.

Limitations

  • Lossy (above); verbatim recall from images is unreliable.
  • PNG encoding adds latency to large requests before they leave.
  • ASCII/Latin-1 well tested; CJK works but conservatively.

Roadmap

Rendering research is parked as of 2026-07-05: verbatim misreads are capacity-bound, not trick-bound, so no font/color/layout change fixes exact-string recall at profitable density. The why is in docs/NOT-OCR.md; the dated analysis and the three documented follow-up threads (glyph-style A/B with banked pages, runtime canary + re-fetch, surrogate-reader pre-flight) are in FINDINGS.md, 2026-07-05 entry. Watch condition: re-run the resolution sweep per model release; readable density moved ~4x in glyph area from Opus 4.8 to Fable 5, and a model that reads production cells near 100% means savings rise for free.

Still open, unchanged: whether imaged bulk stretches effective context (~2x the real content in the same 1M window), and whether a smaller active context improves long-task accuracy. Hypotheses, not claims — they ship as numbers with an n or they get cut.

License

MIT.

About

cut Fable 5 token usage by rendering text context as images

Resources

License

Stars

5.9k stars

Watchers

25 watching

Forks

Packages

 
 
 

Contributors