通过将庞大的上下文渲染为图像来削减 Claude Code 的输入 token——相同的系统提示词、工具文档和历史记录,仅需一小部分 token。
图像的 token 成本由其像素尺寸决定,而非内部包含的文本量。在真实的 Claude Code 流量中,密集内容(代码、JSON、工具输出)每个图像 token 可容纳约 3.1 个字符,而每个文本 token 仅约 1 个字符。pxpipe 是一个利用这一差距的本地代理:它在请求离开你的机器之前,将请求中庞大的部分(系统提示词、工具文档、较早的历史记录)重写为紧凑的 PNG 图像。
节省效果取决于工作负载——pxpipe 在 token 密集的内容上表现优异,而稀疏/小型请求则保持不变——因此这些是实测快照,而非恒定值。主要且持久的结果是输入 token 的减少:密集的系统提示词、工具文档和历史记录以紧凑图像而非文本形式输入(上述示例中约 25k 文本 token 被渲染为约 2.7k 图像 token),每个请求都根据其自身的 count_tokens 反事实进行测量。费用是下游结果——按当前 Fable 标价计算,token 削减可带来约 59–70% 的端到端账单降低(压缩请求上约 72–74%;完整定价计算见 FAQ)。但标价明天可能变化,而 token 数量不会,因此应关注 token 而非金额。两者均可从 ~/.pxpipe/events.jsonl 复现。
这是模型看到的替代文本的内容:
约 48k 字符的系统提示词 + 工具文档(本仓库自身的 README、FINDINGS 和源代码),作为文本约 25k token,作为此页面约 2.7k 图像 token。由真实的 transformRequest 管道生成:空白字符最小化,重新排列为完整行,用 ↵ 标记原始换行,OCR 指令横幅共同渲染在顶部。模型在干净评估中以 100/100 读取此类渲染(见基准测试)。
演示
Fable 5 演示(默认,100/100 读取器):
Fable-AB-Demo.mp4
- 两个演示均在 Fable 5 上显示双面板(左侧为纯文本,右侧为 pxpipe)。
- Fable 能读取 Opus 无法读取的内容。Opus 拒绝处理的图像化短语计数(见下方 Opus 演示):pxpipe 分支在 39 个图像化填充文件中精确计数了 10/10 的 token(与 grep 真实结果逐行匹配),并正确完成了多步账本算术(8037 → … → 15,021)。
- 相同答案,成本降低约 7 倍。两次演示后的会话总计:普通版 $42.21,上下文占用 96%(964.5k/1M —— 再差一步就要强制压缩);而 pxpipe 版仅 $6.06,上下文还有富余(73.5k/1M)。
- 坦诚说明(视频中可见):pxpipe 分支先回答了计数问题,但需要一次后续提示才能按要求的一行格式打印账目余额;普通版则一次就遵循了格式。可读性问题已在 Fable 上解决 —— 单次回复的格式遵循是尚存的粗糙边缘。
Opus 4.8 演示(Opus 默认禁用):
Opus-AB-Demo.mp4
并排对比 —— 普通版 Claude(左)vs pxpipe(右),均使用 Opus 4.8(需手动启用;pxpipe 针对 Fable 进行了调优 —— 见上方 Fable 片段)。点击图片观看(Google Drive)。
- 演示 1 —— 修复一个失败的测试套件:两者均通过;仪表盘显示 pxpipe 将请求削减至极少的 token 量(真实、服务器端测量的上下文/token 缩减)。
- 演示 2 —— 一个大文件上下文(40 个文件,约 382k tokens)加上一个数学问题和一个“统计此短语”任务:两者都能读出数学答案(一个小型文本针)。短语统计需要读取图片填充内容 —— 因此 Opus 上的 pxpipe 无法读取,并诚实地表明不会编造数字(已记录的有损限制:精确数值保留为文本)。而普通版则逐个文件地统计,陷入卡顿。
立即尝试(30 秒)
npx pxpipe-proxy # proxy on 127.0.0.1:47821 ANTHROPIC_BASE_URL=http://localhost:47821 claude # point Claude Code at it
打开 http://127.0.0.1:47821/ 查看实时仪表盘:节省的 token 数、每次会话统计、所有文本→图像转换的并排对比、全局终止开关,以及运行时模型芯片(包括 GPT 5.6 和 GPT 5.5)。
其他一切不变。响应正常流式输出;pxpipe 仅压缩请求(即你发送的上下文),绝不压缩模型的输出。最近的对话轮次保持为文本;系统提示词、工具文档以及较早的批量历史记录则被转换为图像。
这是坦诚的部分,在依赖它之前请先阅读。
它是有损的。pxpipe 属于 Gist 级别,并非无损存储。在一项“大海捞针”评估中,密集图像内容中的精确 12 字符十六进制字符串,Opus 返回结果为 0/15,而 Fable 5 为 13/15,其失败模式是静默虚构:给出一个看似合理但错误的值,而非报错。任何你需要逐字节精确返回的内容(ID、哈希值、密钥、精确数字)都必须保留为文本。最近的版本已如此处理;但专用的逐字风险防护机制尚未构建。
精确召回的安全出口。pxpipe 仅对 Fable 请求进行图像化处理(`PXPIPE_MODELS=claude-fable-5`),因此任何使用非 Fable 模型的子智能体都会以文本形式通过。将需要逐字节精确值的工作路由到该模型——全局设置可使用 `CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6`,或在智能体前置元数据中按智能体设置 `model: sonnet`。它从源文件(文件/JSONL)读取,而非图像化历史记录。这覆盖了你特意路由的精确召回场景;但无法捕获你未预料到的静默误读——这正是上述尚未构建的防护机制。
它会破坏实际工作吗?在我们衡量的指标上表现相当:一个包含 10 个实例的 SWE-bench Lite 试点(简单子集),两组均解决了 10/10,pxpipe 开启时 token 等效成本为 27 美元,关闭时为 54 美元;19 对 SWE-bench Pro 任务(更困难、长周期),pxpipe 开启时解决了 14/19,关闭时解决了 15/19,每次请求成本降低 60%:两组在 18/19 的任务上判定一致,唯一的分歧(一次开启失败)在复现时以 3/3 重新解决,即属于运行间的智能体方差,而非压缩问题。样本量较小,详情和注意事项见下文。
节省成本的效果取决于工作负载。它在 token 密集内容(约 1 字符/token:代码、JSON、哈希值)上效果显著,而在稀疏的英文散文(约 3.5 字符/token)上则会亏损。内置门控机制仅对数学计算上划算的内容进行图像化处理,并根据 N=391 条生产数据行进行了校准。
模型范围:一个 PXPIPE_MODELS CSV 文件控制着哪些模型基础会在两个系列中被成像——默认值为 claude-fable-5,gpt-5.6(GPT 5.5 是可选加入的;它在成像上下文上表现退化)。将 PXPIPE_MODELS 设为 off 可完全禁用成像功能,或者使用 ~/.config/pxpipe/config.json 并设置 { "models": "off" }(或一个列表)。对于 GPT,pxpipe 将工具定义保留为原生 JSON(只有冗长的 schema 描述文本会进入图像),因此工具调用保持可靠;与 Claude 路径不同,GPT 路径不会添加或依赖 Anthropic 的 cache_control 提示词缓存标记。仪表盘芯片可以在不更改客户端配置的情况下实时切换任何模型。Opus 4.7/4.8 原本是 Claude 的范围,但误读了约 7% 的渲染结果(10200→9400),因此在 Fable 5 以相同的图像计费达到 100/100 后,默认将其关闭——你可以通过 PXPIPE_MODELS 或仪表盘芯片自行承担风险将其重新启用。其他所有内容均原样通过。
基准测试(可复现)
使用模型不可能记忆过的新型随机数问题进行测量:
| 测试 | N | 文本 | pxpipe(图像) | tokens |
|---|---|---|---|---|
| 新型算术,claude-fable-5 | 100 | 100% | 100% | −38% |
| 新型算术,claude-opus-4-8 | 100 | 100% | 93% | −38% |
| 要点回忆 A/B(决策、数值、路径、名称、否定;含干扰项;15k-45k 字符会话),Fable 5 | 98/组 | 98/98 | 98/98 | - |
| 状态追踪(数值被修改 3 次,最终值/首次值/计数),Fable 5 | 18/组 | 18/18 | 18/18 | - |
| 对从未陈述过的事实的虚构(越低越好),Fable 5 | 16/组 | 0/16 | 0/16 | - |
| 逐字 12 字符十六进制回忆,密集渲染,Opus | 15 | 15/15 | 0/15 | - |
| 逐字 12 字符十六进制回忆,密集渲染,Fable 5 | 15 | - | 13/15 | - |
SWE-bench Lite 试点(端到端任务质量)
10 个 SWE-bench Lite 实例,Claude Code + Fable 5,通过 pxpipe 开启与关闭进行配对运行,使用官方 swebench Docker 测试框架评分:
| pxpipe 开启 | 关闭 | |
|---|---|---|
| 已解决 | 10/10 | 10/10 |
| 请求大小 vs 自身未压缩主体 | −65% | ±0 |
−65% 是每个请求的(在压缩前对每个主体进行 count_tokens 探测),因此没有轮次计数的混淆。n=10/组,Lite 偏向简单。运行总计、收据、注意事项:eval/swe-bench/。
SWE-bench Pro 基准测试(更困难,长周期)
两次运行中完成了 19 个配对(2 个被丢弃:两个分支的检出均失败),相同设置,官方 SWE-bench_Pro-os Docker 测试框架:
| pxpipe 开启 | 关闭 | |
|---|---|---|
| 已解决 | 14/19 | 15/19 |
| 请求大小 vs 自身未压缩的请求体 | −60% | ±0 |
在 18/19 个实例上,裁决结果一致(其中三个实例的两条测试臂均失败,有一条臂的补丁字节完全相同)。唯一出现分歧的实例(navidrome,ON 臂失败)在 ON 臂上重复了 3 次:三次运行均生成了相同的补丁并成功解决,因此最初的失败是运行间智能体方差所致,而非压缩问题。凭证:eval/swe-bench-pro/。
我们还运行了 GSM8K:得分 96%。但 GSM8K 存在于训练数据中,因此模型会通过自身的误读来回忆记忆中的答案,从而虚增分数,所以我们改用干净的新数字评测作为主要依据。复现:eval/gsm8k/ · eval/needle-haystack/ · eval/gist-recall/ · 完整分析见 FINDINGS.md。
常见问题
标题中的数字是端到端的,还是仅针对你处理过的请求?是端到端的,涵盖全部费用。大多数压缩工具只报告它们处理过的输入部分的节省,这会美化数字。端到端的分母是所有生产请求:pxpipe 正确保留未处理的小请求、所有缓存写入和读取,以及所有输出 token(代理从不压缩输出)。在一个包含 13,709 个请求的快照中,节省为 59%($100 → ~$41);后续一个包含 8,904 个已压缩请求的跟踪记录测得约 70%。仅压缩请求的运行结果更高(约 72–74%),会单独列出,从不作为标题数字。具体数字取决于工作负载——请在你的日志上自行复现。
费用是如何计算的?同一请求的两侧,在同一时刻计算。对于每个 /v1/messages POST 请求,代理会针对原始未压缩的请求体(反事实情况)并行发起一个免费的 count_tokens 探测,同时进行真实转发,并从响应中读取 Anthropic 实际计费的用量数据块。两者都记录在 ~/.pxpipe/events.jsonl 的同一行中,因此不存在轮次或运行间的混淆。美元换算使用 Fable 5 列表费率:输入 ×1.0,缓存写入 ×1.25,缓存读取 ×0.1,输出 ×5。缓存定价对两侧相同应用,因此缓存折扣相互抵消,不会重复计入"节省"。你可以从事件日志中自行推导:公式和字段名在 src/core/baseline.ts 中有文档说明。
它实际上压缩了什么?三种输入块,每种都经过一个盈利性门槛:
- 大型工具结果体(文件读取、命令输出、日志)中超过约 6k 字符的高密度 token 内容
- 较早折叠的历史记录:位于实时尾部之前的轮次会被重新渲染为图像页面,而近期轮次始终保持文本形式
- 静态系统提示词 + 工具文档块
其余所有内容均以字节一致的方式通过:你的消息、近期轮次、模型的输出(它本身就是响应,代理从不触碰它)、稀疏的散文式文本,以及任何太小而无需处理的内容。非 Fable 模型则完全透传。
在基准测试之外,它是否真的失败过?是的,在数周的日常使用中出现过一次:模型从图像化的聊天历史中回忆了一个人的名字,并且自信地给出了错误答案。没有报错,只是一个看似合理的错误名字。这就是已记录在案的失败模式:图像化内容中的精确字符串不具备字节安全性。编码会话可以容忍这一点,因为代理在编辑前会重新读取文件;纯聊天回忆则没有这样的检查机制。
工作原理
tool_result string ──► wrap at 1928px-wide columns ──► pack ~92,000 chars/page ──► PNG[]
代理拦截 /v1/messages 请求,将符合条件的批量历史记录重写为图像块,以缓存友好的方式将它们拼接回去(保留静态前缀,使提示词缓存继续正常工作),然后转发。每次请求的事件会记录到 ~/.pxpipe/events.jsonl 文件中。
经济性分析:一张 1928×1928 的图像成本约为 4,761 个视觉 token,最多可容纳约 92,000 个字符(按观测密度计算约为 48,000 个文本 token),因此纯文本只有在密度超过约 19 字符/token 时才更便宜。Claude Code 的转录文本远低于这个密度(观测值为 1.91 字符/token,N=391)。运行时估算器(estimateImageCount)加上字符/token 门控机制会按请求进行决策;稀疏的散文式文本则保留为文本形式。
库使用(无代理模式)
相同的引擎,无需代理。将文本渲染为 PNG 图像,或运行完整的缓存安全转换:
import { renderTextToPngs, transformAnthropicMessages } from "pxpipe"; const imgs = await renderTextToPngs(toolResultText); // RenderedImage[] const { body, applied, info } = await transformAnthropicMessages({ body: requestBytes, model: "claude-fable-5", });
options.keepSharp(block) 可将指定块固定为文本形式(覆盖针对 ID、哈希值、路径的启发式规则);options.emitRecoverable 返回已图像化块的原始内容,以便有状态调用方能够恢复它们——这是针对下面有损限制的保真度契约的两个组成部分。运行时为纯 JavaScript(支持 Node 和边缘/Workers 环境);@napi-rs/canvas 仅在构建时使用。完整 API、类型和常量请参见 src/core/index.ts。
开发
pnpm install && pnpm test # 376 tests pnpm run build # regenerates dist/
局限性
- 有损性:请参见上文"诚实的一面"。从图像中逐字回忆是不可靠的。
- 渲染延迟:在大型请求离开前,编码 PNG 会额外增加时间(这部分被模型摄入更少 token 所部分抵消)。响应正常流式输出。
- ASCII/Latin-1 字符集经过充分测试;CJK(中日韩)字符可用,但采用保守策略。
- 运行时为纯 JavaScript——可在 Node 和边缘/Workers 环境运行。@napi-rs/canvas 仅作为构建时的开发依赖(用于重新生成字形图集),而非运行时依赖。
- 仅支持 Fable 5。
路线图
以上所有内容均经过实测。以下内容均未经实测。这些是假设,而非声明;它们要么以带 n 前缀的数字形式发布,要么被砍掉。
- 更清晰的字形。13/15 的逐字差距部分源于字体可读性,而非仅模型原因。跨渲染风格的逐字符混淆矩阵测试已暂停(eval/glyph-matrix/);若零成本样式能降低读取错误率,则门控机制可在相同保真度下实现更强压缩。
- 有效上下文。密集文本的 token 消耗约为图像的 1/3。若该结论在实时窗口中成立(而非仅计费层面),则 1M token 可承载约 2 倍的实际内容。待解问题:当大部分内容被图像化后,原本需要约 2M 原始上下文的任务能否在 Fable 的 1M 上下文窗口内运行?
- 更少的活跃文本,更敏锐的模型。长上下文随着内容填充会降低推理能力。将旧内容图像化可缩小模型实际读取的范围,同时保持其可访问性。假设:相同信息量,更小的活跃上下文,更优的长任务准确率。
一个赌注:基于同一套 Fable 5,实现更长的有效上下文和更敏锐的长任务模型。要么拿出数据,要么撤回,中间不掺杂任何炒作。
许可证
关于
通过将文本上下文渲染为图像来降低 Fable 5 的 token 用量
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. pxpipe is a local proxy that exploits that gap: it rewrites the bulky parts of your request (system prompt, tool docs, older history) into compact PNGs before the request leaves your machine.
Savings are workload-dependent — pxpipe wins on token-dense content and leaves sparse/small requests untouched — so these are measured snapshots, not constants. The primary, durable result is input-token reduction: dense system prompts, tool docs, and history go in as compact images instead of text (the example above is ≈25k text tokens rendered as ≈2.7k image tokens), every request measured against its own count_tokens counterfactual. Dollars are downstream of that — at current Fable list prices the token cut lands as a ~59–70% lower end-to-end bill (~72–74% on compressed requests; full pricing math in the FAQ). But list prices can change tomorrow and the token count won't, so tokens — not dollars — are the number to watch. Reproduce both from ~/.pxpipe/events.jsonl.
This is what the model sees instead of text:
~48k characters of system prompt + tool docs (this repo's own README, FINDINGS, and source), ≈25k tokens as text, ≈2.7k image tokens as this page. Produced by the real transformRequest pipeline: whitespace-minified, reflowed into full rows with ↵ marking original newlines, OCR instruction banner co-rendered on top. The model reads renders like this at 100/100 on a clean eval (see benchmarks).
Demo
Fable 5 demo (the default, 100/100 reader):
Fable-AB-Demo.mp4
- Both demos with both panes on Fable 5 (plain left, pxpipe right).
- Fable reads what Opus can't. The imaged phrase-count that Opus refuses (see the Opus demo below): the pxpipe arm counts the exact token 10/10 across 39 imaged filler files (matches
grepground truth line-for-line) and gets the multi-step ledger arithmetic right (8037 → … → 15,021). - Same answers, ~7× cheaper. Session totals after both demos: plain $42.21, context 96% full (964.5k/1M — one task away from forced compaction) vs pxpipe $6.06 with context to spare (73.5k/1M).
- Honest caveat, visible in the clip: the pxpipe arm answered the count first and needed one follow-up nudge to also print the ledger balance in the requested one-line format; the plain arm followed the format on the first try. Legibility is solved on Fable — single-reply format compliance is the remaining rough edge.
Opus 4.8 demo (Opus disabled by default):
Opus-AB-Demo.mp4
Side-by-side — plain Claude (left) vs pxpipe (right), both on Opus 4.8 (opt-in; pxpipe is tuned for Fable — see the Fable clip above). Click the image to watch (Google Drive).
- Demo 1 — fix a failing test suite: both pass; the dashboard shows pxpipe cut the request to a fraction of the tokens (real, server-measured context/token reduction).
- Demo 2 — a big file-context (40 files, ~382k tokens) plus a math question and a "count this phrase" task: the math answer (a small text needle) reads on both. The phrase-count needs reading the imaged filler — so pxpipe-on-Opus can't read it and honestly surfaces that it won't fabricate a number (the documented lossy limit: exact values stay text). Plain, meanwhile, bogs down counting file-by-file.
Try it (30 seconds)
npx pxpipe-proxy # proxy on 127.0.0.1:47821 ANTHROPIC_BASE_URL=http://localhost:47821 claude # point Claude Code at it
Open http://127.0.0.1:47821/ for a live dashboard: tokens saved, per-session stats, every text→image conversion side by side, a global kill switch, and runtime model chips including GPT 5.6 and GPT 5.5.
Nothing else changes. Responses stream normally; pxpipe only compresses the request (your context going up), never the model's output. Recent turns stay text; the system prompt, tool docs, and older bulk history are imaged.
The honest part, read before relying on it
It is lossy. pxpipe is a gist tier, not a lossless store. In a needle-in-haystack eval, exact 12-char hex strings inside dense imaged content came back 0/15 on Opus and 13/15 on Fable 5, and the failure mode is silent confabulation: a plausible wrong value, not an error. Anything you need back byte-exact (IDs, hashes, secrets, exact numbers) must stay text. Recent turns do; a dedicated verbatim-risk guard is not built yet.
Exact-recall escape hatch. pxpipe only images Fable requests (PXPIPE_MODELS=claude-fable-5), so any subagent on a non-Fable model passes through as text. Route work that needs byte-exact values to one — globally with CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6, or per-agent with model: sonnet in the agent frontmatter. It reads from source (file/JSONL), not the imaged history. This covers exact-recall you route on purpose; it does not catch a silent misread you did not expect — that is the unbuilt guard above.
Does it break real work? Parity in what we measured: a 10-instance SWE-bench Lite pilot (the easy subset) resolved 10/10 on both arms, pxpipe ON at $27 vs OFF at $54 token-equivalent, and 19 SWE-bench Pro pairs (harder, long-horizon) resolved 14/19 ON vs 15/19 OFF at -60% per-request: verdicts agree on 18/19, and the single split (one ON fail) re-resolved 3/3 when replicated, i.e. run-to-run agentic variance, not compression. Small n, details and caveats below.
Savings are workload-dependent. It wins on token-dense content (~1 char/token: code, JSON, hashes) and loses money on sparse English prose (~3.5 chars/token). The built-in gate only images content where the math wins, calibrated against N=391 production rows.
Model scope: one PXPIPE_MODELS CSV controls which model bases get imaged across both families — default claude-fable-5,gpt-5.6 (GPT 5.5 is opt-in; it degrades on imaged context). Set PXPIPE_MODELS=off to disable imaging entirely, or use ~/.config/pxpipe/config.json with { "models": "off" } (or a list). For GPT, pxpipe keeps tool definitions in native JSON (only verbose schema prose moves into the image) so tool-calling stays reliable; unlike the Claude path, the GPT path does not add or depend on Anthropic cache_control prompt-cache markers. The dashboard chips can flip any model live without changing client configs. Opus 4.7/4.8 was the original Claude scope but misread ~7% of renders (10200→9400), so it was turned off by default once Fable 5 hit 100/100 with identical image billing — opt it back in at your own risk via PXPIPE_MODELS or the dashboard chips. Everything else passes through untouched.
Benchmarks (reproducible)
Measured with novel random-number problems the model cannot have memorized:
| test | N | text | pxpipe (image) | tokens |
|---|---|---|---|---|
novel arithmetic, claude-fable-5 | 100 | 100% | 100% | −38% |
novel arithmetic, claude-opus-4-8 | 100 | 100% | 93% | −38% |
| gist recall A/B (decisions, values, paths, names, negations; with distractors; 15k-45k char sessions), Fable 5 | 98/arm | 98/98 | 98/98 | - |
| state tracking (value mutated 3x, final/first/count), Fable 5 | 18/arm | 18/18 | 18/18 | - |
| confabulation on never-stated facts (lower is better), Fable 5 | 16/arm | 0/16 | 0/16 | - |
| verbatim 12-char hex recall, dense render, Opus | 15 | 15/15 | 0/15 | - |
| verbatim 12-char hex recall, dense render, Fable 5 | 15 | - | 13/15 | - |
SWE-bench Lite pilot (end-to-end task quality)
10 SWE-bench Lite instances, Claude Code + Fable 5, paired runs through pxpipe ON vs OFF, graded with the official swebench Docker harness:
| pxpipe ON | OFF | |
|---|---|---|
| resolved | 10/10 | 10/10 |
| request size vs own uncompressed body | −65% | ±0 |
The −65% is per-request (count_tokens probe of each body before compression), so it has no turn-count confound. n=10/arm, Lite skews easy. Run totals, receipts, caveats: eval/swe-bench/.
SWE-bench Pro bench (harder, long-horizon)
19 completed pairs across two runs (2 dropped: checkout failed both arms), same setup, official SWE-bench_Pro-os Docker harness:
| pxpipe ON | OFF | |
|---|---|---|
| resolved | 14/19 | 15/19 |
| request size vs own uncompressed body | −60% | ±0 |
Verdicts agree on 18/19 (three instances failed both arms, one with byte-identical patches across arms). The single split (navidrome, ON fail) was replicated 3x on the ON arm: all three runs produced an identical patch and resolved, so the original loss was run-to-run agentic variance, not compression. Receipts: eval/swe-bench-pro/.
We also ran GSM8K: 96% imaged. But GSM8K is in training data, so the model recalls memorized answers through its own misreads, inflating the score, so we lead with the clean novel-number eval instead. Reproduce: eval/gsm8k/ · eval/needle-haystack/ · eval/gist-recall/ · full analysis in FINDINGS.md.
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:
- large
tool_resultbodies (file reads, command output, logs) above ~6k chars of token-dense content - older collapsed history: turns behind the live tail get re-rendered as image pages, recent turns always stay text
- 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. Non-Fable models pass through entirely.
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.
How it works
tool_result string ──► wrap at 1928px-wide columns ──► pack ~92,000 chars/page ──► PNG[]
The proxy intercepts /v1/messages, rewrites eligible bulk history into image blocks, splices them back cache-friendly (static prefix preserved, so prompt caching keeps working), and forwards. Per-request events log to ~/.pxpipe/events.jsonl.
The economics: a 1928×1928 image costs ≈4,761 vision tokens and holds up to ≈92,000 chars (≈48,000 text tokens at the observed density), so plain text is cheaper only when it runs denser than ~19 chars/token. Claude Code transcripts are far below that (observed 1.91 chars/token, N=391). The runtime estimator (estimateImageCount) plus a chars/token gate decides per-request; sparse prose is left as text.
Library use (no proxy)
Same engine, no proxy. Render text → PNGs, or run the full cache-safe transform:
import { renderTextToPngs, transformAnthropicMessages } from "pxpipe"; const imgs = await renderTextToPngs(toolResultText); // RenderedImage[] const { body, applied, info } = await transformAnthropicMessages({ body: requestBytes, model: "claude-fable-5", });
options.keepSharp(block) pins blocks as text (override the heuristic for IDs, hashes, paths); options.emitRecoverable returns the originals of imaged blocks so a stateful caller can recover them — the two halves of the fidelity contract for the lossy limitation below. Runtime is pure-JS (Node and edge/Workers); @napi-rs/canvas is build-time only. Full API, types, and constants: src/core/index.ts.
Development
pnpm install && pnpm test # 376 tests pnpm run build # regenerates dist/
Limitations
- Lossy: see "the honest part" above. Verbatim recall from images is unreliable.
- Render latency: encoding PNGs adds time to large requests before they leave (partly offset by the model ingesting fewer tokens). Responses stream normally.
- ASCII/Latin-1 well tested; CJK works but conservatively.
- Runtime is pure-JS — runs on Node and edge/Workers.
@napi-rs/canvasis a build-time-only dev dep (regenerating the glyph atlas), not a runtime dep. - Fable 5 only.
Roadmap
Everything above is measured. Everything here is not. These are hypotheses, not claims; they ship as numbers with an n or they get cut.
- Sharper glyphs. The 13/15 verbatim gap is partly font legibility, not just the model. A per-char confusion matrix across render styles is paused mid-run (
eval/glyph-matrix/); if a zero-cost style lowers read error, the gate compresses harder at the same fidelity. - Effective context. Dense text carries at ~3x fewer tokens as images. If that holds in the live window and not just the bill, 1M tokens holds ~2x the real content. Open question: can a task needing ~2M raw context run inside Fable's 1M once the bulk is imaged?
- Less active text, sharper model. Long contexts degrade reasoning as they fill. Imaging old bulk shrinks what the model actively reads while keeping it reachable. Hypothesis: same information, smaller active context, better long-task accuracy.
One bet: longer effective context and a sharper model on long tasks, from the same Fable 5. Numbers or retraction, no hype between.
License
About
cut Fable 5 token usage by rendering text context as images