Project rename for the Show HN launch with framing:
'pixelpipe — what if Opus saw a UI instead of just append-only logs?'
Renamed everywhere:
- kebab-case 'claude-image-proxy' -> 'pixelpipe' (42 occurrences)
- snake_case 'claude_image_proxy' -> 'pixelpipe' (5 occurrences)
- 14 files touched: package.json, Cargo.toml, Cargo.lock, *.md,
bin/cli.js, scripts/install.js, src/proxy.py, src/rust/src/main.rs,
src/rust/examples/{render_sample,transform_only,diff_render}.rs,
src/zig/build.zig
Effects on build artifacts:
- npm package name -> 'pixelpipe'
- Rust binary name -> target/release/pixelpipe (was claude-image-proxy)
- Python cache dir -> ~/.cache/pixelpipe/ (was ~/.cache/claude-image-proxy/)
NOT YET DONE in this commit (Tier 2+3, handled in next commit):
- Repo on-disk directory still ~/Downloads/repos/claude-image-proxy/
- Cache dir on disk still ~/.cache/claude-image-proxy/ — Python proxy
currently writes here. Will be migrated alongside the running services
in a coordinated stop/move/restart sequence.
Validation: scripts/diff_transform.py still produces exactly 1 structural
diff (the PNG bytes, expected — different fonts Python/Menlo vs
Rust/JetBrains Mono). No regression from the rename.
Three bug fixes surfaced by scripts/diff_transform.py harness, which
synthesizes a realistic Claude Code request body and runs it through both
Python transform_request() and Rust transform(), then JSON-structurally
diffs the outputs.
Before these fixes, harness found:
- 2 structural diffs in the transformed body JSON
- 4+ TransformInfo divergences
After these fixes, harness finds:
- 1 structural diff: $.messages[0].content[1].source.data
(the base64 PNG, expected to differ — Menlo vs JetBrains Mono fonts)
- TransformInfo still differs on byte-vs-char counts (telemetry-only)
Fixes:
1. src/rust/src/transform.rs:93 — per-tool markdown header
was: '### Tool: {name}\n{desc}'
now: '#### Tool: {name}\n{desc}\n'
Matches proxy.py:230,233. Also restores the trailing newline that
Python emits between tool entries.
2. src/rust/src/transform.rs:123 — tool section markdown header
was: '\n\n# Tool Documentation\n\n'
now: '\n\n#### Tool Documentation\n\n'
Matches proxy.py:248.
3. src/rust/src/transform.rs:265 — preserve system field by default
was: always replaced system with billing_line OR remainder
now: only replaces if billing_line was stripped, otherwise leaves
the original system field untouched.
Matches proxy.py:417 which intentionally leaves system alone in the
default PLACEMENT="user" path 'so Anthropic still has guardrails'.
The image lives in messages[].content as a side-channel; system stays
as original text. This is the behavior our 95+ live Python requests
have validated against Anthropic's API.
4. src/rust/src/lib.rs — expose 'transform' module so harnesses and the
transform_only example can call transform() without going through HTTP.
Added:
- src/rust/examples/transform_only.rs — CLI binary that reads a body
from a file and writes the transformed body + TransformInfo as JSON.
- scripts/diff_transform.py — Python harness that calls both Python
transform_request() and the Rust transform_only binary, then runs a
structural JSON diff with field-by-field TransformInfo comparison.
Validation:
python3 scripts/diff_transform.py
# → 'found 1 structural diff'
# → $.messages[0].content[1].source.data (the PNG, expected)
Remaining known telemetry-only diffs (not blocking the swap):
- system_text_chars: Python counts Unicode chars, Rust counts UTF-8 bytes
- tool_text_added: same UTF-8 issue
- expected_image_tokens: ~30% higher in Rust due to ab_glyph 4px cell
vs PIL 3px cell — Rust images are 33% wider for the same text. Rust
PNG bytes are still smaller overall because of the tight-bound height
(which Python also has now, after the previous baseline commit).
- placement, png_sha8 fields missing from Rust TransformInfo
- min_chars threshold uses bytes (Rust) vs chars (Python)
These can be addressed later for full telemetry parity but the
upstream-visible behavior is byte-equivalent.
src/proxy.py:125 was computing image height as
tallest = max(lines_per_col, last_col_lines) if c_needed == 1 else lines_per_col
The max(lines_per_col, ...) made every single-column render pad to the
full MAX_EDGE=1568 canvas height regardless of content, wasting ~99% of
the image-token budget for small prompts.
Fix: tight-bound the single-column case.
tallest = max(1, last_col_lines) if c_needed == 1 else lines_per_col
Multi-column (c_needed > 1) is unchanged — all earlier columns must be
lines_per_col tall to fit them, so the image must be too.
Measured impact (scripts/visual_compare.py):
sample before after change
----------- ------------ ------------ -----------
17 chars 241x1568 241x14 -99% pixels
176 chars 241x1568 241x42 -97% pixels
885 chars 241x1568 241x315 -80% pixels
Verified edge cases hold:
- empty/short text: still gated by MIN_COMPRESS_CHARS
- 5000-char single line: 241x441 (63 wrapped lines, tight)
- 80-char boundary: 241x14 (2 lines, tight)
- 500-line multi-column: 731x1568 (unchanged, correctly padded)
- 20KB realistic system prompt: 68.5% savings (matches existing 65-73%)
Added scripts/visual_compare.py to inspect Python vs Rust renderings
side-by-side with labels and dimensions.
This is the immediate win that's independent of the Rust rewrite — every
proxied request gets fewer image tokens starting at the next proxy restart.
Functional reimplementation in Rust with the same render_chunks/transform
public surface as src/proxy.py. Compiles clean on darwin-arm64, smoke-
tested but NOT YET battle-tested against real Claude Code sessions.
Layout:
src/rust/
Cargo.toml hyper + tokio + reqwest + ab_glyph + png + clap
Cargo.lock committed (application crate, deterministic builds)
src/main.rs clap CLI matching bin/cli.js flags
src/lib.rs module roots
src/font.rs ab_glyph rasterization of bundled JetBrains Mono
src/render.rs newspaper-column text -> PNG (matches Python algo)
src/transform.rs request body transformation (system prompt -> image)
src/proxy.rs hyper HTTP server, upstream forwarding
src/stats.rs persisted savings counters mirroring proxy.py
assets/JetBrainsMono-Regular.ttf OFL-licensed, legally embeddable
examples/render_sample.rs smoke test: render fibonacci snippet
examples/diff_render.rs diff harness companion (paired with
scripts/diff_render.py)
Diff harness:
scripts/diff_render.py feeds same text through Python and Rust
render_chunks, decodes both PNGs, compares
pixel-by-pixel. First run found two divergences:
1. Rust font.rs:43 uses ab_glyph h_advance which returns ~3.4px ceiled
to 4 for Menlo-5pt 'M'; PIL's getlength('M') returns 3.0 exactly.
Every Rust column is 33% wider than Python's.
2. Python proxy.py:125 pads every single-column image to MAX_EDGE=1568
height regardless of content (already noted in baseline commit).
Neither is fixed yet — surfacing the divergences before any swap. Next:
visual inspect both PNGs to confirm Rust output is at least OCR-readable,
then decide whether to fix bug-for-bug compat or correct both sides.
bin/cli.js still spawns Python; this commit only adds the Rust tree, it
does NOT change runtime behavior. The live proxy at :47821 is unaffected.
Token-saving HTTP proxy for Claude Code that renders system prompt + tools
as 5pt Menlo monospace images. Achieves 65-73% token reduction with 100%
reasoning quality preserved on Opus 4.7.
Production state at this commit:
- src/proxy.py: 972 lines, battle-tested with 46+ real Claude Code sessions
observed (~551k tokens saved, ~$8.27 in stats.json as of commit time)
- bin/cli.js: Node wrapper that spawns Python proxy on port 47821
- scripts/gen_atlas.py: generates the bundled Menlo 5pt glyph atlas
- scripts/install.js: postinstall — verifies Python + Pillow + httpx
- src/zig/: earlier Zig 0.16 renderer (kept for reference, not built)
- CLAUDE.md, HANDOFF.md: contributor notes
- package.json: npm entry point, version 0.1.0
Known issue (deferred for separate fix):
src/proxy.py:125 — `tallest = max(lines_per_col, last_col_lines) if
c_needed == 1 else lines_per_col` pads every single-column image to
full MAX_EDGE=1568 height regardless of content, wasting image-token
budget for small inputs. Tight bounding would improve savings further.
This is the rollback point. Any subsequent changes (Rust port, Python
fixes, etc.) build on this proven baseline.