@xamukavila/pxpipe 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/cli.js +7 -0
- package/dist/core/applicability.d.ts +31 -0
- package/dist/core/applicability.js +96 -0
- package/dist/core/atlas-gray.d.ts +26 -0
- package/dist/core/atlas-gray.js +64 -0
- package/dist/core/atlas.d.ts +34 -0
- package/dist/core/atlas.js +71 -0
- package/dist/core/baseline.d.ts +80 -0
- package/dist/core/baseline.js +101 -0
- package/dist/core/caveman.d.ts +38 -0
- package/dist/core/caveman.js +183 -0
- package/dist/core/export.d.ts +128 -0
- package/dist/core/export.js +390 -0
- package/dist/core/factsheet.d.ts +78 -0
- package/dist/core/factsheet.js +216 -0
- package/dist/core/gpt-model-profiles.d.ts +60 -0
- package/dist/core/gpt-model-profiles.js +161 -0
- package/dist/core/history.d.ts +141 -0
- package/dist/core/history.js +553 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/library.d.ts +74 -0
- package/dist/core/library.js +133 -0
- package/dist/core/measurement.d.ts +22 -0
- package/dist/core/measurement.js +213 -0
- package/dist/core/openai-history.d.ts +124 -0
- package/dist/core/openai-history.js +494 -0
- package/dist/core/openai-savings.d.ts +44 -0
- package/dist/core/openai-savings.js +75 -0
- package/dist/core/openai.d.ts +24 -0
- package/dist/core/openai.js +839 -0
- package/dist/core/png.d.ts +11 -0
- package/dist/core/png.js +132 -0
- package/dist/core/proxy.d.ts +81 -0
- package/dist/core/proxy.js +730 -0
- package/dist/core/render.d.ts +188 -0
- package/dist/core/render.js +785 -0
- package/dist/core/schema-strip.d.ts +29 -0
- package/dist/core/schema-strip.js +160 -0
- package/dist/core/tracker.d.ts +154 -0
- package/dist/core/tracker.js +216 -0
- package/dist/core/transform.d.ts +362 -0
- package/dist/core/transform.js +1828 -0
- package/dist/core/types.d.ts +77 -0
- package/dist/core/types.js +8 -0
- package/dist/dashboard/fragments.d.ts +36 -0
- package/dist/dashboard/fragments.js +938 -0
- package/dist/dashboard/types.d.ts +154 -0
- package/dist/dashboard/types.js +3 -0
- package/dist/dashboard/vendor.d.ts +3 -0
- package/dist/dashboard/vendor.js +6 -0
- package/dist/dashboard.d.ts +245 -0
- package/dist/dashboard.js +1140 -0
- package/dist/export-collect.d.ts +36 -0
- package/dist/export-collect.js +59 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.js +9038 -0
- package/dist/sessions.d.ts +172 -0
- package/dist/sessions.js +510 -0
- package/dist/stats.d.ts +74 -0
- package/dist/stats.js +248 -0
- package/dist/worker.d.ts +53 -0
- package/dist/worker.js +102 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 claude-image-proxy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# pxpipe
|
|
2
|
+
|
|
3
|
+
**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.**
|
|
4
|
+
|
|
5
|
+
An image's token cost is fixed by its pixel dimensions, not by how much text
|
|
6
|
+
is inside it. Dense content (code, JSON, tool output) packs ~3.1 chars per
|
|
7
|
+
image-token vs ~1 char per text-token on real Claude Code traffic. The
|
|
8
|
+
reader is the same vision channel that Anthropic's computer use already
|
|
9
|
+
relies on for screenshots. pxpipe is a local proxy that uses that channel
|
|
10
|
+
for context: it rewrites the bulky parts of each request into compact PNGs
|
|
11
|
+
before it leaves your machine. At current Fable
|
|
12
|
+
list prices that lands as a **~59–70% lower end-to-end bill** — but prices
|
|
13
|
+
move and workloads differ, so the durable number is the token cut itself,
|
|
14
|
+
measured per-request against a free `count_tokens` counterfactual in
|
|
15
|
+
`~/.pxpipe/events.jsonl`.
|
|
16
|
+
|
|
17
|
+
This is what the model sees instead of text:
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
*~48k chars of system prompt + tool docs: ≈25k tokens as text, ≈2.7k image
|
|
22
|
+
tokens as this page. Real pipeline output; the model reads renders like this
|
|
23
|
+
at 100/100 (see benchmarks).*
|
|
24
|
+
|
|
25
|
+
## Demo
|
|
26
|
+
|
|
27
|
+
**Fable 5 (the default, 100/100 reader) — plain left, pxpipe right:**
|
|
28
|
+
|
|
29
|
+
https://github.com/user-attachments/assets/1c8ee63a-fcd7-4958-917b-da788d718349
|
|
30
|
+
|
|
31
|
+
pxpipe counts an exact token **10/10** across 39 imaged filler files
|
|
32
|
+
(matches `grep` line-for-line), gets the multi-step ledger arithmetic right,
|
|
33
|
+
and ends the session at **$6.06** with context to spare (73.5k/1M) vs
|
|
34
|
+
**$42.21** at 96% full. One caveat visible in the clip: the pxpipe arm
|
|
35
|
+
needed a nudge to match the requested one-line output format.
|
|
36
|
+
|
|
37
|
+
**Opus 4.8 (disabled by default) — same layout:**
|
|
38
|
+
|
|
39
|
+
https://github.com/user-attachments/assets/f4e50137-31b5-426f-a6ed-b83f829b4a2c
|
|
40
|
+
|
|
41
|
+
Text needles read fine on both arms; the imaged phrase-count doesn't read on
|
|
42
|
+
Opus — and pxpipe **says so instead of fabricating a number**. That misread
|
|
43
|
+
rate is why Opus is opt-in.
|
|
44
|
+
|
|
45
|
+
## Try it (30 seconds)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx pxpipe-proxy # proxy on 127.0.0.1:47821
|
|
49
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:47821 claude # point Claude Code at it
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Dashboard at <http://127.0.0.1:47821/>: tokens saved, every text→image
|
|
53
|
+
conversion side by side, kill switch, live model chips. Responses stream
|
|
54
|
+
normally — pxpipe compresses the *request* only, never the model's output.
|
|
55
|
+
Recent turns stay text; the system prompt, tool docs, and older bulk history
|
|
56
|
+
are imaged.
|
|
57
|
+
|
|
58
|
+
## The honest part
|
|
59
|
+
|
|
60
|
+
- **It is lossy.** Exact 12-char hex strings in dense imaged content:
|
|
61
|
+
**13/15** on Fable 5, **0/15** on Opus — and misses are *silent
|
|
62
|
+
confabulations*, not errors. Byte-exact values (IDs, hashes, secrets)
|
|
63
|
+
must stay text; recent turns do. A dedicated verbatim-risk guard is not
|
|
64
|
+
built yet.
|
|
65
|
+
- **Escape hatch:** subagents on non-allowlisted models pass through as
|
|
66
|
+
text — route byte-exact work there
|
|
67
|
+
(`CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6`, or `model: sonnet` in
|
|
68
|
+
agent frontmatter).
|
|
69
|
+
- **Real work:** SWE-bench Lite pilot **10/10 both arms** at −65% request
|
|
70
|
+
size; SWE-bench Pro **14/19 ON vs 15/19 OFF** at −60%, verdicts agree
|
|
71
|
+
18/19, and the single split re-resolved 3/3 on replication — run-to-run
|
|
72
|
+
variance, not compression. Small n; receipts in `eval/`.
|
|
73
|
+
- **Workload-dependent.** Wins on token-dense content (~1 char/token),
|
|
74
|
+
loses money on sparse prose (~3.5 chars/token); a profitability gate
|
|
75
|
+
(calibrated on N=391 production rows) images only where the math wins.
|
|
76
|
+
- **Model scope:** default `PXPIPE_MODELS=claude-fable-5,gpt-5.6`. Opus
|
|
77
|
+
4.7/4.8 misread ~7% of renders and GPT 5.5 degrades on imaged context, so
|
|
78
|
+
both are opt-in via `PXPIPE_MODELS` or the dashboard chips.
|
|
79
|
+
`PXPIPE_MODELS=off` disables imaging. Everything else passes through
|
|
80
|
+
byte-identical. On the GPT path, tool definitions stay native JSON and no
|
|
81
|
+
Anthropic `cache_control` markers are used.
|
|
82
|
+
|
|
83
|
+
## Benchmarks (reproducible)
|
|
84
|
+
|
|
85
|
+
Measured with novel random-number problems the model cannot have memorized:
|
|
86
|
+
|
|
87
|
+
| test | N | text | pxpipe (image) | tokens |
|
|
88
|
+
|---|---:|---:|---:|---|
|
|
89
|
+
| novel arithmetic, `claude-fable-5` | 100 | 100% | **100%** | **−38%** |
|
|
90
|
+
| novel arithmetic, `claude-opus-4-8` | 100 | 100% | 93% | −38% |
|
|
91
|
+
| gist recall A/B (decisions, values, paths, names, negations; with distractors; 15k-45k char sessions), Fable 5 | 98/arm | 98/98 | **98/98** | - |
|
|
92
|
+
| state tracking (value mutated 3x, final/first/count), Fable 5 | 18/arm | 18/18 | **18/18** | - |
|
|
93
|
+
| confabulation on never-stated facts (lower is better), Fable 5 | 16/arm | 0/16 | **0/16** | - |
|
|
94
|
+
| verbatim 12-char hex recall, dense render, Opus | 15 | 15/15 | **0/15** | - |
|
|
95
|
+
| verbatim 12-char hex recall, dense render, Fable 5 | 15 | - | **13/15** | - |
|
|
96
|
+
|
|
97
|
+
SWE-bench run totals, receipts, and caveats:
|
|
98
|
+
[`eval/swe-bench/`](eval/swe-bench/) ·
|
|
99
|
+
[`eval/swe-bench-pro/`](eval/swe-bench-pro/) ·
|
|
100
|
+
[`eval/needle-haystack/`](eval/needle-haystack/) ·
|
|
101
|
+
[`eval/gist-recall/`](eval/gist-recall/) · analysis in
|
|
102
|
+
[`FINDINGS.md`](FINDINGS.md). (GSM8K scored 96% imaged, but it's in training
|
|
103
|
+
data — memorized answers survive misreads — so we lead with the novel-number
|
|
104
|
+
evals.)
|
|
105
|
+
|
|
106
|
+
## Caveman prose compression (experimental, opt-in)
|
|
107
|
+
|
|
108
|
+
`caveman` is a second, optional compression layer that runs *before* prose is
|
|
109
|
+
rendered to an image: it deterministically drops low-information tokens (EN/PT
|
|
110
|
+
articles and fillers — "the", "a", "just", "really", …). Fewer chars → fewer
|
|
111
|
+
pixels → a smaller image cache write. It only touches blocks classified as
|
|
112
|
+
prose (`classifyContent === 'other'`); code, JSON, logs, and diffs are never
|
|
113
|
+
compressed, so it composes with the profitability gate rather than replacing
|
|
114
|
+
it.
|
|
115
|
+
|
|
116
|
+
It is **off by default and stays off** until a gist/verbatim recall A/B shows
|
|
117
|
+
the model still reads compressed prose as faithfully as full prose. This is a
|
|
118
|
+
*semantic* lossy layer stacked on the *optical* lossiness of imaging — the cost
|
|
119
|
+
bench below shows it is cheaper and cache-stable, **not** that recall holds.
|
|
120
|
+
|
|
121
|
+
### Configure
|
|
122
|
+
|
|
123
|
+
| Surface | Flag | Values |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Node proxy | `PXPIPE_CAVEMAN` | `1` / `true` / `yes` / `on` (case-insensitive); unset = off |
|
|
126
|
+
| Cloudflare Worker | `CAVEMAN` | truthy string; unset = off |
|
|
127
|
+
| Library | `transformAnthropicMessages({ …, caveman: true })` | boolean, default `false` |
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
PXPIPE_CAVEMAN=1 npx pxpipe-proxy # prose compression ON (experiment)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
⚠️ Flipping the flag changes the rendered image bytes, which **busts warm image
|
|
134
|
+
caches**. In an A/B run hold it fixed per arm — never toggle mid-session.
|
|
135
|
+
|
|
136
|
+
### One reproducible A/B run
|
|
137
|
+
|
|
138
|
+
Single 4-turn headless session per arm (`bench/run.sh`) on
|
|
139
|
+
`claude-opus-4-6[1m]`, scored by `bench/score.mjs`
|
|
140
|
+
(`cost_units = input + 1.25·cache_create + 0.1·cache_read`):
|
|
141
|
+
|
|
142
|
+
| metric | OFF | ON (caveman) | Δ |
|
|
143
|
+
|---|---:|---:|---:|
|
|
144
|
+
| cost_units | 192,921 | 160,834 | **−16.6%** |
|
|
145
|
+
| cache_create tokens | 122,797 | 94,705 | **−22.9%** |
|
|
146
|
+
| cache_read tokens | 388,741 | 419,022 | +7.8% |
|
|
147
|
+
| output tokens | 1,768 | 1,870 | +5.8% |
|
|
148
|
+
| prefix_flips | 0 | 0 | 0 |
|
|
149
|
+
| cold_restarts | 1 | 0 | — |
|
|
150
|
+
| p50 latency | 7,107 ms | 3,395 ms | −52% |
|
|
151
|
+
|
|
152
|
+
Read it honestly — **N=1 per arm, one session**, so this is a smoke bench, not
|
|
153
|
+
a powered eval:
|
|
154
|
+
|
|
155
|
+
- The mechanistic win is **cache_create −22.9%**: compressed prose is fewer
|
|
156
|
+
chars → fewer pixels → smaller image cache writes (cache_create carries the
|
|
157
|
+
heaviest 1.25× weight, so it drives most of the cost delta).
|
|
158
|
+
- The headline **cost_units −16.6%** is partly inflated by one cold cache miss
|
|
159
|
+
on the OFF arm (44,211 cache_create tokens written with no read benefit) —
|
|
160
|
+
plausibly cache-timing noise, not something caveman caused. Don't over-read
|
|
161
|
+
it.
|
|
162
|
+
- The robust, noise-free results: **prefix_flips = 0 on both arms** (caveman is
|
|
163
|
+
deterministic — it does not bust the cache prefix) and **output tokens flat**
|
|
164
|
+
(the model answered normally on compressed prose on this task).
|
|
165
|
+
- Latency tracks image size but is also sensitive to the cold restart.
|
|
166
|
+
|
|
167
|
+
Reproduce:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
bash bench/run.sh --model 'claude-opus-4-6[1m]' --label off
|
|
171
|
+
PXPIPE_CAVEMAN=1 bash bench/run.sh --model 'claude-opus-4-6[1m]' --label caveman
|
|
172
|
+
node bench/score.mjs bench/runs/<off-dir> bench/runs/<caveman-dir>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## How it works
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
tool_result string ──► wrap at 1928px-wide columns ──► pack ~92,000 chars/page ──► PNG[]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The proxy intercepts `/v1/messages`, rewrites eligible bulk into image
|
|
182
|
+
blocks, splices them back cache-friendly (static prefix preserved, prompt
|
|
183
|
+
caching keeps working), and forwards. A 1928×1928 image costs ≈4,761 vision
|
|
184
|
+
tokens and holds ≈92,000 chars, so text wins only above ~19 chars/token —
|
|
185
|
+
Claude Code traffic runs ~1.91 (N=391). A per-request estimator decides;
|
|
186
|
+
sparse prose stays text. Events log to `~/.pxpipe/events.jsonl`.
|
|
187
|
+
|
|
188
|
+
## Library use (no proxy)
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { renderTextToImages, transformAnthropicMessages } from "pxpipe-proxy";
|
|
192
|
+
|
|
193
|
+
const { pages } = await renderTextToImages(toolResultText); // pages[i].png: Uint8Array
|
|
194
|
+
const { body, applied, info } = await transformAnthropicMessages({
|
|
195
|
+
body: requestBytes,
|
|
196
|
+
model: "claude-fable-5",
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`options.keepSharp(block)` pins blocks as text; `options.emitRecoverable`
|
|
201
|
+
returns the originals of imaged blocks. Pure-JS runtime (Node and
|
|
202
|
+
edge/Workers); `@napi-rs/canvas` is build-time only. Full API:
|
|
203
|
+
`src/core/index.ts`.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pnpm install && pnpm test
|
|
209
|
+
pnpm run build # regenerates dist/
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## FAQ
|
|
213
|
+
|
|
214
|
+
**Is the headline end-to-end, or only on the requests you touched?**
|
|
215
|
+
End-to-end, the whole bill. Most compression tools report savings only on
|
|
216
|
+
the input slice they touched, which flatters the number. The end-to-end
|
|
217
|
+
denominator is *every* production request: the small ones pxpipe correctly
|
|
218
|
+
left untouched, all cache writes and reads, and all output tokens (which the
|
|
219
|
+
proxy never compresses). On a 13,709-request snapshot that was 59% ($100 →
|
|
220
|
+
~$41); a later 8,904-compressed-request trace measured ~70%. Compressed-only
|
|
221
|
+
runs higher (~72–74%) and is quoted separately, never as the headline. The
|
|
222
|
+
exact figure is workload-dependent — reproduce it on your own log.
|
|
223
|
+
|
|
224
|
+
**How is the math measured?**
|
|
225
|
+
Both sides of the same request, at the same moment. For every `/v1/messages`
|
|
226
|
+
POST the proxy fires a free `count_tokens` probe on the original uncompressed
|
|
227
|
+
body (the counterfactual) in parallel with the real forward, and reads
|
|
228
|
+
Anthropic's actually-billed usage block off the response. Both land in the
|
|
229
|
+
same row of `~/.pxpipe/events.jsonl`, so there is no turn-count or
|
|
230
|
+
run-to-run confound. Dollar conversion uses Fable 5 list ratios: input ×1.0,
|
|
231
|
+
cache write ×1.25, cache read ×0.1, output ×5. Cache pricing is applied
|
|
232
|
+
identically to both sides, so the caching discount cancels and cannot be
|
|
233
|
+
double-counted as "savings". Re-derive it yourself from the events log: the
|
|
234
|
+
formula and field names are documented in `src/core/baseline.ts`.
|
|
235
|
+
|
|
236
|
+
**What does it actually compress?**
|
|
237
|
+
Three kinds of *input* blocks, each behind a profitability gate:
|
|
238
|
+
|
|
239
|
+
1. large `tool_result` bodies (file reads, command output, logs) above
|
|
240
|
+
~6k chars of token-dense content
|
|
241
|
+
2. older collapsed history: turns behind the live tail get re-rendered as
|
|
242
|
+
image pages, recent turns always stay text
|
|
243
|
+
3. the static system prompt + tool docs slab
|
|
244
|
+
|
|
245
|
+
Everything else passes through byte-identical: your messages, recent turns,
|
|
246
|
+
the model's output (it is the response, the proxy never touches it), sparse
|
|
247
|
+
prose, and anything too small to win. Models outside the allowlist pass
|
|
248
|
+
through entirely — the default scope is Fable 5 and GPT 5.6 only. Opus 4.8
|
|
249
|
+
and GPT 5.5 read imaged content measurably worse (FINDINGS.md 2026-06-16),
|
|
250
|
+
so they are deliberately opt-in via the dashboard or `PXPIPE_MODELS`, never
|
|
251
|
+
silently imaged.
|
|
252
|
+
|
|
253
|
+
**Has it ever failed for real, outside the benchmarks?**
|
|
254
|
+
Yes, once in weeks of daily use: the model recalled a person's name from
|
|
255
|
+
imaged chat history and got it confidently wrong. No error, just a
|
|
256
|
+
plausible wrong name. That is the documented failure mode: exact strings
|
|
257
|
+
in imaged content are not byte-safe. Coding sessions tolerate this because
|
|
258
|
+
the agent re-reads files before editing; pure chat recall has no such check.
|
|
259
|
+
This failure mode is measured, not anecdotal:
|
|
260
|
+
[the legibility audit](docs/LEGIBILITY-AUDIT-2026-07-01.md) quantifies
|
|
261
|
+
exact-string recall off rendered pages (blind reads top out at 63% on dense
|
|
262
|
+
identifiers, with every miss predicted by a glyph-confusability matrix) and
|
|
263
|
+
documents the shipped mitigations — page geometry clamped to the API's
|
|
264
|
+
resample cap so billed pixels actually reach the vision encoder, and exact
|
|
265
|
+
identifiers (SHAs, numbers) riding alongside as text.
|
|
266
|
+
|
|
267
|
+
**Why are misses silent confabulations instead of read errors?**
|
|
268
|
+
Because model vision is not OCR: the image becomes patch embeddings, never
|
|
269
|
+
discrete characters, so there is no per-glyph confidence to fail loudly
|
|
270
|
+
on. When pixels underdetermine a glyph, the language prior fills the gap
|
|
271
|
+
with something plausible. Mechanism and receipts:
|
|
272
|
+
[docs/NOT-OCR.md](docs/NOT-OCR.md).
|
|
273
|
+
|
|
274
|
+
**Didn't DeepSeek-OCR show this doesn't hold up in practice?**
|
|
275
|
+
No: it proved the channel works, using an encoder/decoder pair trained for
|
|
276
|
+
the job. The skepticism dates from October 2025, when no stock production
|
|
277
|
+
model could read dense renders; that changed with Fable 5 (0/15 verbatim
|
|
278
|
+
hex on Opus 4.8 vs 13/15 on Fable 5, same pages). Timeline and per-model
|
|
279
|
+
numbers: [docs/NOT-OCR.md](docs/NOT-OCR.md).
|
|
280
|
+
|
|
281
|
+
**Why does the README read like an AI wrote it?**
|
|
282
|
+
Because one did. Most of this repo's commits — the code and the docs — were
|
|
283
|
+
authored by Opus/Fable agent sessions running behind pxpipe itself, reading
|
|
284
|
+
their own collapsed history as image pages while they worked.
|
|
285
|
+
|
|
286
|
+
## Limitations
|
|
287
|
+
|
|
288
|
+
- Lossy (above); verbatim recall from images is unreliable.
|
|
289
|
+
- PNG encoding adds latency to large requests before they leave.
|
|
290
|
+
- ASCII/Latin-1 well tested; CJK works but conservatively.
|
|
291
|
+
|
|
292
|
+
## Roadmap
|
|
293
|
+
|
|
294
|
+
Rendering research is parked as of 2026-07-05: verbatim misreads are
|
|
295
|
+
capacity-bound, not trick-bound, so no font/color/layout change fixes
|
|
296
|
+
exact-string recall at profitable density. The why is in
|
|
297
|
+
[docs/NOT-OCR.md](docs/NOT-OCR.md); the dated analysis and the three
|
|
298
|
+
documented follow-up threads (glyph-style A/B with banked pages, runtime
|
|
299
|
+
canary + re-fetch, surrogate-reader pre-flight) are in
|
|
300
|
+
[FINDINGS.md](FINDINGS.md), 2026-07-05 entry. Watch condition: re-run the
|
|
301
|
+
resolution sweep per model release; readable density moved ~4x in glyph
|
|
302
|
+
area from Opus 4.8 to Fable 5, and a model that reads production cells
|
|
303
|
+
near 100% means savings rise for free.
|
|
304
|
+
|
|
305
|
+
Still open, unchanged: whether imaged bulk stretches effective context (~2x
|
|
306
|
+
the real content in the same 1M window), and whether a smaller active
|
|
307
|
+
context improves long-task accuracy. Hypotheses, not claims — they ship as
|
|
308
|
+
numbers with an n or they get cut.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tiny shim: just runs the bundled Node entry. Real CLI logic lives in src/node.ts.
|
|
3
|
+
import('../dist/node.js').catch((err) => {
|
|
4
|
+
console.error('[pxpipe] failed to start:', err);
|
|
5
|
+
console.error('[pxpipe] did you forget to `npm run build`?');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Applicability helpers for pxpipe's production-safe model scope. */
|
|
2
|
+
export type PxpipeApplicabilityReason = 'eligible' | 'unsupported_model' | 'unsupported_method' | 'unsupported_path' | 'empty_body';
|
|
3
|
+
export interface PxpipeApplicabilityInput {
|
|
4
|
+
readonly model?: string | null;
|
|
5
|
+
readonly method?: string | null;
|
|
6
|
+
readonly path?: string | null;
|
|
7
|
+
readonly bodyBytes?: number | null;
|
|
8
|
+
}
|
|
9
|
+
/** Current effective allowed-model scope (Claude + GPT). */
|
|
10
|
+
export declare function getAllowedModelBases(): string[];
|
|
11
|
+
/** PXPIPE_MODELS env / default scope, independent of runtime override.
|
|
12
|
+
* Dashboard unions this into its chip set so env-enabled models are always shown as toggles. */
|
|
13
|
+
export declare function getConfiguredModelBases(): string[];
|
|
14
|
+
/** Set the dashboard runtime override. Empty array = compress nothing; null = clear override. Not persisted. */
|
|
15
|
+
export declare function setAllowedModelBases(list: readonly string[] | null): void;
|
|
16
|
+
/** True when pxpipe may transform this Anthropic model. */
|
|
17
|
+
export declare function isPxpipeSupportedModel(model: string | null | undefined): boolean;
|
|
18
|
+
/** True when pxpipe may transform this GPT model. Shares the single PXPIPE_MODELS scope. */
|
|
19
|
+
export declare function isPxpipeSupportedGptModel(model: string | null | undefined): boolean;
|
|
20
|
+
/** Canonical set of Anthropic Messages routes pxpipe transforms. Shared with
|
|
21
|
+
* createProxy (src/core/proxy.ts) so the public applicability helper and the
|
|
22
|
+
* proxy router can never disagree on which paths are eligible — they did: the
|
|
23
|
+
* proxy accepts /anthropic/messages, but the helper's old `endsWith` check
|
|
24
|
+
* rejected it (and would have wrongly accepted /foo/v1/messages). Exact matches
|
|
25
|
+
* only, so /v1/messages/count_tokens stays unsupported. */
|
|
26
|
+
export declare function isAnthropicMessagesPath(pathname: string): boolean;
|
|
27
|
+
export declare function shouldTransformAnthropicMessages(input: PxpipeApplicabilityInput): {
|
|
28
|
+
eligible: boolean;
|
|
29
|
+
reason: PxpipeApplicabilityReason;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=applicability.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Applicability helpers for pxpipe's production-safe model scope. */
|
|
2
|
+
/** Bracketed variant tags (e.g. `[1m]`) stripped before model matching so base and variant gate identically. */
|
|
3
|
+
const VARIANT_TAG = /\[[^\]]*\]/g;
|
|
4
|
+
function baseModelId(model) {
|
|
5
|
+
return model.replace(VARIANT_TAG, '');
|
|
6
|
+
}
|
|
7
|
+
/** Dashboard runtime override; null = fall back to PXPIPE_MODELS env / built-in default. In-memory only. */
|
|
8
|
+
let runtimeModelBases = null;
|
|
9
|
+
/** Built-in default scope when PXPIPE_MODELS is unset: Fable 5 (Claude) plus
|
|
10
|
+
* GPT 5.6. GPT 5.5 and Opus 4.8 are intentionally off — same pipeline but
|
|
11
|
+
* measurably worse at reading imaged content (FINDINGS.md 2026-06-16: Opus 4.8
|
|
12
|
+
* ~2pp arithmetic, 6/15 dense-hex recall vs Fable's 100/100; GPT 5.5 likewise
|
|
13
|
+
* degrades on imaged history/context) — so silently imaging them is the wrong
|
|
14
|
+
* default. Both stay opt-in via the dashboard chips or PXPIPE_MODELS. */
|
|
15
|
+
const DEFAULT_MODEL_BASES = ['claude-fable-5', 'gpt-5.6'];
|
|
16
|
+
function falsey(v) {
|
|
17
|
+
return /^(0|false|no|off|none)$/i.test(v.trim());
|
|
18
|
+
}
|
|
19
|
+
/** PXPIPE_MODELS env / built-in default, ignoring the runtime override. One CSV
|
|
20
|
+
* controls every family (Claude + GPT). Resolution (read per-call so scope flips LIVE):
|
|
21
|
+
* - unset or empty → built-in default (Fable 5 + GPT 5.6)
|
|
22
|
+
* - `off`/`0`/`false`/... → compress nothing
|
|
23
|
+
* - CSV of model bases → exactly those families (e.g. `claude-fable-5,gpt-5.6`) */
|
|
24
|
+
function envOrDefaultBases() {
|
|
25
|
+
// Edge-safe: `process` is undefined off-Node; `typeof` avoids a ReferenceError.
|
|
26
|
+
const raw = typeof process !== 'undefined' ? process.env?.PXPIPE_MODELS : undefined;
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return [...DEFAULT_MODEL_BASES];
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return [...DEFAULT_MODEL_BASES];
|
|
32
|
+
if (falsey(trimmed))
|
|
33
|
+
return [];
|
|
34
|
+
return trimmed.split(',').map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
function allowedModelBases() {
|
|
37
|
+
if (runtimeModelBases !== null)
|
|
38
|
+
return [...runtimeModelBases];
|
|
39
|
+
return envOrDefaultBases();
|
|
40
|
+
}
|
|
41
|
+
/** Current effective allowed-model scope (Claude + GPT). */
|
|
42
|
+
export function getAllowedModelBases() {
|
|
43
|
+
return allowedModelBases();
|
|
44
|
+
}
|
|
45
|
+
/** PXPIPE_MODELS env / default scope, independent of runtime override.
|
|
46
|
+
* Dashboard unions this into its chip set so env-enabled models are always shown as toggles. */
|
|
47
|
+
export function getConfiguredModelBases() {
|
|
48
|
+
return envOrDefaultBases();
|
|
49
|
+
}
|
|
50
|
+
/** Set the dashboard runtime override. Empty array = compress nothing; null = clear override. Not persisted. */
|
|
51
|
+
export function setAllowedModelBases(list) {
|
|
52
|
+
runtimeModelBases = list === null ? null : list.map((s) => s.trim()).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
/** Membership test against the single allowed scope. Matches exact base or `-suffix`
|
|
55
|
+
* alias; [variant] tags stripped first. */
|
|
56
|
+
function isAllowed(model) {
|
|
57
|
+
if (typeof model !== 'string')
|
|
58
|
+
return false;
|
|
59
|
+
const base = baseModelId(model);
|
|
60
|
+
return allowedModelBases().some((b) => base === b || base.startsWith(`${b}-`));
|
|
61
|
+
}
|
|
62
|
+
/** True when pxpipe may transform this Anthropic model. */
|
|
63
|
+
export function isPxpipeSupportedModel(model) {
|
|
64
|
+
return isAllowed(model);
|
|
65
|
+
}
|
|
66
|
+
/** True when pxpipe may transform this GPT model. Shares the single PXPIPE_MODELS scope. */
|
|
67
|
+
export function isPxpipeSupportedGptModel(model) {
|
|
68
|
+
return isAllowed(model);
|
|
69
|
+
}
|
|
70
|
+
/** Canonical set of Anthropic Messages routes pxpipe transforms. Shared with
|
|
71
|
+
* createProxy (src/core/proxy.ts) so the public applicability helper and the
|
|
72
|
+
* proxy router can never disagree on which paths are eligible — they did: the
|
|
73
|
+
* proxy accepts /anthropic/messages, but the helper's old `endsWith` check
|
|
74
|
+
* rejected it (and would have wrongly accepted /foo/v1/messages). Exact matches
|
|
75
|
+
* only, so /v1/messages/count_tokens stays unsupported. */
|
|
76
|
+
export function isAnthropicMessagesPath(pathname) {
|
|
77
|
+
return pathname === '/v1/messages'
|
|
78
|
+
|| pathname === '/anthropic/v1/messages'
|
|
79
|
+
|| pathname === '/anthropic/messages';
|
|
80
|
+
}
|
|
81
|
+
export function shouldTransformAnthropicMessages(input) {
|
|
82
|
+
if (input.method !== undefined && input.method !== null && input.method.toUpperCase() !== 'POST') {
|
|
83
|
+
return { eligible: false, reason: 'unsupported_method' };
|
|
84
|
+
}
|
|
85
|
+
if (input.path !== undefined && input.path !== null && !isAnthropicMessagesPath(input.path)) {
|
|
86
|
+
return { eligible: false, reason: 'unsupported_path' };
|
|
87
|
+
}
|
|
88
|
+
if (input.bodyBytes !== undefined && input.bodyBytes !== null && input.bodyBytes <= 0) {
|
|
89
|
+
return { eligible: false, reason: 'empty_body' };
|
|
90
|
+
}
|
|
91
|
+
if (!isPxpipeSupportedModel(input.model)) {
|
|
92
|
+
return { eligible: false, reason: 'unsupported_model' };
|
|
93
|
+
}
|
|
94
|
+
return { eligible: true, reason: 'eligible' };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=applicability.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Latin advance width in pixels. CJK glyphs advance 10px (= 2 × this). */
|
|
2
|
+
export declare const ATLAS_GRAY_CELL_W = 5;
|
|
3
|
+
/** Cell height in pixels. */
|
|
4
|
+
export declare const ATLAS_GRAY_CELL_H = 8;
|
|
5
|
+
/** Distance from cell top to baseline. */
|
|
6
|
+
export declare const ATLAS_GRAY_ASCENT = 7;
|
|
7
|
+
/** Distance from baseline to cell bottom. */
|
|
8
|
+
export declare const ATLAS_GRAY_DESCENT = 1;
|
|
9
|
+
/** Sorted codepoint table. `ATLAS_GRAY_CODEPOINTS[rank]` is the codepoint at `rank`. */
|
|
10
|
+
export declare const ATLAS_GRAY_CODEPOINTS: Uint32Array;
|
|
11
|
+
/** BYTE offset into ATLAS_GRAY_PIXELS for the glyph at each rank.
|
|
12
|
+
* (Unlike the 1-bit atlas, this is a byte offset — 1 byte per pixel.) */
|
|
13
|
+
export declare const ATLAS_GRAY_OFFSETS: Uint32Array;
|
|
14
|
+
/** 1 if the glyph at this rank is double-wide (East Asian Wide), 0 otherwise. */
|
|
15
|
+
export declare const ATLAS_GRAY_WIDE_FLAGS: Uint8Array;
|
|
16
|
+
/** Coverage bytes (0-255), one byte per pixel. Runtime extraction:
|
|
17
|
+
* byteIdx = ATLAS_GRAY_OFFSETS[rank] + gy * srcW + gx
|
|
18
|
+
* coverage = ATLAS_GRAY_PIXELS[byteIdx]
|
|
19
|
+
* where srcW is ATLAS_GRAY_CELL_W (narrow) or 2*ATLAS_GRAY_CELL_W (wide). */
|
|
20
|
+
export declare const ATLAS_GRAY_PIXELS: Uint8Array;
|
|
21
|
+
/** Number of glyphs in the gray atlas. */
|
|
22
|
+
export declare const ATLAS_GRAY_NUM_GLYPHS: number;
|
|
23
|
+
/** Binary-search the sparse codepoint table. Returns rank (≥0) or -1 if absent.
|
|
24
|
+
* Mirrors atlasRank() from atlas.ts but operates on ATLAS_GRAY_CODEPOINTS. */
|
|
25
|
+
export declare function atlasGrayRank(codepoint: number): number;
|
|
26
|
+
//# sourceMappingURL=atlas-gray.d.ts.map
|