@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
@@ -0,0 +1,133 @@
1
+ import { isPxpipeSupportedModel } from './applicability.js';
2
+ import { countCacheControlMarkers } from './measurement.js';
3
+ import { renderTextToPngsWithCharLimit, renderTextToPngsMultiCol, measureContentCols, maxFittingCols, reflow, DENSE_CONTENT_COLS, DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_RENDER_STYLE, MAX_HEIGHT_PX, } from './render.js';
4
+ import { transformRequest, } from './transform.js';
5
+ function toUint8Array(bytes) {
6
+ if (bytes instanceof Uint8Array)
7
+ return bytes;
8
+ if (bytes instanceof ArrayBuffer)
9
+ return new Uint8Array(bytes);
10
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
11
+ }
12
+ function emptyInfo(reason) {
13
+ return {
14
+ compressed: false,
15
+ reason,
16
+ origChars: 0,
17
+ compressedChars: 0,
18
+ imageCount: 0,
19
+ imageBytes: 0,
20
+ staticChars: 0,
21
+ dynamicChars: 0,
22
+ dynamicBlockCount: 0,
23
+ droppedChars: 0,
24
+ };
25
+ }
26
+ function classifyReason(info) {
27
+ if (info.compressed)
28
+ return 'applied';
29
+ const r = info.reason ?? '';
30
+ if (r.startsWith('parse_error'))
31
+ return 'parse_error';
32
+ if (r.startsWith('compress=false'))
33
+ return 'compress_disabled';
34
+ if (r.startsWith('below_min_chars'))
35
+ return 'below_min_chars';
36
+ if (r.startsWith('below_min_tokens'))
37
+ return 'below_min_tokens';
38
+ if (r.startsWith('not_profitable'))
39
+ return 'not_profitable';
40
+ if (r.includes('image') && r.includes('limit'))
41
+ return 'image_limit';
42
+ return 'passthrough';
43
+ }
44
+ /**
45
+ * Library wrapper for the Anthropic Messages transformer: model gate, machine-readable
46
+ * reasons, and cache_control ownership flag (prevents hosts stacking a second injector).
47
+ */
48
+ export async function transformAnthropicMessages(input) {
49
+ const original = toUint8Array(input.body);
50
+ if (!isPxpipeSupportedModel(input.model)) {
51
+ return {
52
+ body: original,
53
+ applied: false,
54
+ reason: 'unsupported_model',
55
+ detail: input.model ?? undefined,
56
+ info: emptyInfo('unsupported_model'),
57
+ cache: { ownsCacheControl: false, markerCount: countCacheControlMarkers(original) },
58
+ };
59
+ }
60
+ try {
61
+ const { body, info } = await transformRequest(original, input.options);
62
+ const reason = classifyReason(info);
63
+ const markerCount = countCacheControlMarkers(body);
64
+ return {
65
+ body,
66
+ applied: info.compressed,
67
+ reason,
68
+ detail: info.reason,
69
+ info,
70
+ cache: {
71
+ ownsCacheControl: info.compressed && markerCount > 0,
72
+ markerCount,
73
+ },
74
+ };
75
+ }
76
+ catch (e) {
77
+ return {
78
+ body: original,
79
+ applied: false,
80
+ reason: 'transform_error',
81
+ detail: e instanceof Error ? e.message : String(e),
82
+ info: emptyInfo(`transform_error: ${e instanceof Error ? e.message : String(e)}`),
83
+ cache: { ownsCacheControl: false, markerCount: countCacheControlMarkers(original) },
84
+ };
85
+ }
86
+ }
87
+ /**
88
+ * Render arbitrary text to dense PNG pages — the public, documented entry for the
89
+ * renderer the proxy uses internally. Sizes a narrow canvas to the content (`shrink`)
90
+ * and packs multiple columns (`multiCol`) so short-line content isn't priced at full
91
+ * width. Returns raw PNG bytes + pixel dimensions, ready to write to disk or wrap in
92
+ * image blocks. This is the surface SDK consumers should use instead of reaching into
93
+ * the internal leaf renderers in `render.ts`.
94
+ */
95
+ export async function renderTextToImages(text, opts = {}) {
96
+ const maxCols = Math.max(1, (opts.cols ?? DENSE_CONTENT_COLS) | 0);
97
+ const style = opts.style ?? DENSE_RENDER_STYLE;
98
+ const maxHeightPx = opts.maxHeightPx ?? MAX_HEIGHT_PX;
99
+ const maxChars = opts.maxCharsPerImage ?? DENSE_CONTENT_CHARS_PER_IMAGE;
100
+ // Reflow (the proxy's dense default; opt-in here): minify trailing whitespace + collapse
101
+ // blank-line runs, then join hard newlines with the ↵ sentinel so short lines PACK into
102
+ // full-width rows instead of one-line-per-row with a ragged right margin. Indentation is
103
+ // preserved (minifyForRender only touches trailing ws), so code stays readable and the ↵
104
+ // marks every real newline so the text is fully reconstructable. This is exactly what the
105
+ // proxy's history path does before rendering — without it, a 384-col canvas holding ~25-col
106
+ // code lines wastes ~75% of every row, which is why raw exports looked sparse. reflow()
107
+ // bails (→ raw text) only if the source already contains ↵, which is vanishingly rare.
108
+ const source = opts.reflow ? reflow(text) ?? text : text;
109
+ // Width/columns: measure the content, then pack as many side-by-side columns as fit the
110
+ // width cap (auto) or the caller's explicit count. Reflowed source is one ↵-joined full-
111
+ // width line, so this collapses to a single dense 384-col column — byte-identical to the
112
+ // proxy's history render (renderTextToPngsWithCharLimit at DENSE_CONTENT_COLS).
113
+ const cols = opts.shrink === false ? maxCols : measureContentCols(source, maxCols);
114
+ const requestedCols = opts.multiCol === undefined || opts.multiCol === 'auto'
115
+ ? Math.max(1, maxFittingCols(cols))
116
+ : Math.max(1, opts.multiCol | 0);
117
+ const numCols = cols < maxCols ? 1 : requestedCols;
118
+ const imgs = numCols > 1
119
+ ? await renderTextToPngsMultiCol(source, cols, numCols)
120
+ : await renderTextToPngsWithCharLimit(source, cols, maxChars, style, maxHeightPx);
121
+ let droppedChars = 0;
122
+ let pixels = 0;
123
+ for (const im of imgs) {
124
+ droppedChars += im.droppedChars;
125
+ pixels += im.width * im.height;
126
+ }
127
+ return {
128
+ pages: imgs.map((im) => ({ png: im.png, width: im.width, height: im.height })),
129
+ droppedChars,
130
+ pixels,
131
+ };
132
+ }
133
+ //# sourceMappingURL=library.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure body-shaping utilities for the uncompressed count_tokens counterfactual.
3
+ * No fetch, auth, or Node APIs — hosts supply their own transport.
4
+ */
5
+ export interface CountTokensBodies {
6
+ /** Full original body, filtered to count_tokens-accepted fields. */
7
+ readonly fullBody: Uint8Array | null;
8
+ /** Original body truncated at the latest cache_control marker; null when none exists. */
9
+ readonly cacheablePrefixBody: Uint8Array | null;
10
+ }
11
+ type BytesLike = Uint8Array | ArrayBuffer | ArrayBufferView;
12
+ export declare function buildCountTokensBodies(bytes: BytesLike): CountTokensBodies;
13
+ export declare function buildBaselineCountTokensBody(bytes: BytesLike): Uint8Array | null;
14
+ /** Build a body containing only the longest cacheable prefix (everything up to and including the last
15
+ * cache_control marker). count_tokens on this body gives cacheable_prefix_tokens.
16
+ * Walk order (latest-first in cache order): messages → system → tools.
17
+ * Returns null when no markers exist (cacheable_prefix_tokens = 0). */
18
+ export declare function buildCacheablePrefixCountTokensBody(bytes: BytesLike): Uint8Array | null;
19
+ /** Count cache_control markers anywhere in an Anthropic Messages body. */
20
+ export declare function countCacheControlMarkers(bytes: BytesLike): number;
21
+ export {};
22
+ //# sourceMappingURL=measurement.d.ts.map
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Pure body-shaping utilities for the uncompressed count_tokens counterfactual.
3
+ * No fetch, auth, or Node APIs — hosts supply their own transport.
4
+ */
5
+ /** Fields accepted by /v1/messages/count_tokens. Any other field returns 400 "Unknown parameter". */
6
+ const COUNT_TOKENS_FIELDS = new Set([
7
+ 'model',
8
+ 'messages',
9
+ 'system',
10
+ 'tools',
11
+ 'tool_choice',
12
+ 'thinking',
13
+ 'mcp_servers',
14
+ ]);
15
+ function toUint8Array(bytes) {
16
+ if (bytes instanceof Uint8Array)
17
+ return bytes;
18
+ if (bytes instanceof ArrayBuffer)
19
+ return new Uint8Array(bytes);
20
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
21
+ }
22
+ export function buildCountTokensBodies(bytes) {
23
+ const b = toUint8Array(bytes);
24
+ return {
25
+ fullBody: buildBaselineCountTokensBody(b),
26
+ cacheablePrefixBody: buildCacheablePrefixCountTokensBody(b),
27
+ };
28
+ }
29
+ export function buildBaselineCountTokensBody(bytes) {
30
+ const b = toUint8Array(bytes);
31
+ try {
32
+ const obj = JSON.parse(new TextDecoder().decode(b));
33
+ const out = {};
34
+ for (const k of Object.keys(obj)) {
35
+ if (COUNT_TOKENS_FIELDS.has(k))
36
+ out[k] = obj[k];
37
+ }
38
+ if (typeof out.model !== 'string' || !Array.isArray(out.messages))
39
+ return null;
40
+ return new TextEncoder().encode(JSON.stringify(out));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** True when an object carries a cache_control key (presence only; value ignored). */
47
+ function hasCacheControl(x) {
48
+ return (typeof x === 'object'
49
+ && x !== null
50
+ && x.cache_control != null);
51
+ }
52
+ /** Return tool_use ids with no matching tool_result. count_tokens rejects orphans;
53
+ * truncating at a cache_control marker commonly creates them (result is in the dropped tail). */
54
+ function findOrphanToolUseIds(messages) {
55
+ const uses = [];
56
+ const results = new Set();
57
+ for (const msg of messages) {
58
+ if (!msg || typeof msg !== 'object')
59
+ continue;
60
+ const content = msg.content;
61
+ if (!Array.isArray(content))
62
+ continue;
63
+ for (const blk of content) {
64
+ if (!blk || typeof blk !== 'object')
65
+ continue;
66
+ const t = blk.type;
67
+ if (t === 'tool_use') {
68
+ const id = blk.id;
69
+ if (typeof id === 'string')
70
+ uses.push(id);
71
+ }
72
+ else if (t === 'tool_result') {
73
+ const id = blk.tool_use_id;
74
+ if (typeof id === 'string')
75
+ results.add(id);
76
+ }
77
+ }
78
+ }
79
+ return uses.filter((id) => !results.has(id));
80
+ }
81
+ /** Append minimal synthetic tool_results for orphan tool_use ids so count_tokens won't reject the body.
82
+ * Adds only a handful of tokens; keeps estimate within ~1% of truth. */
83
+ function appendSyntheticToolResults(truncated) {
84
+ const messages = truncated.messages;
85
+ if (!Array.isArray(messages))
86
+ return truncated;
87
+ const orphanIds = findOrphanToolUseIds(messages);
88
+ if (orphanIds.length === 0)
89
+ return truncated;
90
+ const syntheticUserMsg = {
91
+ role: 'user',
92
+ content: orphanIds.map((id) => ({
93
+ type: 'tool_result',
94
+ tool_use_id: id,
95
+ content: 'ok',
96
+ })),
97
+ };
98
+ return { ...truncated, messages: [...messages, syntheticUserMsg] };
99
+ }
100
+ /** Build a body containing only the longest cacheable prefix (everything up to and including the last
101
+ * cache_control marker). count_tokens on this body gives cacheable_prefix_tokens.
102
+ * Walk order (latest-first in cache order): messages → system → tools.
103
+ * Returns null when no markers exist (cacheable_prefix_tokens = 0). */
104
+ export function buildCacheablePrefixCountTokensBody(bytes) {
105
+ const b = toUint8Array(bytes);
106
+ let obj;
107
+ try {
108
+ obj = JSON.parse(new TextDecoder().decode(b));
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ if (typeof obj.model !== 'string')
114
+ return null;
115
+ const system = obj.system;
116
+ const messages = obj.messages;
117
+ const tools = obj.tools;
118
+ let truncated = null;
119
+ if (Array.isArray(messages)) {
120
+ for (let mi = messages.length - 1; mi >= 0 && truncated == null; mi--) {
121
+ const msg = messages[mi];
122
+ const content = msg?.content;
123
+ if (Array.isArray(content)) {
124
+ for (let bi = content.length - 1; bi >= 0; bi--) {
125
+ if (hasCacheControl(content[bi])) {
126
+ const truncatedMsg = { ...msg, content: content.slice(0, bi + 1) };
127
+ const truncatedMessages = messages.slice(0, mi).concat([truncatedMsg]);
128
+ truncated = {
129
+ model: obj.model,
130
+ messages: truncatedMessages,
131
+ };
132
+ if (system !== undefined)
133
+ truncated.system = system;
134
+ if (tools !== undefined)
135
+ truncated.tools = tools;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ else if (hasCacheControl(msg)) {
141
+ truncated = {
142
+ model: obj.model,
143
+ messages: messages.slice(0, mi + 1),
144
+ };
145
+ if (system !== undefined)
146
+ truncated.system = system;
147
+ if (tools !== undefined)
148
+ truncated.tools = tools;
149
+ }
150
+ }
151
+ }
152
+ if (truncated == null && Array.isArray(system)) {
153
+ for (let si = system.length - 1; si >= 0; si--) {
154
+ if (hasCacheControl(system[si])) {
155
+ truncated = {
156
+ model: obj.model,
157
+ system: system.slice(0, si + 1),
158
+ messages: [{ role: 'user', content: 'x' }],
159
+ };
160
+ if (tools !== undefined)
161
+ truncated.tools = tools;
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ if (truncated == null && Array.isArray(tools)) {
167
+ for (let ti = tools.length - 1; ti >= 0; ti--) {
168
+ if (hasCacheControl(tools[ti])) {
169
+ truncated = {
170
+ model: obj.model,
171
+ tools: tools.slice(0, ti + 1),
172
+ messages: [{ role: 'user', content: 'x' }],
173
+ };
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ if (truncated == null)
179
+ return null;
180
+ truncated = appendSyntheticToolResults(truncated);
181
+ const out = {};
182
+ for (const k of Object.keys(truncated)) {
183
+ if (COUNT_TOKENS_FIELDS.has(k))
184
+ out[k] = truncated[k];
185
+ }
186
+ return new TextEncoder().encode(JSON.stringify(out));
187
+ }
188
+ /** Count cache_control markers anywhere in an Anthropic Messages body. */
189
+ export function countCacheControlMarkers(bytes) {
190
+ const b = toUint8Array(bytes);
191
+ try {
192
+ return countCacheControlValue(JSON.parse(new TextDecoder().decode(b)));
193
+ }
194
+ catch {
195
+ return 0;
196
+ }
197
+ }
198
+ function countCacheControlValue(value) {
199
+ if (!value || typeof value !== 'object')
200
+ return 0;
201
+ let n = hasCacheControl(value) ? 1 : 0;
202
+ if (Array.isArray(value)) {
203
+ for (const item of value)
204
+ n += countCacheControlValue(item);
205
+ }
206
+ else {
207
+ for (const item of Object.values(value)) {
208
+ n += countCacheControlValue(item);
209
+ }
210
+ }
211
+ return n;
212
+ }
213
+ //# sourceMappingURL=measurement.js.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * GPT history-image compression.
3
+ *
4
+ * The static system+tool slab is small (~30k chars); the bulk of a GPT agent
5
+ * request is the conversation transcript, which OpenCode resends in full every
6
+ * turn — the Responses API is driven statelessly here (no `previous_response_id`),
7
+ * so turns 1..N-1 are re-sent as plain text on turn N. pxpipe collapses the OLD
8
+ * closed-tool-call prefix of that transcript into 1-N PNG images and keeps the
9
+ * recent tail as text.
10
+ *
11
+ * OpenAI prompt-caching is automatic and prefix-based: no `cache_control`
12
+ * breakpoints, no 1.25× write premium, cached reads at ~0.1×. The collapse
13
+ * boundary is snapped to a chunk grid so the history image stays byte-identical
14
+ * across turns and keeps hitting that automatic cache (the same flap-avoidance
15
+ * trick src/core/history.ts uses for Anthropic).
16
+ *
17
+ * This mirrors src/core/history.ts but operates on Responses `input` items and
18
+ * Chat `messages` rather than Anthropic Message blocks. The two formats differ
19
+ * enough (function_call/function_call_output vs tool_calls/tool role) that a
20
+ * shared block type isn't worth it; instead each format is lowered to a common
21
+ * HistoryTurn list and the planner/renderer are shared.
22
+ */
23
+ import { type RenderedImage } from './render.js';
24
+ /** Break-even gate predicate, injected to avoid a circular import with openai.ts.
25
+ * Receives the full string (not length) so the renderer's row-aware image-count
26
+ * estimate sees real newlines — history text is newline-heavy. */
27
+ export type GptProfitableFn = (text: string, cols: number) => boolean;
28
+ export interface GptHistoryOptions {
29
+ /** Trailing items kept as live text (never collapsed). */
30
+ keepTail: number;
31
+ /** Minimum collapsible items in [protectedPrefix..boundary]; below this the
32
+ * cache-amortization math doesn't pay (imaging a tiny prefix is net cost). */
33
+ minCollapsePrefix: number;
34
+ /** Minimum collapsed-text size in o200k TOKENS (not chars). OpenAI caches the
35
+ * text transcript at ~0.1× already and bills images by vision tokens, so the
36
+ * break-even is a token comparison — 8000 chars of dense JSON tokenizes very
37
+ * differently from 8000 chars of prose. Below this, imaging a tiny prefix is
38
+ * net cost. */
39
+ minCollapseTokens: number;
40
+ /** Soft-wrap columns for the dense renderer. */
41
+ cols: number;
42
+ /** Advance the collapse boundary in steps of this many items so the rendered
43
+ * PNG stays byte-identical across turns and keeps hitting the prompt cache.
44
+ * 0 = per-item moving boundary (cache-hostile; tests only). */
45
+ collapseChunk: number;
46
+ /** Render the collapse range as independent image chunks of this many turns on
47
+ * an ABSOLUTE grid anchored at protectedPrefix. A completed chunk's bytes are
48
+ * fixed by its turn range alone, so old chunks stay byte-identical (cache_read
49
+ * forever) as the conversation grows — only the newest partial chunk
50
+ * re-renders. 0 = render the whole range as one blob (legacy, non-append-only). */
51
+ freezeChunk: number;
52
+ /** Target size of one frozen image SECTION, in o200k tokens. The collapse range
53
+ * is cut into sections by walking turns from protectedPrefix and sealing a
54
+ * section each time its cumulative token count crosses this target. A sealed
55
+ * section's bytes are a pure function of its turn range (independent of where
56
+ * the conversation currently ends), so it stays byte-identical — and OpenAI
57
+ * prefix-cache-hits — as the conversation grows. Leftover tail turns that don't
58
+ * fill a whole section are left UNCOLLAPSED (live text) until they do. Chosen so
59
+ * each section renders to roughly one ≤6000px image, well under gpt-5.x's
60
+ * 10,000-patch `detail:original` budget. Turn size, not turn count, drives this. */
61
+ sectionTokens: number;
62
+ /** Max rendered image height in px (per-model; from the GPT profile). Threaded
63
+ * into renderTextToPngs so history pages split at the same height the gate prices. */
64
+ maxHeightPx: number;
65
+ /** Hard cap on GPT history image count. This is a TRUE cap, not a threshold:
66
+ * collapse the oldest completed sections until the next section would exceed
67
+ * the cap, then leave the remaining history as ordinary text. Prevents 80+
68
+ * image gpt-5.5 requests without dropping context or live tool state. */
69
+ maxImages: number;
70
+ /** Reflow the transcript before rendering: pack soft-wrapped lines and mark
71
+ * every hard newline with the ↵ sentinel — same treatment as the static
72
+ * slab. History text is newline-heavy (role headers, JSON args), so without
73
+ * this each short line wastes a full render row and no ↵ marker appears.
74
+ * The returned `text` (o200k baseline + cache byte-stability) stays the
75
+ * ORIGINAL, un-reflowed transcript. */
76
+ reflow: boolean;
77
+ }
78
+ export declare const GPT_HISTORY_DEFAULTS: GptHistoryOptions;
79
+ /** One conversation item lowered to a renderable unit. */
80
+ export interface HistoryTurn {
81
+ /** Serialized text (with role header / tool markers). Empty = skip (e.g. reasoning). */
82
+ text: string;
83
+ /** Tool-call ids this item opens (function_call / assistant tool_calls). */
84
+ openIds: string[];
85
+ /** Tool-call ids this item closes (function_call_output / tool message). */
86
+ closeIds: string[];
87
+ /** Item we can't safely serialize (unknown kind, item_reference) — a hard
88
+ * barrier: never collapse across it, since dropping it could lose state. */
89
+ opaque: boolean;
90
+ /** Raw body when this item is a real USER request (role==='user', not a tool
91
+ * result). The planner pins the MOST RECENT such turn as legible text instead
92
+ * of imaging it, so the live ask is never OCR-only. undefined = not a user turn. */
93
+ userText?: string;
94
+ }
95
+ export interface GptCollapsePlan {
96
+ /** Rendered history images BEFORE the pinned user turn (or ALL images when no
97
+ * turn was pinned). Empty when no collapse happened. */
98
+ images: RenderedImage[];
99
+ /** Rendered history images AFTER the pinned user turn. Empty unless a pin split
100
+ * the range. Total imaged = images ∪ imagesAfter. */
101
+ imagesAfter: RenderedImage[];
102
+ /** Raw text of the most-recent user request, kept legible (NOT imaged) and
103
+ * spliced between `images` and `imagesAfter`. undefined = nothing pinned. */
104
+ pinText?: string;
105
+ /** The collapsed transcript text that was rendered (for o200k token counting). */
106
+ text: string;
107
+ /** Inclusive start index into the original item array. */
108
+ start: number;
109
+ /** Exclusive end index. Caller splices [start, endExclusive) → one synthetic item. */
110
+ endExclusive: number;
111
+ collapsedTurns: number;
112
+ collapsedChars: number;
113
+ reason?: 'prefix_too_short' | 'no_closed_prefix' | 'below_min_tokens' | 'not_profitable' | 'too_many_images' | 'render_empty';
114
+ droppedChars: number;
115
+ droppedCodepoints: Map<number, number>;
116
+ }
117
+ /**
118
+ * Plan + render a history collapse over pre-lowered turns. Pure w.r.t. the input
119
+ * (caller does the splice and builds the format-specific synthetic item).
120
+ */
121
+ export declare function planGptCollapse(turns: HistoryTurn[], protectedPrefix: number, isProfitable: GptProfitableFn, opts?: Partial<GptHistoryOptions>): Promise<GptCollapsePlan>;
122
+ export declare function responsesItemsToTurns(items: unknown[]): HistoryTurn[];
123
+ export declare function chatMessagesToTurns(messages: unknown[]): HistoryTurn[];
124
+ //# sourceMappingURL=openai-history.d.ts.map