@oh-my-pi/pi-agent-core 18.6.0 → 18.6.3
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 +29 -0
- package/dist/types/agent.d.ts +12 -5
- package/dist/types/compaction/anthropic.d.ts +14 -8
- package/dist/types/compaction/compaction-v2-streaming.d.ts +5 -1
- package/dist/types/compaction/compaction.d.ts +14 -0
- package/dist/types/compaction/entries.d.ts +2 -2
- package/dist/types/compaction/openai.d.ts +14 -2
- package/dist/types/image-tokens.d.ts +29 -0
- package/dist/types/tokenizer.d.ts +9 -6
- package/package.json +8 -8
- package/src/agent.ts +77 -17
- package/src/compaction/anthropic.ts +30 -12
- package/src/compaction/compaction-v2-streaming.ts +13 -15
- package/src/compaction/compaction.ts +127 -31
- package/src/compaction/entries.ts +2 -2
- package/src/compaction/openai.ts +63 -28
- package/src/image-tokens.ts +77 -0
- package/src/tokenizer.ts +24 -17
package/src/tokenizer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { materializeString, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
6
6
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
7
7
|
import { isEstimateCacheable, messageEstimateVersion } from "./compaction/message-cache";
|
|
8
|
+
import { base64ImageSize, estimateImageContentTokens } from "./image-tokens";
|
|
8
9
|
import type { AgentMessage } from "./types";
|
|
9
10
|
|
|
10
11
|
const testEnv = Bun.env.NODE_ENV === "test";
|
|
@@ -131,12 +132,6 @@ export interface TokenBudgetCheck {
|
|
|
131
132
|
exact: boolean;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/**
|
|
135
|
-
* Image content has no tokenizer representation; charge a fixed estimate
|
|
136
|
-
* matching what providers typically bill for inline images.
|
|
137
|
-
*/
|
|
138
|
-
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
139
|
-
|
|
140
135
|
/**
|
|
141
136
|
* Memoized estimates for one message under this tokenizer's encoding, split by
|
|
142
137
|
* the {@link MessageCountOptions.excludeEncryptedReasoning} option so the two
|
|
@@ -154,13 +149,15 @@ interface MessageEstimate {
|
|
|
154
149
|
* Model-aware local token counter. Immutable: the catalog-resolved encoding
|
|
155
150
|
* is fixed at construction, so a cached count can never straddle two
|
|
156
151
|
* encodings. An `Agent` owns one for its active model (swapping the instance
|
|
157
|
-
* when the model's encoding changes); one-shot
|
|
158
|
-
* the model that will be billed. Known
|
|
159
|
-
* counts; unknown models keep the fast
|
|
160
|
-
* `PI_TOKENIZER_ACCURATE=1`).
|
|
152
|
+
* when the model's encoding or snapcompact frame pricing changes); one-shot
|
|
153
|
+
* flows construct their own for the model that will be billed. Known
|
|
154
|
+
* tokenizer families use exact native counts; unknown models keep the fast
|
|
155
|
+
* byte estimate (or o200k when `PI_TOKENIZER_ACCURATE=1`).
|
|
161
156
|
*/
|
|
162
157
|
export class Tokenizer {
|
|
163
158
|
readonly #encoding: natives.Encoding | null;
|
|
159
|
+
readonly #frameBilling: snapcompact.FrameBilling;
|
|
160
|
+
readonly frameBillingKey: string;
|
|
164
161
|
|
|
165
162
|
/** Exact counts only; byte fallbacks remain mode-dependent and uncached. */
|
|
166
163
|
readonly #nativeCounts = new LRUCache<string, number>({
|
|
@@ -179,8 +176,10 @@ export class Tokenizer {
|
|
|
179
176
|
*/
|
|
180
177
|
#estimates = new WeakMap<AgentMessage, MessageEstimate>();
|
|
181
178
|
|
|
182
|
-
constructor(model?: Pick<Model, "tokenizer"> | null) {
|
|
179
|
+
constructor(model?: (Pick<Model, "tokenizer"> & snapcompact.ShapeTarget) | null) {
|
|
183
180
|
this.#encoding = tokenizerEncodingForModel(model);
|
|
181
|
+
this.#frameBilling = snapcompact.frameBilling(model ?? undefined);
|
|
182
|
+
this.frameBillingKey = snapcompact.frameBillingKey(this.#frameBilling);
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
get encoding(): natives.Encoding | null {
|
|
@@ -219,7 +218,8 @@ export class Tokenizer {
|
|
|
219
218
|
* Settled historical messages are counted once and reused until an owner
|
|
220
219
|
* (prune/shake/strip-images) calls `invalidateMessageCache`; streaming
|
|
221
220
|
* assistants bypass the memo entirely (see the message-cache settle-gate
|
|
222
|
-
* invariant). Image blocks charge
|
|
221
|
+
* invariant). Image blocks charge {@link estimateImageContentTokens}, the
|
|
222
|
+
* same dimension-based estimate the native remote-compaction probe applies.
|
|
223
223
|
*/
|
|
224
224
|
countMessage(message: AgentMessage, options?: MessageCountOptions): number {
|
|
225
225
|
const floored = options?.excludeEncryptedReasoning === true;
|
|
@@ -273,7 +273,7 @@ export class Tokenizer {
|
|
|
273
273
|
if (block.type === "text" && block.text) {
|
|
274
274
|
fragments.push(block.text);
|
|
275
275
|
} else if (block.type === "image") {
|
|
276
|
-
extra +=
|
|
276
|
+
extra += estimateImageContentTokens(block);
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -323,7 +323,7 @@ export class Tokenizer {
|
|
|
323
323
|
if (block.type === "text" && block.text) {
|
|
324
324
|
fragments.push(block.text);
|
|
325
325
|
} else if (block.type === "image") {
|
|
326
|
-
extra +=
|
|
326
|
+
extra += estimateImageContentTokens(block);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
}
|
|
@@ -336,11 +336,12 @@ export class Tokenizer {
|
|
|
336
336
|
if (message.blocks) {
|
|
337
337
|
for (const block of message.blocks) {
|
|
338
338
|
if (block.type === "text") fragments.push(block.text);
|
|
339
|
-
else extra +=
|
|
339
|
+
else extra += this.#frameTokens(block.data);
|
|
340
340
|
}
|
|
341
341
|
} else if (message.images) {
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
for (const image of message.images) {
|
|
343
|
+
extra += this.#frameTokens(image.data);
|
|
344
|
+
}
|
|
344
345
|
}
|
|
345
346
|
}
|
|
346
347
|
break;
|
|
@@ -352,4 +353,10 @@ export class Tokenizer {
|
|
|
352
353
|
if (fragments.length === 0) return extra;
|
|
353
354
|
return extra + this.countTokens(fragments);
|
|
354
355
|
}
|
|
356
|
+
|
|
357
|
+
/** One snapcompact frame at the active model's price for its pixel size; unreadable frames cost the ceiling. */
|
|
358
|
+
#frameTokens(data: string): number {
|
|
359
|
+
const size = base64ImageSize(data);
|
|
360
|
+
return size ? snapcompact.frameTokens(this.#frameBilling, size) : snapcompact.FRAME_TOKEN_ESTIMATE;
|
|
361
|
+
}
|
|
355
362
|
}
|