@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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/cli.js +7 -0
  4. package/dist/core/applicability.d.ts +31 -0
  5. package/dist/core/applicability.js +96 -0
  6. package/dist/core/atlas-gray.d.ts +26 -0
  7. package/dist/core/atlas-gray.js +64 -0
  8. package/dist/core/atlas.d.ts +34 -0
  9. package/dist/core/atlas.js +71 -0
  10. package/dist/core/baseline.d.ts +80 -0
  11. package/dist/core/baseline.js +101 -0
  12. package/dist/core/caveman.d.ts +38 -0
  13. package/dist/core/caveman.js +183 -0
  14. package/dist/core/export.d.ts +128 -0
  15. package/dist/core/export.js +390 -0
  16. package/dist/core/factsheet.d.ts +78 -0
  17. package/dist/core/factsheet.js +216 -0
  18. package/dist/core/gpt-model-profiles.d.ts +60 -0
  19. package/dist/core/gpt-model-profiles.js +161 -0
  20. package/dist/core/history.d.ts +141 -0
  21. package/dist/core/history.js +553 -0
  22. package/dist/core/index.d.ts +8 -0
  23. package/dist/core/index.js +8 -0
  24. package/dist/core/library.d.ts +74 -0
  25. package/dist/core/library.js +133 -0
  26. package/dist/core/measurement.d.ts +22 -0
  27. package/dist/core/measurement.js +213 -0
  28. package/dist/core/openai-history.d.ts +124 -0
  29. package/dist/core/openai-history.js +494 -0
  30. package/dist/core/openai-savings.d.ts +44 -0
  31. package/dist/core/openai-savings.js +75 -0
  32. package/dist/core/openai.d.ts +24 -0
  33. package/dist/core/openai.js +839 -0
  34. package/dist/core/png.d.ts +11 -0
  35. package/dist/core/png.js +132 -0
  36. package/dist/core/proxy.d.ts +81 -0
  37. package/dist/core/proxy.js +730 -0
  38. package/dist/core/render.d.ts +188 -0
  39. package/dist/core/render.js +785 -0
  40. package/dist/core/schema-strip.d.ts +29 -0
  41. package/dist/core/schema-strip.js +160 -0
  42. package/dist/core/tracker.d.ts +154 -0
  43. package/dist/core/tracker.js +216 -0
  44. package/dist/core/transform.d.ts +362 -0
  45. package/dist/core/transform.js +1828 -0
  46. package/dist/core/types.d.ts +77 -0
  47. package/dist/core/types.js +8 -0
  48. package/dist/dashboard/fragments.d.ts +36 -0
  49. package/dist/dashboard/fragments.js +938 -0
  50. package/dist/dashboard/types.d.ts +154 -0
  51. package/dist/dashboard/types.js +3 -0
  52. package/dist/dashboard/vendor.d.ts +3 -0
  53. package/dist/dashboard/vendor.js +6 -0
  54. package/dist/dashboard.d.ts +245 -0
  55. package/dist/dashboard.js +1140 -0
  56. package/dist/export-collect.d.ts +36 -0
  57. package/dist/export-collect.js +59 -0
  58. package/dist/node.d.ts +9 -0
  59. package/dist/node.js +9038 -0
  60. package/dist/sessions.d.ts +172 -0
  61. package/dist/sessions.js +510 -0
  62. package/dist/stats.d.ts +74 -0
  63. package/dist/stats.js +248 -0
  64. package/dist/worker.d.ts +53 -0
  65. package/dist/worker.js +102 -0
  66. 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
+ ![example: a real `transformRequest` output: system prompt + tool docs reflowed into one dense 1573×1248 page, instruction banner on top, ↵ marking original newlines](https://raw.githubusercontent.com/teamchong/pxpipe/main/docs/assets/example-render.png)
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