@linxiraos/pi-snapcompact 1.1.26 → 1.2.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/CHANGELOG.md +18 -4
- package/README.md +3 -3
- package/dist/types/snapcompact.d.ts +49 -17
- package/package.json +6 -6
- package/src/snapcompact.d.ts +49 -17
- package/src/snapcompact.ts +109 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
1
|
## [Unreleased]
|
|
4
2
|
|
|
5
|
-
## [1.
|
|
3
|
+
## [1.2.0] - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- 版本线推进至 1.1.28;本版无独立用户可见变化。
|
|
6
|
+
|
|
7
|
+
## [1.1.27] - 2026-10-07
|
|
8
|
+
|
|
9
|
+
- 版本线推进至 1.1.27;本版无独立用户可见变化。
|
|
10
|
+
|
|
11
|
+
### Breaking Changes
|
|
12
|
+
|
|
13
|
+
- `maxFramesForDataBudget()` now takes the frame shape instead of a byte budget, so the default 1568px shapes get 26 frames instead of 17 ([#14277](https://github.com/can1357/oh-my-pi/pull/14277) by [@will-bogusz](https://github.com/will-bogusz)).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added `frameBilling`, `frameTokens` and `frameBillingKey`, which price a rendered frame at what the model reading it is billed ([#14291](https://github.com/can1357/oh-my-pi/pull/14291) by [@will-bogusz](https://github.com/will-bogusz))
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
6
20
|
|
|
7
|
-
-
|
|
21
|
+
- Fixed snapcompact frame token estimates following the gateway instead of the model reading the frames, which mispriced Claude behind OpenRouter or Vertex and Claude 4.6 and older ([#14286](https://github.com/can1357/oh-my-pi/pull/14286) by [@will-bogusz](https://github.com/will-bogusz)).
|
|
8
22
|
|
|
9
23
|
## [1.1.25] - 2026-10-03
|
|
10
24
|
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Built for [omp](https://github.com/can1357/oh-my-pi)'s compaction pipeline, but
|
|
|
10
10
|
|
|
11
11
|
1. Discarded history is serialized to compact text (`serializeConversation`), with per-tool-result and per-argument character caps.
|
|
12
12
|
2. Text is normalized for the selected native font (`normalize`): ANSI sequences stripped, whitespace collapsed, newline runs folded into a single full-block glyph, box drawing and compatibility symbols folded to ASCII, semantic emoji folded to ASCII labels, decorative emoji dropped, and non-Latin glyphs preserved when either the selected font or the embedded Silver fallback can render them.
|
|
13
|
-
3. Pages of text are rasterized into PNG frames (`render` / `renderMany`). Frame width is fixed per shape; height hugs the rows actually printed,
|
|
13
|
+
3. Pages of text are rasterized into PNG frames (`render` / `renderMany`). Frame width is fixed per shape; height hugs the rows actually printed, with a 64px minimum so short final frames remain valid for vision processors.
|
|
14
14
|
4. Frames persist in the compaction entry's `preserveData` and are re-attached to the summary message on every context rebuild.
|
|
15
15
|
|
|
16
16
|
Frame shapes are provider-aware, chosen by SQuAD recall evals (see `research/`) against real provider billing:
|
|
@@ -22,7 +22,7 @@ Frame shapes are provider-aware, chosen by SQuAD recall evals (see `research/`)
|
|
|
22
22
|
| OpenAI | `8on22-bw` | X.org 8x13 glyphs on a 22px pitch, sent at `detail: "original"` |
|
|
23
23
|
| Unknown | Anthropic shape | Per-provider image-count budgets guard against gateways that silently drop frames |
|
|
24
24
|
|
|
25
|
-
`resolveShape({ api, id })` matches the model id, not just the wire API — a Claude routed through Vertex or OpenRouter keeps its Claude shape
|
|
25
|
+
`resolveShape({ api, id })` matches the model id, not just the wire API — a Claude routed through Vertex or OpenRouter keeps its Claude shape. Frames are priced by the reading model's lineage: its catalog `image-tokenization` rule (so that Claude is billed as Claude, and Opus 4.6 under the standard 1,568-token tier), falling back to the catalog rule for the wire API carrying the request when the lineage has none. A reader with neither costs `FRAME_TOKEN_ESTIMATE` per frame.
|
|
26
26
|
|
|
27
27
|
Bitmap shapes keep their provider-tuned geometry and draw missing glyphs through the embedded Silver TrueType fallback one character at a time; East Asian (CJK/Kana/Hangul) glyphs render full-width across two cells so they stay legible in the narrow ASCII grid. Selecting `silver16-bw` uses Silver for the whole frame.
|
|
28
28
|
|
|
@@ -62,7 +62,7 @@ const result = await compact(preparation, { model });
|
|
|
62
62
|
- **Rendering**: `render`, `renderMany`, `frames`, `geometry`
|
|
63
63
|
- **Shapes**: `SHAPES`, `SHAPE_VARIANTS`, `resolveShape`, `resolveShapeForText`, `idealShapeVariant`, `isShape`, `isShapeVariantName`
|
|
64
64
|
- **Text**: `serializeConversation`, `normalize`, `scanRenderability`, `renderabilityProbeText`, `dimStopwords`, `wrap`
|
|
65
|
-
- **Budgets**: `providerImageBudget`, `MAX_FRAMES_DEFAULT`, `FRAME_TOKEN_ESTIMATE`, `HQ_EDGE_FRAMES`
|
|
65
|
+
- **Budgets**: `providerImageBudget`, `MAX_FRAMES_DEFAULT`, `FRAME_TOKEN_ESTIMATE`, `frameBilling`, `frameTokens`, `frameBillingKey`, `HQ_EDGE_FRAMES`
|
|
66
66
|
- **File ops**: `createFileOps`, `computeFileLists`, `upsertFileOperations`
|
|
67
67
|
|
|
68
68
|
## References
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* conversation is rendered into PNG frames of pixel-font text that vision
|
|
6
6
|
* models read back directly, like an archivist at a snapcompact frame
|
|
7
7
|
* reader. Frames are `frameSize` wide; their height hugs the text rows
|
|
8
|
-
* actually printed,
|
|
8
|
+
* actually printed, with a 64px floor for vision processors that reject
|
|
9
|
+
* smaller images.
|
|
9
10
|
*
|
|
10
11
|
* The frame shape is provider-aware. Original choices came from the SQuAD
|
|
11
12
|
* prose evals (`packages/snapcompact`, 200k-token monolithic runs); the
|
|
@@ -31,12 +32,17 @@
|
|
|
31
32
|
* billing (32px × 1.2, 10k-patch budget at `detail: "original"`) is
|
|
32
33
|
* area-proportional, so resolution cannot improve chars/$ — 1568 stays.
|
|
33
34
|
* `detail: "high"` would downgrade (2,500-patch cap); `original` is sent.
|
|
34
|
-
* - **Unknown providers** default to `8on22-bw`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* - **Unknown providers** default to `8on22-bw`. `providerImageBudget` still
|
|
36
|
+
* caps per-request images per provider so inline imaging cannot flood a
|
|
37
|
+
* request with attachments, but the old OpenRouter-specific 8-image cap is
|
|
38
|
+
* gone; routers now use the same permissive budget as direct
|
|
39
|
+
* Anthropic/Claude lines unless configured otherwise upstream.
|
|
40
|
+
*
|
|
41
|
+
* The per-frame token estimate follows the reading model's catalog
|
|
42
|
+
* `image-tokenization` rule (its lineage, not the gateway), so Claude behind
|
|
43
|
+
* OpenRouter is billed as Claude and Opus 4.6 under its 1,568-token cap.
|
|
44
|
+
* Models without a lineage rule fall back to the catalog rule for their wire
|
|
45
|
+
* API; a reader with neither costs {@link FRAME_TOKEN_ESTIMATE} per frame.
|
|
40
46
|
*
|
|
41
47
|
* The whole pass is local and deterministic — no LLM call, no API key, no
|
|
42
48
|
* latency beyond rendering. Rasterization and PNG encoding happen in native
|
|
@@ -45,6 +51,7 @@
|
|
|
45
51
|
* re-attached to the compaction summary message on every context rebuild.
|
|
46
52
|
*/
|
|
47
53
|
import type { Api, ImageContent, Message, TextContent } from "@linxiraos/pi-ai";
|
|
54
|
+
import { type ImageSize, type ImageTokenization, type ImageTokenizationTarget } from "@linxiraos/pi-catalog/compat/image-tokenization";
|
|
48
55
|
/** One eval-validated frame shape: font, cell, ink, repetition, and size. */
|
|
49
56
|
export interface Shape {
|
|
50
57
|
/** Bundled font in the native renderer. */
|
|
@@ -245,6 +252,23 @@ export type ShapeVariantName = keyof typeof SHAPE_VARIANTS;
|
|
|
245
252
|
export declare const SHAPE_VARIANT_NAMES: readonly ShapeVariantName[];
|
|
246
253
|
/** Runtime guard for variant names loaded from config. */
|
|
247
254
|
export declare function isShapeVariantName(value: unknown): value is ShapeVariantName;
|
|
255
|
+
/**
|
|
256
|
+
* How a reader is billed per frame: its catalog `image-tokenization` rule
|
|
257
|
+
* (the model's lineage, else its wire API's fallback) and the `detail` hint
|
|
258
|
+
* snapcompact sends. OpenAI-family wires send `detail: "original"` (`high`
|
|
259
|
+
* caps the frame at 2,500 patches). Without a rule, every frame costs
|
|
260
|
+
* {@link FRAME_TOKEN_ESTIMATE}.
|
|
261
|
+
*/
|
|
262
|
+
export interface FrameBilling {
|
|
263
|
+
rule?: ImageTokenization;
|
|
264
|
+
imageDetail?: ImageContent["detail"];
|
|
265
|
+
}
|
|
266
|
+
/** Resolve the frame billing of `target`. */
|
|
267
|
+
export declare function frameBilling(target: ShapeTarget | undefined): FrameBilling;
|
|
268
|
+
/** Identity of a frame price list: equal keys price every frame alike. */
|
|
269
|
+
export declare function frameBillingKey(billing: FrameBilling): string;
|
|
270
|
+
/** Billed-token estimate for one rendered frame of `size` under `billing`. */
|
|
271
|
+
export declare function frameTokens(billing: FrameBilling, size: ImageSize): number;
|
|
248
272
|
/** Eval-validated shapes, keyed by the provider family they won on. */
|
|
249
273
|
export declare const SHAPES: {
|
|
250
274
|
/** `11on16-bw`: 8x13 glyphs on an 11px advance (extra tracking), black ink.
|
|
@@ -275,19 +299,23 @@ export interface IdealShape {
|
|
|
275
299
|
}
|
|
276
300
|
/** Eval-ideal format for a model id, or undefined when unmeasured. */
|
|
277
301
|
export declare function idealShapeVariant(modelId: string): IdealShape | undefined;
|
|
278
|
-
/** What will read the frames: the
|
|
302
|
+
/** What will read the frames: the model id (shape and billing), the wire API
|
|
303
|
+
* (request hints, fallback billing), and optionally the host and parsed
|
|
304
|
+
* identity of a built model. */
|
|
279
305
|
export interface ShapeTarget {
|
|
280
306
|
api?: Api;
|
|
281
307
|
id?: string;
|
|
308
|
+
provider?: string;
|
|
309
|
+
identity?: ImageTokenizationTarget["identity"];
|
|
282
310
|
}
|
|
283
311
|
/**
|
|
284
312
|
* Pick the frame shape for a reader. An explicit `variant` (anything but
|
|
285
313
|
* `"auto"`) forces that geometry; otherwise the model id selects the
|
|
286
314
|
* eval-winning shape — and frame size — for its model line, falling back to
|
|
287
315
|
* the API family's winner when the model is unmeasured. Billing (token
|
|
288
|
-
* estimate, detail hint)
|
|
289
|
-
*
|
|
290
|
-
* `
|
|
316
|
+
* estimate, detail hint) is computed for the resolved frame size from the
|
|
317
|
+
* model's catalog image rule. Accepts a full pi-ai `Model` or any
|
|
318
|
+
* `{ api, id }` subset.
|
|
291
319
|
*/
|
|
292
320
|
export declare function resolveShape(model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape;
|
|
293
321
|
/**
|
|
@@ -312,10 +340,11 @@ export declare const MAX_FRAMES_DEFAULT = 80;
|
|
|
312
340
|
* foveated archive — the session head (oldest) and the slice just before the
|
|
313
341
|
* text region (newest) — with the denser low-quality tier filling the middle. */
|
|
314
342
|
export declare const HQ_EDGE_FRAMES = 3;
|
|
315
|
-
/** Conservative per-frame token estimate
|
|
316
|
-
* upper bound across shapes: high-res Claude frames hit the
|
|
317
|
-
* cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
318
|
-
* undercounting a high-res archive at the raised
|
|
343
|
+
/** Conservative per-frame token estimate for archive sizing and for frames of
|
|
344
|
+
* unknown size — the upper bound across shapes: high-res Claude frames hit the
|
|
345
|
+
* 4,784 visual-token cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
346
|
+
* overflow guard from undercounting a high-res archive at the raised
|
|
347
|
+
* {@link MAX_FRAMES_DEFAULT}. */
|
|
319
348
|
export declare const FRAME_TOKEN_ESTIMATE = 5024;
|
|
320
349
|
/** Conservative upper bound for one persisted frame's base64 payload. The
|
|
321
350
|
* measured high-res Anthropic `8x13`/`11on16` PNG frames sit around 159 KB;
|
|
@@ -328,8 +357,11 @@ export declare const FRAME_DATA_BYTES_ESTIMATE = 170000;
|
|
|
328
357
|
* a 1M-token model can afford 70 images on paper, but not the resulting
|
|
329
358
|
* ~11 MB JSON payload on every turn. */
|
|
330
359
|
export declare const FRAME_DATA_BYTES_BUDGET = 3000000;
|
|
331
|
-
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET}
|
|
332
|
-
|
|
360
|
+
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET} for frames
|
|
361
|
+
* rendered by `shape`. {@link AREA_PRICED_VARIANTS} below 1932px are charged
|
|
362
|
+
* {@link FRAME_DATA_BYTES_ESTIMATE} scaled by pixel area; every other shape
|
|
363
|
+
* pays the full estimate. */
|
|
364
|
+
export declare function maxFramesForDataBudget(shape: ShapeGeometry): number;
|
|
333
365
|
/** Base64 byte length for persisted snapcompact frames. */
|
|
334
366
|
export declare function frameDataBytes(frames: readonly Pick<Frame, "data">[]): number;
|
|
335
367
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-snapcompact",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Linxira-OS",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@linxiraos/pi-ai": "1.
|
|
35
|
-
"@linxiraos/pi-catalog": "1.
|
|
36
|
-
"@linxiraos/pi-natives": "1.
|
|
37
|
-
"@linxiraos/pi-utils": "1.
|
|
38
|
-
"@linxiraos/pi-wire": "1.
|
|
34
|
+
"@linxiraos/pi-ai": "1.2.0",
|
|
35
|
+
"@linxiraos/pi-catalog": "1.2.0",
|
|
36
|
+
"@linxiraos/pi-natives": "1.2.0",
|
|
37
|
+
"@linxiraos/pi-utils": "1.2.0",
|
|
38
|
+
"@linxiraos/pi-wire": "1.2.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "^1.3.14"
|
package/src/snapcompact.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* conversation is rendered into PNG frames of pixel-font text that vision
|
|
6
6
|
* models read back directly, like an archivist at a snapcompact frame
|
|
7
7
|
* reader. Frames are `frameSize` wide; their height hugs the text rows
|
|
8
|
-
* actually printed,
|
|
8
|
+
* actually printed, with a 64px floor for vision processors that reject
|
|
9
|
+
* smaller images.
|
|
9
10
|
*
|
|
10
11
|
* The frame shape is provider-aware. Original choices came from the SQuAD
|
|
11
12
|
* prose evals (`packages/snapcompact`, 200k-token monolithic runs); the
|
|
@@ -31,12 +32,17 @@
|
|
|
31
32
|
* billing (32px × 1.2, 10k-patch budget at `detail: "original"`) is
|
|
32
33
|
* area-proportional, so resolution cannot improve chars/$ — 1568 stays.
|
|
33
34
|
* `detail: "high"` would downgrade (2,500-patch cap); `original` is sent.
|
|
34
|
-
* - **Unknown providers** default to `8on22-bw`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* - **Unknown providers** default to `8on22-bw`. `providerImageBudget` still
|
|
36
|
+
* caps per-request images per provider so inline imaging cannot flood a
|
|
37
|
+
* request with attachments, but the old OpenRouter-specific 8-image cap is
|
|
38
|
+
* gone; routers now use the same permissive budget as direct
|
|
39
|
+
* Anthropic/Claude lines unless configured otherwise upstream.
|
|
40
|
+
*
|
|
41
|
+
* The per-frame token estimate follows the reading model's catalog
|
|
42
|
+
* `image-tokenization` rule (its lineage, not the gateway), so Claude behind
|
|
43
|
+
* OpenRouter is billed as Claude and Opus 4.6 under its 1,568-token cap.
|
|
44
|
+
* Models without a lineage rule fall back to the catalog rule for their wire
|
|
45
|
+
* API; a reader with neither costs {@link FRAME_TOKEN_ESTIMATE} per frame.
|
|
40
46
|
*
|
|
41
47
|
* The whole pass is local and deterministic — no LLM call, no API key, no
|
|
42
48
|
* latency beyond rendering. Rasterization and PNG encoding happen in native
|
|
@@ -45,6 +51,7 @@
|
|
|
45
51
|
* re-attached to the compaction summary message on every context rebuild.
|
|
46
52
|
*/
|
|
47
53
|
import type { Api, ImageContent, Message, TextContent } from "@linxiraos/pi-ai";
|
|
54
|
+
import { type ImageSize, type ImageTokenization, type ImageTokenizationTarget } from "@linxiraos/pi-catalog/compat/image-tokenization";
|
|
48
55
|
/** One eval-validated frame shape: font, cell, ink, repetition, and size. */
|
|
49
56
|
export interface Shape {
|
|
50
57
|
/** Bundled font in the native renderer. */
|
|
@@ -245,6 +252,23 @@ export type ShapeVariantName = keyof typeof SHAPE_VARIANTS;
|
|
|
245
252
|
export declare const SHAPE_VARIANT_NAMES: readonly ShapeVariantName[];
|
|
246
253
|
/** Runtime guard for variant names loaded from config. */
|
|
247
254
|
export declare function isShapeVariantName(value: unknown): value is ShapeVariantName;
|
|
255
|
+
/**
|
|
256
|
+
* How a reader is billed per frame: its catalog `image-tokenization` rule
|
|
257
|
+
* (the model's lineage, else its wire API's fallback) and the `detail` hint
|
|
258
|
+
* snapcompact sends. OpenAI-family wires send `detail: "original"` (`high`
|
|
259
|
+
* caps the frame at 2,500 patches). Without a rule, every frame costs
|
|
260
|
+
* {@link FRAME_TOKEN_ESTIMATE}.
|
|
261
|
+
*/
|
|
262
|
+
export interface FrameBilling {
|
|
263
|
+
rule?: ImageTokenization;
|
|
264
|
+
imageDetail?: ImageContent["detail"];
|
|
265
|
+
}
|
|
266
|
+
/** Resolve the frame billing of `target`. */
|
|
267
|
+
export declare function frameBilling(target: ShapeTarget | undefined): FrameBilling;
|
|
268
|
+
/** Identity of a frame price list: equal keys price every frame alike. */
|
|
269
|
+
export declare function frameBillingKey(billing: FrameBilling): string;
|
|
270
|
+
/** Billed-token estimate for one rendered frame of `size` under `billing`. */
|
|
271
|
+
export declare function frameTokens(billing: FrameBilling, size: ImageSize): number;
|
|
248
272
|
/** Eval-validated shapes, keyed by the provider family they won on. */
|
|
249
273
|
export declare const SHAPES: {
|
|
250
274
|
/** `11on16-bw`: 8x13 glyphs on an 11px advance (extra tracking), black ink.
|
|
@@ -275,19 +299,23 @@ export interface IdealShape {
|
|
|
275
299
|
}
|
|
276
300
|
/** Eval-ideal format for a model id, or undefined when unmeasured. */
|
|
277
301
|
export declare function idealShapeVariant(modelId: string): IdealShape | undefined;
|
|
278
|
-
/** What will read the frames: the
|
|
302
|
+
/** What will read the frames: the model id (shape and billing), the wire API
|
|
303
|
+
* (request hints, fallback billing), and optionally the host and parsed
|
|
304
|
+
* identity of a built model. */
|
|
279
305
|
export interface ShapeTarget {
|
|
280
306
|
api?: Api;
|
|
281
307
|
id?: string;
|
|
308
|
+
provider?: string;
|
|
309
|
+
identity?: ImageTokenizationTarget["identity"];
|
|
282
310
|
}
|
|
283
311
|
/**
|
|
284
312
|
* Pick the frame shape for a reader. An explicit `variant` (anything but
|
|
285
313
|
* `"auto"`) forces that geometry; otherwise the model id selects the
|
|
286
314
|
* eval-winning shape — and frame size — for its model line, falling back to
|
|
287
315
|
* the API family's winner when the model is unmeasured. Billing (token
|
|
288
|
-
* estimate, detail hint)
|
|
289
|
-
*
|
|
290
|
-
* `
|
|
316
|
+
* estimate, detail hint) is computed for the resolved frame size from the
|
|
317
|
+
* model's catalog image rule. Accepts a full pi-ai `Model` or any
|
|
318
|
+
* `{ api, id }` subset.
|
|
291
319
|
*/
|
|
292
320
|
export declare function resolveShape(model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape;
|
|
293
321
|
/**
|
|
@@ -312,10 +340,11 @@ export declare const MAX_FRAMES_DEFAULT = 80;
|
|
|
312
340
|
* foveated archive — the session head (oldest) and the slice just before the
|
|
313
341
|
* text region (newest) — with the denser low-quality tier filling the middle. */
|
|
314
342
|
export declare const HQ_EDGE_FRAMES = 3;
|
|
315
|
-
/** Conservative per-frame token estimate
|
|
316
|
-
* upper bound across shapes: high-res Claude frames hit the
|
|
317
|
-
* cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
318
|
-
* undercounting a high-res archive at the raised
|
|
343
|
+
/** Conservative per-frame token estimate for archive sizing and for frames of
|
|
344
|
+
* unknown size — the upper bound across shapes: high-res Claude frames hit the
|
|
345
|
+
* 4,784 visual-token cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
346
|
+
* overflow guard from undercounting a high-res archive at the raised
|
|
347
|
+
* {@link MAX_FRAMES_DEFAULT}. */
|
|
319
348
|
export declare const FRAME_TOKEN_ESTIMATE = 5024;
|
|
320
349
|
/** Conservative upper bound for one persisted frame's base64 payload. The
|
|
321
350
|
* measured high-res Anthropic `8x13`/`11on16` PNG frames sit around 159 KB;
|
|
@@ -328,8 +357,11 @@ export declare const FRAME_DATA_BYTES_ESTIMATE = 170000;
|
|
|
328
357
|
* a 1M-token model can afford 70 images on paper, but not the resulting
|
|
329
358
|
* ~11 MB JSON payload on every turn. */
|
|
330
359
|
export declare const FRAME_DATA_BYTES_BUDGET = 3000000;
|
|
331
|
-
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET}
|
|
332
|
-
|
|
360
|
+
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET} for frames
|
|
361
|
+
* rendered by `shape`. {@link AREA_PRICED_VARIANTS} below 1932px are charged
|
|
362
|
+
* {@link FRAME_DATA_BYTES_ESTIMATE} scaled by pixel area; every other shape
|
|
363
|
+
* pays the full estimate. */
|
|
364
|
+
export declare function maxFramesForDataBudget(shape: ShapeGeometry): number;
|
|
333
365
|
/** Base64 byte length for persisted snapcompact frames. */
|
|
334
366
|
export declare function frameDataBytes(frames: readonly Pick<Frame, "data">[]): number;
|
|
335
367
|
/**
|
package/src/snapcompact.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* conversation is rendered into PNG frames of pixel-font text that vision
|
|
6
6
|
* models read back directly, like an archivist at a snapcompact frame
|
|
7
7
|
* reader. Frames are `frameSize` wide; their height hugs the text rows
|
|
8
|
-
* actually printed,
|
|
8
|
+
* actually printed, with a 64px floor for vision processors that reject
|
|
9
|
+
* smaller images.
|
|
9
10
|
*
|
|
10
11
|
* The frame shape is provider-aware. Original choices came from the SQuAD
|
|
11
12
|
* prose evals (`packages/snapcompact`, 200k-token monolithic runs); the
|
|
@@ -31,12 +32,17 @@
|
|
|
31
32
|
* billing (32px × 1.2, 10k-patch budget at `detail: "original"`) is
|
|
32
33
|
* area-proportional, so resolution cannot improve chars/$ — 1568 stays.
|
|
33
34
|
* `detail: "high"` would downgrade (2,500-patch cap); `original` is sent.
|
|
34
|
-
* - **Unknown providers** default to `8on22-bw`
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* - **Unknown providers** default to `8on22-bw`. `providerImageBudget` still
|
|
36
|
+
* caps per-request images per provider so inline imaging cannot flood a
|
|
37
|
+
* request with attachments, but the old OpenRouter-specific 8-image cap is
|
|
38
|
+
* gone; routers now use the same permissive budget as direct
|
|
39
|
+
* Anthropic/Claude lines unless configured otherwise upstream.
|
|
40
|
+
*
|
|
41
|
+
* The per-frame token estimate follows the reading model's catalog
|
|
42
|
+
* `image-tokenization` rule (its lineage, not the gateway), so Claude behind
|
|
43
|
+
* OpenRouter is billed as Claude and Opus 4.6 under its 1,568-token cap.
|
|
44
|
+
* Models without a lineage rule fall back to the catalog rule for their wire
|
|
45
|
+
* API; a reader with neither costs {@link FRAME_TOKEN_ESTIMATE} per frame.
|
|
40
46
|
*
|
|
41
47
|
* The whole pass is local and deterministic — no LLM call, no API key, no
|
|
42
48
|
* latency beyond rendering. Rasterization and PNG encoding happen in native
|
|
@@ -46,6 +52,13 @@
|
|
|
46
52
|
*/
|
|
47
53
|
|
|
48
54
|
import type { Api, ImageContent, Message, TextContent } from "@linxiraos/pi-ai";
|
|
55
|
+
import {
|
|
56
|
+
type ImageSize,
|
|
57
|
+
type ImageTokenization,
|
|
58
|
+
type ImageTokenizationTarget,
|
|
59
|
+
imageTokens,
|
|
60
|
+
resolveImageTokenization,
|
|
61
|
+
} from "@linxiraos/pi-catalog/compat/image-tokenization";
|
|
49
62
|
import { classifyModel, compareRevision, parseRevision } from "@linxiraos/pi-catalog/identity";
|
|
50
63
|
import { renderSnapcompactPng, snapcompactSupportedChars } from "@linxiraos/pi-natives";
|
|
51
64
|
import { formatGroupedPaths, prompt } from "@linxiraos/pi-utils";
|
|
@@ -202,10 +215,10 @@ export function isShapeVariantName(value: unknown): value is ShapeVariantName {
|
|
|
202
215
|
return typeof value === "string" && value in SHAPE_VARIANTS;
|
|
203
216
|
}
|
|
204
217
|
|
|
205
|
-
/**
|
|
206
|
-
type
|
|
218
|
+
/** Wire API families with distinct default shapes and request hints. */
|
|
219
|
+
type ApiFamily = "anthropic" | "google" | "openai" | "unknown";
|
|
207
220
|
|
|
208
|
-
function
|
|
221
|
+
function apiFamily(api?: Api): ApiFamily {
|
|
209
222
|
switch (api) {
|
|
210
223
|
case "anthropic-messages":
|
|
211
224
|
case "bedrock-converse-stream":
|
|
@@ -220,39 +233,47 @@ function billingFamily(api?: Api): BillingFamily {
|
|
|
220
233
|
case "google-vertex":
|
|
221
234
|
return "google";
|
|
222
235
|
default:
|
|
223
|
-
// Unknown APIs share Anthropic's pixel-area pricing as the safe ceiling.
|
|
224
236
|
return "unknown";
|
|
225
237
|
}
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* default HIGH = 1,120 tokens — regardless of pixel size.
|
|
235
|
-
* - OpenAI: 32px patches × 1.2 flagship multiplier, 10,000-patch budget at
|
|
236
|
-
* `detail: "original"` (1568 → 2,881 measured).
|
|
241
|
+
* How a reader is billed per frame: its catalog `image-tokenization` rule
|
|
242
|
+
* (the model's lineage, else its wire API's fallback) and the `detail` hint
|
|
243
|
+
* snapcompact sends. OpenAI-family wires send `detail: "original"` (`high`
|
|
244
|
+
* caps the frame at 2,500 patches). Without a rule, every frame costs
|
|
245
|
+
* {@link FRAME_TOKEN_ESTIMATE}.
|
|
237
246
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
export interface FrameBilling {
|
|
248
|
+
rule?: ImageTokenization;
|
|
249
|
+
imageDetail?: ImageContent["detail"];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Resolve the frame billing of `target`. */
|
|
253
|
+
export function frameBilling(target: ShapeTarget | undefined): FrameBilling {
|
|
254
|
+
const rule = target ? resolveImageTokenization(target) : undefined;
|
|
255
|
+
const imageDetail = apiFamily(target?.api) === "openai" ? "original" : undefined;
|
|
256
|
+
return { ...(rule && { rule }), ...(imageDetail && { imageDetail }) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Identity of a frame price list: equal keys price every frame alike. */
|
|
260
|
+
export function frameBillingKey(billing: FrameBilling): string {
|
|
261
|
+
return `${billing.imageDetail ?? "auto"}:${billing.rule ? JSON.stringify(billing.rule) : "ceiling"}`;
|
|
251
262
|
}
|
|
252
263
|
|
|
253
|
-
/**
|
|
254
|
-
function
|
|
255
|
-
return
|
|
264
|
+
/** Billed-token estimate for one rendered frame of `size` under `billing`. */
|
|
265
|
+
export function frameTokens(billing: FrameBilling, size: ImageSize): number {
|
|
266
|
+
return billing.rule ? imageTokens(billing.rule, size, billing.imageDetail) : FRAME_TOKEN_ESTIMATE;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Attach the reader's billing to a variant geometry. The square price is a
|
|
271
|
+
* planning bound: shorter frames can bill differently.
|
|
272
|
+
*/
|
|
273
|
+
function priceShape(base: ShapeGeometry, target: ShapeTarget | undefined): Shape {
|
|
274
|
+
const billing = frameBilling(target);
|
|
275
|
+
const frameTokenEstimate = frameTokens(billing, { width: base.frameSize, height: base.frameSize });
|
|
276
|
+
return { ...base, frameTokenEstimate, ...(billing.imageDetail && { imageDetail: billing.imageDetail }) };
|
|
256
277
|
}
|
|
257
278
|
|
|
258
279
|
/** Eval-validated shapes, keyed by the provider family they won on. */
|
|
@@ -262,18 +283,18 @@ export const SHAPES = {
|
|
|
262
283
|
* on opus-4.8: f1 .806 vs .755 for plain `8on16-bw` and .351 for the prior
|
|
263
284
|
* `6x12-dim` default — letter-spacing the readable cell wins; the dense
|
|
264
285
|
* 6x12 was below the OCR ~16px/char floor and abstained. */
|
|
265
|
-
anthropic: priceShape(SHAPE_VARIANTS["11on16-bw"], "anthropic"),
|
|
286
|
+
anthropic: priceShape(SHAPE_VARIANTS["11on16-bw"], { api: "anthropic-messages" }),
|
|
266
287
|
/** `8on22-bw`: 8x13 glyphs on a 22px pitch (extra leading), black ink.
|
|
267
288
|
* Tool-result legibility bench on gemini-3.5-flash: f1 .934 vs .807 for
|
|
268
289
|
* plain `8on16-bw` and .287 for the prior `doc-8on16-sent-dim`; the
|
|
269
290
|
* line-spacing reduces row crowding so line numbers stay legible. */
|
|
270
|
-
google: priceShape(SHAPE_VARIANTS["8on22-bw"], "google"),
|
|
291
|
+
google: priceShape(SHAPE_VARIANTS["8on22-bw"], { api: "google-generative-ai" }),
|
|
271
292
|
/** `8on22-bw`: 8x13 glyphs on a 22px pitch (extra leading), black ink.
|
|
272
293
|
* Same line-spacing win for OpenAI; bench on gpt-5.5/gpt-5.4-mini showed
|
|
273
294
|
* leading lifts recall on the readable cell over plain `8on16-bw`. */
|
|
274
|
-
openai: priceShape(SHAPE_VARIANTS["8on22-bw"], "openai"),
|
|
295
|
+
openai: priceShape(SHAPE_VARIANTS["8on22-bw"], { api: "openai-responses" }),
|
|
275
296
|
/** Original 5x8 X.org shape (pre-shape-table sessions rendered this). */
|
|
276
|
-
legacy: priceShape(SHAPE_VARIANTS["5x8-sent"], "anthropic"),
|
|
297
|
+
legacy: priceShape(SHAPE_VARIANTS["5x8-sent"], { api: "anthropic-messages" }),
|
|
277
298
|
} satisfies Record<string, Shape>;
|
|
278
299
|
|
|
279
300
|
/** Runtime guard for shape overrides loaded from config or preserve data. */
|
|
@@ -303,9 +324,9 @@ export function isShape(value: unknown): value is Shape {
|
|
|
303
324
|
);
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
/** Eval-winning variant per
|
|
307
|
-
*
|
|
308
|
-
const FAMILY_VARIANT: Record<
|
|
327
|
+
/** Eval-winning variant per wire family (fallback when the model id matches
|
|
328
|
+
* no known reader line). */
|
|
329
|
+
const FAMILY_VARIANT: Record<ApiFamily, ShapeVariantName> = {
|
|
309
330
|
anthropic: "11on16-bw",
|
|
310
331
|
google: "8on22-bw",
|
|
311
332
|
openai: "8on22-bw",
|
|
@@ -316,20 +337,13 @@ const FAMILY_VARIANT: Record<BillingFamily, ShapeVariantName> = {
|
|
|
316
337
|
* pixels (identical per-frame bill) but a tighter 8px cell, trading some
|
|
317
338
|
* legibility for ~40% more chars per frame so the least-important middle of a
|
|
318
339
|
* long archive compresses into fewer frames. */
|
|
319
|
-
const FAMILY_VARIANT_LOW: Record<
|
|
340
|
+
const FAMILY_VARIANT_LOW: Record<ApiFamily, ShapeVariantName> = {
|
|
320
341
|
anthropic: "8on16-bw",
|
|
321
342
|
google: "8on16-bw",
|
|
322
343
|
openai: "8on16-bw",
|
|
323
344
|
unknown: "8on16-bw",
|
|
324
345
|
};
|
|
325
346
|
|
|
326
|
-
const FAMILY_SHAPE: Record<BillingFamily, Shape> = {
|
|
327
|
-
anthropic: SHAPES.anthropic,
|
|
328
|
-
google: SHAPES.google,
|
|
329
|
-
openai: SHAPES.openai,
|
|
330
|
-
unknown: priceShape(SHAPE_VARIANTS["8on22-bw"], "unknown"),
|
|
331
|
-
};
|
|
332
|
-
|
|
333
347
|
/** One model line's ideal format: variant plus an optional frame-size
|
|
334
348
|
* override when the line reads larger frames at no extra cost. */
|
|
335
349
|
export interface IdealShape {
|
|
@@ -386,10 +400,14 @@ export function idealShapeVariant(modelId: string): IdealShape | undefined {
|
|
|
386
400
|
return MODEL_VARIANTS.find(([pattern]) => pattern.test(modelId))?.[1];
|
|
387
401
|
}
|
|
388
402
|
|
|
389
|
-
/** What will read the frames: the
|
|
403
|
+
/** What will read the frames: the model id (shape and billing), the wire API
|
|
404
|
+
* (request hints, fallback billing), and optionally the host and parsed
|
|
405
|
+
* identity of a built model. */
|
|
390
406
|
export interface ShapeTarget {
|
|
391
407
|
api?: Api;
|
|
392
408
|
id?: string;
|
|
409
|
+
provider?: string;
|
|
410
|
+
identity?: ImageTokenizationTarget["identity"];
|
|
393
411
|
}
|
|
394
412
|
|
|
395
413
|
/**
|
|
@@ -397,18 +415,15 @@ export interface ShapeTarget {
|
|
|
397
415
|
* `"auto"`) forces that geometry; otherwise the model id selects the
|
|
398
416
|
* eval-winning shape — and frame size — for its model line, falling back to
|
|
399
417
|
* the API family's winner when the model is unmeasured. Billing (token
|
|
400
|
-
* estimate, detail hint)
|
|
401
|
-
*
|
|
402
|
-
* `
|
|
418
|
+
* estimate, detail hint) is computed for the resolved frame size from the
|
|
419
|
+
* model's catalog image rule. Accepts a full pi-ai `Model` or any
|
|
420
|
+
* `{ api, id }` subset.
|
|
403
421
|
*/
|
|
404
422
|
export function resolveShape(model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape {
|
|
405
|
-
|
|
406
|
-
if (variant && variant !== "auto") return priceShape(SHAPE_VARIANTS[variant], family);
|
|
423
|
+
if (variant && variant !== "auto") return priceShape(SHAPE_VARIANTS[variant], model);
|
|
407
424
|
const ideal = model?.id ? idealShapeVariant(model.id) : undefined;
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
const base = SHAPE_VARIANTS[name];
|
|
411
|
-
return priceShape(ideal?.frameSize ? { ...base, frameSize: ideal.frameSize } : base, family);
|
|
425
|
+
const base = SHAPE_VARIANTS[ideal?.variant ?? FAMILY_VARIANT[apiFamily(model?.api)]];
|
|
426
|
+
return priceShape(ideal?.frameSize ? { ...base, frameSize: ideal.frameSize } : base, model);
|
|
412
427
|
}
|
|
413
428
|
|
|
414
429
|
const CJK_HEAVY_MIN_WIDE_CHARS = 8;
|
|
@@ -468,10 +483,11 @@ export const MAX_FRAMES_DEFAULT = 80;
|
|
|
468
483
|
* text region (newest) — with the denser low-quality tier filling the middle. */
|
|
469
484
|
export const HQ_EDGE_FRAMES = 3;
|
|
470
485
|
|
|
471
|
-
/** Conservative per-frame token estimate
|
|
472
|
-
* upper bound across shapes: high-res Claude frames hit the
|
|
473
|
-
* cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
474
|
-
* undercounting a high-res archive at the raised
|
|
486
|
+
/** Conservative per-frame token estimate for archive sizing and for frames of
|
|
487
|
+
* unknown size — the upper bound across shapes: high-res Claude frames hit the
|
|
488
|
+
* 4,784 visual-token cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the
|
|
489
|
+
* overflow guard from undercounting a high-res archive at the raised
|
|
490
|
+
* {@link MAX_FRAMES_DEFAULT}. */
|
|
475
491
|
export const FRAME_TOKEN_ESTIMATE = 5024;
|
|
476
492
|
|
|
477
493
|
/** Conservative upper bound for one persisted frame's base64 payload. The
|
|
@@ -487,9 +503,29 @@ export const FRAME_DATA_BYTES_ESTIMATE = 170_000;
|
|
|
487
503
|
* ~11 MB JSON payload on every turn. */
|
|
488
504
|
export const FRAME_DATA_BYTES_BUDGET = 3_000_000;
|
|
489
505
|
|
|
490
|
-
/**
|
|
491
|
-
|
|
492
|
-
|
|
506
|
+
/** Default edge variants whose frames shrink with pixel area. Inkier variants
|
|
507
|
+
* keep the full {@link FRAME_DATA_BYTES_ESTIMATE} at any frame size. */
|
|
508
|
+
const AREA_PRICED_VARIANTS: readonly ShapeGeometry[] = [SHAPE_VARIANTS["8on22-bw"], SHAPE_VARIANTS["11on16-bw"]];
|
|
509
|
+
|
|
510
|
+
/** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET} for frames
|
|
511
|
+
* rendered by `shape`. {@link AREA_PRICED_VARIANTS} below 1932px are charged
|
|
512
|
+
* {@link FRAME_DATA_BYTES_ESTIMATE} scaled by pixel area; every other shape
|
|
513
|
+
* pays the full estimate. */
|
|
514
|
+
export function maxFramesForDataBudget(shape: ShapeGeometry): number {
|
|
515
|
+
const areaPriced = AREA_PRICED_VARIANTS.some(
|
|
516
|
+
variant =>
|
|
517
|
+
variant.font === shape.font &&
|
|
518
|
+
variant.cellWidth === shape.cellWidth &&
|
|
519
|
+
variant.cellHeight === shape.cellHeight &&
|
|
520
|
+
variant.stretch === shape.stretch &&
|
|
521
|
+
variant.variant === shape.variant &&
|
|
522
|
+
variant.stopwordDim === shape.stopwordDim &&
|
|
523
|
+
variant.columns === shape.columns &&
|
|
524
|
+
variant.lineRepeat === shape.lineRepeat,
|
|
525
|
+
);
|
|
526
|
+
const areaRatio = areaPriced ? (shape.frameSize / HIGH_RES_ANTHROPIC_VARIANT.frameSize) ** 2 : 1;
|
|
527
|
+
const frameBytes = Math.min(FRAME_DATA_BYTES_ESTIMATE, Math.ceil(FRAME_DATA_BYTES_ESTIMATE * areaRatio));
|
|
528
|
+
return Math.max(1, Math.floor(FRAME_DATA_BYTES_BUDGET / frameBytes));
|
|
493
529
|
}
|
|
494
530
|
|
|
495
531
|
/** Base64 byte length for persisted snapcompact frames. */
|
|
@@ -1945,8 +1981,12 @@ export function historyBlocks(archive: Archive, options: HistoryBlockOptions = {
|
|
|
1945
1981
|
* variant exists (foveation off). */
|
|
1946
1982
|
function denseCompanion(high: Shape, api: Api | undefined): Shape {
|
|
1947
1983
|
if (high.columns === 2 || high.font === "silver") return high;
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1984
|
+
const low: Shape = {
|
|
1985
|
+
...SHAPE_VARIANTS[FAMILY_VARIANT_LOW[apiFamily(api)]],
|
|
1986
|
+
frameSize: high.frameSize,
|
|
1987
|
+
frameTokenEstimate: high.frameTokenEstimate,
|
|
1988
|
+
...(high.imageDetail && { imageDetail: high.imageDetail }),
|
|
1989
|
+
};
|
|
1950
1990
|
return geometry(low).capacity > geometry(high).capacity ? low : high;
|
|
1951
1991
|
}
|
|
1952
1992
|
|