pi-midcompact 0.4.0 → 0.5.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.
@@ -0,0 +1,185 @@
1
+ // Sole owner of factual message content statistics. Never converts local char or
2
+ // image byte counts into token claims. Image base64 never contributes to text
3
+ // char counts.
4
+
5
+ import type { ContentMetrics, ImageFact, MessageLike } from "./types.js";
6
+
7
+ /** Count Unicode code points of a string. */
8
+ export function codePointCount(text: string): number {
9
+ let count = 0;
10
+ // for..of iterates by code point, not UTF-16 code unit.
11
+ for (const _ of text) count += 1;
12
+ return count;
13
+ }
14
+
15
+ /** Decode a base64 string into a Uint8Array without depending on Node Buffer. */
16
+ function decodeBase64(data: string): Uint8Array {
17
+ const binary = atob(data);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
20
+ return bytes;
21
+ }
22
+
23
+ /** Best-effort pixel dimension read from decoded image bytes. Returns undefined on failure. */
24
+ export function readImageDimensions(bytes: Uint8Array): { width?: number; height?: number } {
25
+ if (bytes.length < 8) return {};
26
+ // PNG: 89 50 4E 47 0D 0A 1A 0A; width/height are big-endian at offsets 16/20.
27
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
28
+ if (bytes.length < 24) return {};
29
+ const view = new DataView(bytes.buffer, bytes.byteOffset + 16, 8);
30
+ return { width: view.getUint32(0), height: view.getUint32(4) };
31
+ }
32
+ // GIF: 47 49 46 38; width/height little-endian at offsets 6/8.
33
+ if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) {
34
+ return { width: bytes[6]! | (bytes[7]! << 8), height: bytes[8]! | (bytes[9]! << 8) };
35
+ }
36
+ // JPEG: scan SOF0 (0xFFC0) segment for dimensions.
37
+ if (bytes[0] === 0xff && bytes[1] === 0xd8) {
38
+ let i = 2;
39
+ while (i < bytes.length - 9) {
40
+ if (bytes[i] !== 0xff) { i += 1; continue; }
41
+ const marker = bytes[i + 1];
42
+ if (marker === undefined) break;
43
+ // SOF0..SOF15 carry dimensions; skip DHT (0xC4) and the reserved JPG marker (0xC8).
44
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8) {
45
+ const height = (bytes[i + 5]! << 8) | bytes[i + 6]!;
46
+ const width = (bytes[i + 7]! << 8) | bytes[i + 8]!;
47
+ return { width, height };
48
+ }
49
+ const len = (bytes[i + 2]! << 8) | bytes[i + 3]!;
50
+ i += 2 + len;
51
+ continue;
52
+ }
53
+ return {};
54
+ }
55
+ // WebP: RIFF....WEBP; VP8/VP8L/VP8X variants.
56
+ if (
57
+ bytes.length >= 30 &&
58
+ bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
59
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
60
+ ) {
61
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
62
+ const chunk = (bytes[12]! << 16) | (bytes[13]! << 8) | bytes[14]!;
63
+ if (chunk === 0x56503820) { // "VP8 "
64
+ return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
65
+ }
66
+ if (chunk === 0x56503858) { // "VP8X"
67
+ const w = 1 + (view.getUint32(24, true) >>> 0);
68
+ return { width: w & 0xffffff, height: view.getUint32(27, true) & 0xffffff };
69
+ }
70
+ return {};
71
+ }
72
+ return {};
73
+ }
74
+
75
+ function safeJson(value: unknown): string {
76
+ try {
77
+ return JSON.stringify(value) ?? "";
78
+ } catch {
79
+ return "[unserializable]";
80
+ }
81
+ }
82
+
83
+ interface PartLike {
84
+ type?: string;
85
+ text?: string;
86
+ thinking?: string;
87
+ name?: string;
88
+ arguments?: unknown;
89
+ data?: string;
90
+ mimeType?: string;
91
+ id?: string;
92
+ }
93
+
94
+ function isObject(value: unknown): value is Record<string, unknown> {
95
+ return typeof value === "object" && value !== null;
96
+ }
97
+
98
+ /**
99
+ * Measure the content parts of a message (its `content` field). Image parts are
100
+ * recorded as ImageFacts and never contribute to contentChars.
101
+ */
102
+ export function measureContentParts(content: unknown, startIndex = 0): ContentMetrics {
103
+ if (typeof content === "string") {
104
+ return { contentChars: codePointCount(content), imageCount: 0, images: [] };
105
+ }
106
+ if (!Array.isArray(content)) return { contentChars: 0, imageCount: 0, images: [] };
107
+
108
+ let contentChars = 0;
109
+ const images: ImageFact[] = [];
110
+ let imageIndex = 0;
111
+
112
+ for (const raw of content) {
113
+ if (!isObject(raw)) continue;
114
+ const part = raw as PartLike;
115
+ if (part.type === "text" && typeof part.text === "string") {
116
+ contentChars += codePointCount(part.text);
117
+ } else if (part.type === "thinking" && typeof part.thinking === "string") {
118
+ contentChars += codePointCount(part.thinking);
119
+ } else if (part.type === "toolCall") {
120
+ if (typeof part.name === "string") contentChars += codePointCount(part.name);
121
+ // Normalized arguments representation; id is metadata and not counted.
122
+ contentChars += codePointCount(safeJson(part.arguments));
123
+ } else if (part.type === "image") {
124
+ const mimeType = typeof part.mimeType === "string" ? part.mimeType : "application/octet-stream";
125
+ const data = typeof part.data === "string" ? part.data : "";
126
+ let payloadBytes = 0;
127
+ let dimensions: { width?: number; height?: number } = {};
128
+ if (data.length > 0) {
129
+ try {
130
+ const bytes = decodeBase64(data);
131
+ payloadBytes = bytes.length;
132
+ dimensions = readImageDimensions(bytes);
133
+ } catch {
134
+ payloadBytes = 0;
135
+ }
136
+ }
137
+ images.push({
138
+ index: startIndex + imageIndex,
139
+ mimeType,
140
+ payloadBytes,
141
+ ...(dimensions.width !== undefined ? { width: dimensions.width } : {}),
142
+ ...(dimensions.height !== undefined ? { height: dimensions.height } : {}),
143
+ });
144
+ imageIndex += 1;
145
+ }
146
+ }
147
+
148
+ return { contentChars, imageCount: images.length, images };
149
+ }
150
+
151
+ /**
152
+ * Measure a single message, including role-specific content fields
153
+ * (bash command/output, custom summary). Image payload bytes are never added to
154
+ * contentChars.
155
+ */
156
+ export function measureMessage(message: MessageLike): ContentMetrics {
157
+ const base = measureContentParts(message.content);
158
+ let contentChars = base.contentChars;
159
+ const images = [...base.images];
160
+
161
+ if (message.role === "bashExecution") {
162
+ if (typeof message.command === "string") contentChars += codePointCount(message.command);
163
+ if (typeof message.output === "string") contentChars += codePointCount(message.output);
164
+ }
165
+ if (message.role === "custom" && typeof message.summary === "string") {
166
+ contentChars += codePointCount(message.summary);
167
+ }
168
+
169
+ return { contentChars, imageCount: images.length, images };
170
+ }
171
+
172
+ /** Re-index images sequentially across the aggregated list. */
173
+ export function aggregateMetrics(parts: readonly ContentMetrics[]): ContentMetrics {
174
+ let contentChars = 0;
175
+ let imageCount = 0;
176
+ const images: ImageFact[] = [];
177
+ for (const part of parts) {
178
+ contentChars += part.contentChars;
179
+ imageCount += part.imageCount;
180
+ for (const image of part.images) {
181
+ images.push({ ...image, index: images.length });
182
+ }
183
+ }
184
+ return { contentChars, imageCount, images };
185
+ }