@qilin-agent/token-meter 0.0.0-stage → 3.1.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/LICENSE +21 -0
- package/README.i18n.yaml +64 -0
- package/README.md +154 -2
- package/README.zh.md +155 -0
- package/lib/index.js +795 -0
- package/lib/types/breakdown-projection.d.ts +98 -0
- package/lib/types/breakdown-projection.js +68 -0
- package/lib/types/client.d.ts +9 -0
- package/lib/types/client.js +7 -0
- package/lib/types/estimate.d.ts +50 -0
- package/lib/types/estimate.js +95 -0
- package/lib/types/index.d.ts +72 -0
- package/lib/types/index.js +257 -0
- package/lib/types/projection.d.ts +74 -0
- package/lib/types/projection.js +7 -0
- package/lib/types/route-pricing.d.ts +31 -0
- package/lib/types/route-pricing.js +56 -0
- package/lib/types/surface-fold.d.ts +68 -0
- package/lib/types/surface-fold.js +98 -0
- package/lib/types/surface-projection.d.ts +58 -0
- package/lib/types/surface-projection.js +68 -0
- package/lib/types/turn-usage.d.ts +33 -0
- package/lib/types/turn-usage.js +229 -0
- package/lib/types/types.d.ts +56 -0
- package/lib/types/types.js +7 -0
- package/lib/types/usage-projection.d.ts +223 -0
- package/lib/types/usage-projection.js +189 -0
- package/package.json +61 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { Service } from "@qilin-agent/kylin";
|
|
2
|
+
import z from "@qilin-agent/schemastery";
|
|
3
|
+
import { assembleAssistantStream, lastAssistantStreamChunk } from "@qilin-agent/llm";
|
|
4
|
+
import { deepFreeze } from "@qilin-agent/util-values";
|
|
5
|
+
import { SessionLogOffset, SessionSeq, canonicalHeader, deriveEventMessage, headerEquals, isSurfaceEvent } from "@qilin-agent/session";
|
|
6
|
+
import { z as z$1 } from "zod";
|
|
7
|
+
//#region lib/types/estimate.js
|
|
8
|
+
/**
|
|
9
|
+
* Fixed-density heuristic token pricing shared by the meter service and the
|
|
10
|
+
* pure context-breakdown projection, so both surfaces price identical content
|
|
11
|
+
* to identical numbers.
|
|
12
|
+
*
|
|
13
|
+
* @module @qilin-agent/token-meter/estimate
|
|
14
|
+
*/
|
|
15
|
+
/** Fixed text-density estimate used until exact tokenization is needed. */
|
|
16
|
+
const CHARS_PER_TOKEN = 4;
|
|
17
|
+
/** Per-block structural overhead for JSON framing and type tags. */
|
|
18
|
+
const BLOCK_OVERHEAD = 4;
|
|
19
|
+
/**
|
|
20
|
+
* Structural JSON price of one block outside the typed pricing arms: the
|
|
21
|
+
* fixed heuristic for merge-extended blocks and for image references, whose
|
|
22
|
+
* request price is route-owned rather than fixed. Image offload marks do not
|
|
23
|
+
* change this reference-only heuristic; route pricing owns their placeholders.
|
|
24
|
+
* @param block - block to price without mutation.
|
|
25
|
+
* @returns heuristic tokens for the block's JSON structure.
|
|
26
|
+
*/
|
|
27
|
+
function estimateStructuralBlock(block) {
|
|
28
|
+
if (block.type === "image") {
|
|
29
|
+
const { offloaded: _offloaded, ...reference } = block;
|
|
30
|
+
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(reference).length / CHARS_PER_TOKEN);
|
|
31
|
+
}
|
|
32
|
+
return BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Price content blocks recursively under the fixed density heuristic.
|
|
36
|
+
* @param blocks - content blocks to price without mutation.
|
|
37
|
+
* @returns heuristic tokens including per-block structural overhead.
|
|
38
|
+
*/
|
|
39
|
+
function estimateContent(blocks) {
|
|
40
|
+
let tokens = 0;
|
|
41
|
+
for (const block of blocks) switch (block.type) {
|
|
42
|
+
case "text":
|
|
43
|
+
case "reasoning":
|
|
44
|
+
tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
45
|
+
break;
|
|
46
|
+
case "tool-call":
|
|
47
|
+
tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN) + Math.ceil(block.arguments.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
48
|
+
break;
|
|
49
|
+
default: tokens += estimateStructuralBlock(block);
|
|
50
|
+
}
|
|
51
|
+
return tokens;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Price the rendered system prompt: the `system/message` surface node's text.
|
|
55
|
+
* Adapters serialize the prompt as a plain string — a system-role message or
|
|
56
|
+
* the request's system field — not as a typed content block, so the price is
|
|
57
|
+
* text density plus role framing with no per-block overhead.
|
|
58
|
+
* @param message - system-role message to price without mutation.
|
|
59
|
+
* @returns heuristic system-prompt tokens; 0 for empty content ("no system prompt").
|
|
60
|
+
*/
|
|
61
|
+
function estimateSystemMessage(message) {
|
|
62
|
+
if (message.content.length === 0) return 0;
|
|
63
|
+
let characters = 0;
|
|
64
|
+
for (const block of message.content) characters += block.type === "text" ? block.text.length : JSON.stringify(block).length;
|
|
65
|
+
return Math.ceil(characters / CHARS_PER_TOKEN) + 4;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Heuristically price one model-visible message.
|
|
69
|
+
* @param message - message to price without mutation.
|
|
70
|
+
* @returns content and role-framing tokens under the fixed heuristic; a
|
|
71
|
+
* system-role message prices as {@link estimateSystemMessage}.
|
|
72
|
+
*/
|
|
73
|
+
function estimateMessage(message) {
|
|
74
|
+
if (message.role === "system") return estimateSystemMessage(message);
|
|
75
|
+
return estimateContent(message.content) + 4;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Price the tool-schema part of a canonical request envelope — the envelope's
|
|
79
|
+
* only priced field, since the system prompt is a surface node.
|
|
80
|
+
* @param header - canonical envelope, or undefined before any request.
|
|
81
|
+
* @returns heuristic tool-schema tokens; 0 when absent or empty.
|
|
82
|
+
*/
|
|
83
|
+
function estimateToolsTokens(header) {
|
|
84
|
+
if (header?.tools === void 0 || header.tools.length === 0) return 0;
|
|
85
|
+
return Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region lib/types/surface-fold.js
|
|
89
|
+
/**
|
|
90
|
+
* Positional pricing shared by measurement and the context-breakdown fold:
|
|
91
|
+
* measurement retains attachment details for route pricing; breakdown keeps
|
|
92
|
+
* only retained node identities, heuristic prices, and system classification.
|
|
93
|
+
* The occupancy projection uses the scalar shadow-price protocol instead.
|
|
94
|
+
*
|
|
95
|
+
* The fold is a plan/commit pair: {@link planSurfaceTokens} runs every
|
|
96
|
+
* fallible step read-only and {@link commitSurfaceTokens} mutates in place,
|
|
97
|
+
* so a throw leaves the caller's state untouched and the same malformed
|
|
98
|
+
* event fails identically on every retry.
|
|
99
|
+
* Nodes also carry durable attachment occurrences and their structural prices,
|
|
100
|
+
* so `measure()` can price the request representation sent to the model.
|
|
101
|
+
*
|
|
102
|
+
* @module @qilin-agent/token-meter/surface-fold
|
|
103
|
+
*/
|
|
104
|
+
/** Collect projected attachment occurrences and their structural prices. */
|
|
105
|
+
function collectProjectedAttachments(blocks, images, files) {
|
|
106
|
+
let imageTokens = 0;
|
|
107
|
+
let fileTokens = 0;
|
|
108
|
+
for (const block of blocks) if (block.type === "image") {
|
|
109
|
+
images.push(block);
|
|
110
|
+
imageTokens += estimateStructuralBlock(block);
|
|
111
|
+
} else if (block.type === "file") {
|
|
112
|
+
files.push(block.attachment);
|
|
113
|
+
fileTokens += estimateStructuralBlock(block);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
imageTokens,
|
|
117
|
+
fileTokens
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Build one priced node from a surface event's derived message. */
|
|
121
|
+
function analyzeNode(seq, message) {
|
|
122
|
+
if (message === null) return {
|
|
123
|
+
seq,
|
|
124
|
+
heuristicTokens: 0,
|
|
125
|
+
imageStructuralTokens: 0,
|
|
126
|
+
fileStructuralTokens: 0,
|
|
127
|
+
images: [],
|
|
128
|
+
files: []
|
|
129
|
+
};
|
|
130
|
+
const heuristicTokens = estimateMessage(message);
|
|
131
|
+
const images = [];
|
|
132
|
+
const files = [];
|
|
133
|
+
const structural = collectProjectedAttachments(message.content, images, files);
|
|
134
|
+
return {
|
|
135
|
+
seq,
|
|
136
|
+
heuristicTokens,
|
|
137
|
+
imageStructuralTokens: structural.imageTokens,
|
|
138
|
+
fileStructuralTokens: structural.fileTokens,
|
|
139
|
+
images,
|
|
140
|
+
files
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate and price one surface event without mutating the surface.
|
|
145
|
+
* @param nodes - the priced surface preceding this event, in model-visible order.
|
|
146
|
+
* @param event - the surface event to place.
|
|
147
|
+
* @returns the plan for {@link commitSurfaceTokens}.
|
|
148
|
+
* @throws when a replacement names a range absent from `nodes` — committed
|
|
149
|
+
* logs are surface-validated at append time, so an unresolvable range is log
|
|
150
|
+
* corruption and must fail loud rather than skip the event.
|
|
151
|
+
*/
|
|
152
|
+
function planSurfaceTokens(nodes, event) {
|
|
153
|
+
const node = analyzeNode(event.seq, deriveEventMessage(event));
|
|
154
|
+
const tokens = node.heuristicTokens;
|
|
155
|
+
const op = event.surfaceOp;
|
|
156
|
+
if (op === "append") return {
|
|
157
|
+
tokens,
|
|
158
|
+
deltaTokens: tokens,
|
|
159
|
+
node,
|
|
160
|
+
target: "append"
|
|
161
|
+
};
|
|
162
|
+
const startIdx = nodes.findIndex((candidate) => candidate.seq === op.startSeq);
|
|
163
|
+
const endIdx = nodes.findIndex((candidate) => candidate.seq === op.endSeq);
|
|
164
|
+
if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) throw new Error(`token surface: replace at seq ${event.seq} has invalid current range ${op.startSeq}-${op.endSeq}`);
|
|
165
|
+
return {
|
|
166
|
+
tokens,
|
|
167
|
+
deltaTokens: tokens - nodes.slice(startIdx, endIdx + 1).reduce((total, candidate) => total + candidate.heuristicTokens, 0),
|
|
168
|
+
node,
|
|
169
|
+
target: {
|
|
170
|
+
startIdx,
|
|
171
|
+
endIdx
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Apply one validated plan to the priced surface in place; infallible, so it
|
|
177
|
+
* cannot leave a half-applied surface behind.
|
|
178
|
+
* @param nodes - the exact priced surface the plan was built against.
|
|
179
|
+
* @param plan - the transition returned by {@link planSurfaceTokens}.
|
|
180
|
+
*/
|
|
181
|
+
function commitSurfaceTokens(nodes, plan) {
|
|
182
|
+
if (plan.target === "append") {
|
|
183
|
+
nodes.push(plan.node);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
nodes.splice(plan.target.startIdx, plan.target.endIdx - plan.target.startIdx + 1, plan.node);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region lib/types/breakdown-projection.js
|
|
190
|
+
/**
|
|
191
|
+
* Heuristic composition of the current retained surface, independent of route
|
|
192
|
+
* image pricing and provider usage. Positional entries preserve system-prompt
|
|
193
|
+
* classification across replacements without retaining historical messages.
|
|
194
|
+
*/
|
|
195
|
+
const tokenCount = z$1.number().int().nonnegative();
|
|
196
|
+
const sessionSeq = z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq);
|
|
197
|
+
const breakdownSchema = z$1.object({
|
|
198
|
+
systemTokens: tokenCount,
|
|
199
|
+
toolsTokens: tokenCount,
|
|
200
|
+
messageTokens: tokenCount
|
|
201
|
+
}).strict();
|
|
202
|
+
/**
|
|
203
|
+
* Context composition with the last nonempty surviving system in surface
|
|
204
|
+
* order classified as system tokens; all other visible prices are messages.
|
|
205
|
+
* Replacements use the measurement planner, not shadow-price claims. State
|
|
206
|
+
* and surface transitions cost O(current retained surface), not O(log length).
|
|
207
|
+
* Tools are priced from the latest request header. No route pricing applies.
|
|
208
|
+
*/
|
|
209
|
+
const contextBreakdownProjectionDefinition = {
|
|
210
|
+
key: "contextBreakdown",
|
|
211
|
+
stateVersion: 5,
|
|
212
|
+
stateSchema: z$1.object({
|
|
213
|
+
nodes: z$1.array(z$1.object({
|
|
214
|
+
seq: sessionSeq,
|
|
215
|
+
heuristicTokens: tokenCount,
|
|
216
|
+
system: z$1.boolean()
|
|
217
|
+
}).strict()),
|
|
218
|
+
breakdown: breakdownSchema
|
|
219
|
+
}).strict(),
|
|
220
|
+
init: () => ({
|
|
221
|
+
nodes: [],
|
|
222
|
+
breakdown: {
|
|
223
|
+
systemTokens: 0,
|
|
224
|
+
toolsTokens: 0,
|
|
225
|
+
messageTokens: 0
|
|
226
|
+
}
|
|
227
|
+
}),
|
|
228
|
+
apply: (state, event) => {
|
|
229
|
+
if (event.type === "request/header") {
|
|
230
|
+
const toolsTokens = estimateToolsTokens(canonicalHeader(event.data.header));
|
|
231
|
+
return toolsTokens === state.breakdown.toolsTokens ? state : {
|
|
232
|
+
...state,
|
|
233
|
+
breakdown: {
|
|
234
|
+
...state.breakdown,
|
|
235
|
+
toolsTokens
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (!isSurfaceEvent(event)) return state;
|
|
240
|
+
const plan = planSurfaceTokens(state.nodes, event);
|
|
241
|
+
const nodes = [...state.nodes];
|
|
242
|
+
commitSurfaceTokens(nodes, {
|
|
243
|
+
...plan,
|
|
244
|
+
node: {
|
|
245
|
+
seq: event.seq,
|
|
246
|
+
heuristicTokens: plan.tokens,
|
|
247
|
+
system: event.type === "system/message"
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const systemTokens = nodes.findLast((node) => node.system && node.heuristicTokens > 0)?.heuristicTokens ?? 0;
|
|
251
|
+
const messageTokens = state.breakdown.systemTokens + state.breakdown.messageTokens + plan.deltaTokens - systemTokens;
|
|
252
|
+
return {
|
|
253
|
+
nodes,
|
|
254
|
+
breakdown: systemTokens === state.breakdown.systemTokens && messageTokens === state.breakdown.messageTokens ? state.breakdown : {
|
|
255
|
+
systemTokens,
|
|
256
|
+
toolsTokens: state.breakdown.toolsTokens,
|
|
257
|
+
messageTokens
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
wire: {
|
|
262
|
+
viewSchema: breakdownSchema,
|
|
263
|
+
view: (state) => state.breakdown
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region lib/types/surface-projection.js
|
|
268
|
+
/**
|
|
269
|
+
* The context-pressure projection's O(1) surface-token fold.
|
|
270
|
+
*
|
|
271
|
+
* Its checkpoint keeps scalar totals rather than the retained surface used
|
|
272
|
+
* by context breakdown. Replacements ride the compact
|
|
273
|
+
* seam's shadow-price protocol: the metering event immediately before a
|
|
274
|
+
* surface `replace` (`compaction/summary` or `compaction/prune`) states the
|
|
275
|
+
* heuristic price of the exact replaced range, so the fold keeps a running
|
|
276
|
+
* total plus at most one pending claim and never retains per-node prices.
|
|
277
|
+
* The counts are exact by construction: producers derive them from the same
|
|
278
|
+
* fixed estimator this module prices appends with. A replacement without an
|
|
279
|
+
* armed claim folds with zero delta because bounded state cannot reconstruct
|
|
280
|
+
* the replaced range; this preserves replay at the cost of possible drift.
|
|
281
|
+
*
|
|
282
|
+
* @module @qilin-agent/token-meter/surface-projection
|
|
283
|
+
*/
|
|
284
|
+
/**
|
|
285
|
+
* Fold one committed event onto a running surface-token total.
|
|
286
|
+
*
|
|
287
|
+
* A shadow-price event arms a claim; any other event expires it, and a
|
|
288
|
+
* surface `replace` consumes the claim naming its exact range — the
|
|
289
|
+
* producers append the metering event and the replacement synchronously
|
|
290
|
+
* adjacent, so a surviving claim always prices the very next event.
|
|
291
|
+
* A replace with no claim folds with zero delta because the bounded state
|
|
292
|
+
* cannot reconstruct the replaced range. An armed claim for another range
|
|
293
|
+
* still fails because the adjacent events contradict each other.
|
|
294
|
+
* @param claim - the claim armed by the immediately preceding event, if any.
|
|
295
|
+
* @param event - the next committed session event.
|
|
296
|
+
* @returns the signed token delta and the claim state after this event.
|
|
297
|
+
* @throws when a replacement arrives with an armed claim for a different
|
|
298
|
+
* range — the metering event was adjacent, so this is a live producer's
|
|
299
|
+
* shadow-price contract violation, not historical data, and must fail
|
|
300
|
+
* loud rather than let the total drift.
|
|
301
|
+
*/
|
|
302
|
+
function foldSurfaceProjection(claim, event) {
|
|
303
|
+
if (event.type === "compaction/summary" || event.type === "compaction/prune") {
|
|
304
|
+
const { shadowedRange, shadowedTokenCount } = event.data;
|
|
305
|
+
return {
|
|
306
|
+
deltaTokens: 0,
|
|
307
|
+
claim: {
|
|
308
|
+
start: SessionSeq(shadowedRange.start),
|
|
309
|
+
end: SessionSeq(shadowedRange.end),
|
|
310
|
+
tokens: shadowedTokenCount
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (!isSurfaceEvent(event)) return {
|
|
315
|
+
deltaTokens: 0,
|
|
316
|
+
claim: void 0
|
|
317
|
+
};
|
|
318
|
+
const message = deriveEventMessage(event);
|
|
319
|
+
const tokens = message === null ? 0 : estimateMessage(message);
|
|
320
|
+
const op = event.surfaceOp;
|
|
321
|
+
if (op === "append") return {
|
|
322
|
+
deltaTokens: tokens,
|
|
323
|
+
claim: void 0
|
|
324
|
+
};
|
|
325
|
+
if (claim === void 0) return {
|
|
326
|
+
deltaTokens: 0,
|
|
327
|
+
claim: void 0
|
|
328
|
+
};
|
|
329
|
+
if (claim.start !== op.startSeq || claim.end !== op.endSeq) throw new Error(`token surface: replace at seq ${event.seq} over range ${op.startSeq}-${op.endSeq} has no adjacent shadow price (armed claim covers ${claim.start}-${claim.end})`);
|
|
330
|
+
return {
|
|
331
|
+
deltaTokens: tokens - claim.tokens,
|
|
332
|
+
claim: void 0
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region lib/types/usage-projection.js
|
|
337
|
+
/**
|
|
338
|
+
* Pure folds for durable provider-reported token usage and context occupancy.
|
|
339
|
+
*/
|
|
340
|
+
const zeroBuckets = () => ({
|
|
341
|
+
uncachedInputTokens: 0,
|
|
342
|
+
outputTokens: 0,
|
|
343
|
+
cacheReadTokens: 0,
|
|
344
|
+
cacheWriteTokens: 0
|
|
345
|
+
});
|
|
346
|
+
const bucketsFrom = (usage) => ({
|
|
347
|
+
uncachedInputTokens: usage.inputTokens,
|
|
348
|
+
outputTokens: usage.outputTokens,
|
|
349
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
350
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0
|
|
351
|
+
});
|
|
352
|
+
const bucketsEqual = (left, right) => left.uncachedInputTokens === right.uncachedInputTokens && left.outputTokens === right.outputTokens && left.cacheReadTokens === right.cacheReadTokens && left.cacheWriteTokens === right.cacheWriteTokens;
|
|
353
|
+
const addReplacing = (totals, previous, next) => ({
|
|
354
|
+
uncachedInputTokens: totals.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) + next.uncachedInputTokens,
|
|
355
|
+
outputTokens: totals.outputTokens - (previous?.outputTokens ?? 0) + next.outputTokens,
|
|
356
|
+
cacheReadTokens: totals.cacheReadTokens - (previous?.cacheReadTokens ?? 0) + next.cacheReadTokens,
|
|
357
|
+
cacheWriteTokens: totals.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) + next.cacheWriteTokens
|
|
358
|
+
});
|
|
359
|
+
const projectionSchema = z$1.object({
|
|
360
|
+
uncachedInputTokens: z$1.number().int().nonnegative(),
|
|
361
|
+
outputTokens: z$1.number().int().nonnegative(),
|
|
362
|
+
cacheReadTokens: z$1.number().int().nonnegative(),
|
|
363
|
+
cacheWriteTokens: z$1.number().int().nonnegative()
|
|
364
|
+
}).strict();
|
|
365
|
+
/**
|
|
366
|
+
* The token-usage unit's state schema — the one definition of the state
|
|
367
|
+
* shape; the state type is inferred from it.
|
|
368
|
+
*/
|
|
369
|
+
const tokenUsageStateSchema = z$1.object({
|
|
370
|
+
totals: projectionSchema,
|
|
371
|
+
last: z$1.object({
|
|
372
|
+
turn: z$1.number().int().nonnegative(),
|
|
373
|
+
step: z$1.number().int().nonnegative(),
|
|
374
|
+
buckets: projectionSchema
|
|
375
|
+
}).nullable()
|
|
376
|
+
}).strict();
|
|
377
|
+
const pressureSchema = z$1.object({
|
|
378
|
+
pressureTokens: z$1.number().int().nonnegative().optional(),
|
|
379
|
+
projectedTokens: z$1.number().int().nonnegative().optional(),
|
|
380
|
+
contextWindow: z$1.number().int().positive().optional()
|
|
381
|
+
}).strict().transform(({ pressureTokens, projectedTokens, contextWindow }) => ({
|
|
382
|
+
...pressureTokens === void 0 ? {} : { pressureTokens },
|
|
383
|
+
...projectedTokens === void 0 ? {} : { projectedTokens },
|
|
384
|
+
...contextWindow === void 0 ? {} : { contextWindow }
|
|
385
|
+
}));
|
|
386
|
+
/** Prompt-side pressure of one request: input plus cache traffic, no output. */
|
|
387
|
+
const pressureFrom = (usage) => usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
388
|
+
/** The usage one durable Assistant settlement reports for its attempt, if any. */
|
|
389
|
+
function usageOf(event) {
|
|
390
|
+
if (event.type === "assistant/message" && event.data.usage !== void 0) return event.data.usage;
|
|
391
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") return void 0;
|
|
392
|
+
return lastAssistantStreamChunk(event.data.stream, "usage")?.usage;
|
|
393
|
+
}
|
|
394
|
+
/** The context-pressure state schema and source of its inferred type. */
|
|
395
|
+
const contextPressureStateSchema = z$1.object({
|
|
396
|
+
contextWindow: z$1.number().int().positive().optional(),
|
|
397
|
+
pressureTokens: z$1.number().int().nonnegative().optional(),
|
|
398
|
+
surfaceTokens: z$1.number().int().nonnegative(),
|
|
399
|
+
sampledSurfaceTokens: z$1.number().int().nonnegative().optional(),
|
|
400
|
+
claim: z$1.object({
|
|
401
|
+
start: z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
402
|
+
end: z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
403
|
+
tokens: z$1.number().int().nonnegative()
|
|
404
|
+
}).optional()
|
|
405
|
+
}).strict();
|
|
406
|
+
/**
|
|
407
|
+
* Token-meter's session projection unit.
|
|
408
|
+
*
|
|
409
|
+
* Each v2 Assistant settlement contributes the last usage sample embedded in
|
|
410
|
+
* its stream. `llm/retry-started` closes the replacement slot so the retried
|
|
411
|
+
* attempt adds to the total.
|
|
412
|
+
*/
|
|
413
|
+
const tokenUsageProjectionDefinition = {
|
|
414
|
+
key: "tokenUsage",
|
|
415
|
+
stateVersion: 2,
|
|
416
|
+
stateSchema: tokenUsageStateSchema,
|
|
417
|
+
init: () => ({
|
|
418
|
+
totals: zeroBuckets(),
|
|
419
|
+
last: null
|
|
420
|
+
}),
|
|
421
|
+
apply: (state, event) => {
|
|
422
|
+
if (event.type === "llm/retry-started") return state.last?.turn === event.data.turn && state.last.step === event.data.step ? {
|
|
423
|
+
...state,
|
|
424
|
+
last: null
|
|
425
|
+
} : state;
|
|
426
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") return state;
|
|
427
|
+
const sample = usageOf(event);
|
|
428
|
+
if (sample === void 0) return state;
|
|
429
|
+
const { turn, step } = event.data;
|
|
430
|
+
const buckets = bucketsFrom(sample);
|
|
431
|
+
const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last.buckets : void 0;
|
|
432
|
+
if (previous !== void 0 && bucketsEqual(previous, buckets)) return state;
|
|
433
|
+
return {
|
|
434
|
+
totals: addReplacing(state.totals, previous, buckets),
|
|
435
|
+
last: {
|
|
436
|
+
turn,
|
|
437
|
+
step,
|
|
438
|
+
buckets
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
wire: {
|
|
443
|
+
viewSchema: projectionSchema,
|
|
444
|
+
view: (state) => state.totals
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Token-meter's context-occupancy projection unit.
|
|
449
|
+
*
|
|
450
|
+
* Independent last-wins slots: the newest usage sample supplies the provider
|
|
451
|
+
* numerator, the newest `request/context` record the denominator. Both are
|
|
452
|
+
* whole values, so replay order alone decides the result and no cross-field
|
|
453
|
+
* consistency is claimed — the pair is explicitly not one atomic request
|
|
454
|
+
* observation (see {@link ContextPressureProjection}).
|
|
455
|
+
*
|
|
456
|
+
* `pressureTokens` is prompt-side only, so it holds still while a turn streams
|
|
457
|
+
* and steps forward once the next request reports its usage. Because nothing
|
|
458
|
+
* but a request reports usage, it also cannot see a compaction: the fold
|
|
459
|
+
* therefore carries a running surface total alongside it and publishes
|
|
460
|
+
* `projectedTokens` — the sample plus the surface's signed movement since it
|
|
461
|
+
* was taken — so occupancy answers for the next request rather than the last
|
|
462
|
+
* one. The total rides {@link foldSurfaceProjection}, so the state stays O(1)
|
|
463
|
+
* and a replacement shrinks it by its logged shadow price. A replacement
|
|
464
|
+
* without a claim preserves the previous total. A usage sample is stamped
|
|
465
|
+
* BEFORE the same event joins the surface, so an `assistant/message` anchors
|
|
466
|
+
* against the surface its own request saw.
|
|
467
|
+
*/
|
|
468
|
+
const contextPressureProjectionDefinition = {
|
|
469
|
+
key: "contextPressure",
|
|
470
|
+
stateVersion: 5,
|
|
471
|
+
stateSchema: contextPressureStateSchema,
|
|
472
|
+
init: () => ({ surfaceTokens: 0 }),
|
|
473
|
+
apply: (state, event) => {
|
|
474
|
+
const fold = foldSurfaceProjection(state.claim, event);
|
|
475
|
+
let next = state;
|
|
476
|
+
if (event.type === "request/context") {
|
|
477
|
+
const contextWindow = event.data.contextWindow;
|
|
478
|
+
if (contextWindow !== state.contextWindow) if (contextWindow !== void 0) next = {
|
|
479
|
+
...next,
|
|
480
|
+
contextWindow
|
|
481
|
+
};
|
|
482
|
+
else {
|
|
483
|
+
const { contextWindow: _removed, ...withoutContextWindow } = next;
|
|
484
|
+
next = withoutContextWindow;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const usage = usageOf(event);
|
|
488
|
+
if (usage !== void 0) {
|
|
489
|
+
const pressureTokens = pressureFrom(usage);
|
|
490
|
+
if (pressureTokens !== next.pressureTokens || next.sampledSurfaceTokens !== next.surfaceTokens) next = {
|
|
491
|
+
...next,
|
|
492
|
+
pressureTokens,
|
|
493
|
+
sampledSurfaceTokens: next.surfaceTokens
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (fold.deltaTokens !== 0) next = {
|
|
497
|
+
...next,
|
|
498
|
+
surfaceTokens: next.surfaceTokens + fold.deltaTokens
|
|
499
|
+
};
|
|
500
|
+
if (state.claim === void 0 && fold.claim === void 0) return next;
|
|
501
|
+
const { claim: _expired, ...withoutClaim } = next;
|
|
502
|
+
return fold.claim === void 0 ? withoutClaim : {
|
|
503
|
+
...withoutClaim,
|
|
504
|
+
claim: fold.claim
|
|
505
|
+
};
|
|
506
|
+
},
|
|
507
|
+
wire: {
|
|
508
|
+
viewSchema: pressureSchema,
|
|
509
|
+
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
|
|
510
|
+
...contextWindow === void 0 ? {} : { contextWindow },
|
|
511
|
+
...pressureTokens === void 0 ? {} : { pressureTokens },
|
|
512
|
+
...pressureTokens === void 0 || sampledSurfaceTokens === void 0 ? {} : { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) }
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region lib/types/route-pricing.js
|
|
518
|
+
/**
|
|
519
|
+
* Request-projected surface pricing: replaces attachment-block heuristics with
|
|
520
|
+
* the image and file representations sent to the routed model.
|
|
521
|
+
*
|
|
522
|
+
* @module @qilin-agent/token-meter/route-pricing
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* Price one ordered surface under its model-request attachment projection.
|
|
526
|
+
* @param nodes - the fold's current or snapshotted surface, in model-visible order.
|
|
527
|
+
* @param pricing - the routed model's image pricing, or undefined to keep the fixed heuristic.
|
|
528
|
+
* @param fileText - exact file handle projection used by the mounted LLM service.
|
|
529
|
+
* @returns detached public nodes and their route-priced total.
|
|
530
|
+
* @throws when the pricing answers a different occurrence count than it was
|
|
531
|
+
* asked — misalignment would silently misprice nodes, so it must fail loud.
|
|
532
|
+
*/
|
|
533
|
+
function priceSurface(nodes, pricing, fileText) {
|
|
534
|
+
const images = pricing === void 0 ? [] : nodes.flatMap((node) => node.images);
|
|
535
|
+
const hasFiles = fileText !== void 0 && nodes.some((node) => node.files.length > 0);
|
|
536
|
+
if ((pricing === void 0 || images.length === 0) && !hasFiles) {
|
|
537
|
+
let surfaceTokens = 0;
|
|
538
|
+
return {
|
|
539
|
+
nodes: nodes.map((node) => {
|
|
540
|
+
surfaceTokens += node.heuristicTokens;
|
|
541
|
+
return {
|
|
542
|
+
seq: node.seq,
|
|
543
|
+
tokens: node.heuristicTokens,
|
|
544
|
+
heuristicTokens: node.heuristicTokens
|
|
545
|
+
};
|
|
546
|
+
}),
|
|
547
|
+
surfaceTokens
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const prices = pricing === void 0 ? [] : pricing.priceImages(images);
|
|
551
|
+
if (pricing !== void 0 && prices.length !== images.length) throw new Error(`token meter: route image pricing answered ${prices.length} prices for ${images.length} occurrences`);
|
|
552
|
+
let cursor = 0;
|
|
553
|
+
let surfaceTokens = 0;
|
|
554
|
+
return {
|
|
555
|
+
nodes: nodes.map((node) => {
|
|
556
|
+
let tokens = node.heuristicTokens;
|
|
557
|
+
if (fileText !== void 0 && node.files.length > 0) {
|
|
558
|
+
tokens -= node.fileStructuralTokens;
|
|
559
|
+
for (const file of node.files) tokens += estimateContent([{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: fileText(file)
|
|
562
|
+
}]);
|
|
563
|
+
}
|
|
564
|
+
if (pricing !== void 0 && node.images.length > 0) {
|
|
565
|
+
tokens -= node.imageStructuralTokens;
|
|
566
|
+
for (let occurrence = 0; occurrence < node.images.length; occurrence += 1) {
|
|
567
|
+
const price = prices[cursor];
|
|
568
|
+
cursor += 1;
|
|
569
|
+
tokens += price.visualTokens + estimateContent([{
|
|
570
|
+
type: "text",
|
|
571
|
+
text: price.text
|
|
572
|
+
}]);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
surfaceTokens += tokens;
|
|
576
|
+
return {
|
|
577
|
+
seq: node.seq,
|
|
578
|
+
tokens,
|
|
579
|
+
heuristicTokens: node.heuristicTokens
|
|
580
|
+
};
|
|
581
|
+
}),
|
|
582
|
+
surfaceTokens
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region lib/types/index.js
|
|
587
|
+
/**
|
|
588
|
+
* Single replay-aware token-meter service for request and surface pressure.
|
|
589
|
+
*
|
|
590
|
+
* @module @qilin-agent/token-meter
|
|
591
|
+
*/
|
|
592
|
+
/** Sum disjoint provider usage buckets without double-counting reasoning output. */
|
|
593
|
+
function usageTokens(usage) {
|
|
594
|
+
return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
|
|
595
|
+
}
|
|
596
|
+
/** Compare optional envelopes so a headerless estimate can track later surface deltas. */
|
|
597
|
+
function optionalHeaderEquals(left, right) {
|
|
598
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
599
|
+
return headerEquals(left, right);
|
|
600
|
+
}
|
|
601
|
+
/** Reject stale or misspelled keys before defaults can hide them. */
|
|
602
|
+
function validateConfigKeys(config) {
|
|
603
|
+
for (const key of Object.keys(config)) throw new Error(`TokenMeterConfig: unknown key "${key}" (no settings are supported)`);
|
|
604
|
+
}
|
|
605
|
+
/** Replay owner for one service-wide estimator and isolated per-session folds. */
|
|
606
|
+
var TokenMeter = class extends Service {
|
|
607
|
+
static Config = z.object({});
|
|
608
|
+
static inject = ["sessionProjections"];
|
|
609
|
+
states = /* @__PURE__ */ new WeakMap();
|
|
610
|
+
constructor(ctx, config = {}) {
|
|
611
|
+
super(ctx, "tokenMeter");
|
|
612
|
+
validateConfigKeys(config);
|
|
613
|
+
ctx.sessionProjections.register(tokenUsageProjectionDefinition);
|
|
614
|
+
ctx.sessionProjections.register(contextPressureProjectionDefinition);
|
|
615
|
+
ctx.sessionProjections.register(contextBreakdownProjectionDefinition);
|
|
616
|
+
ctx.on("session/event", (session) => {
|
|
617
|
+
if (this.states.has(session)) this._sync(session);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Measure current request pressure and surface through the durable tail.
|
|
622
|
+
*
|
|
623
|
+
* The effective envelope's routed provider/model selects the request-image
|
|
624
|
+
* pricing every node is priced under: a route whose adapter declares image
|
|
625
|
+
* pricing charges each retained image its visual tokens plus its
|
|
626
|
+
* model-visible text, while other routes keep the fixed heuristic. Provider
|
|
627
|
+
* usage is reused only when the latest successful call's canonical request
|
|
628
|
+
* envelope matches `requestHeader` and its total is no lower than that
|
|
629
|
+
* call's full route-priced anchor; otherwise the complete envelope and
|
|
630
|
+
* surface are repriced. The anchor includes all surface nodes immediately
|
|
631
|
+
* before the assistant message, including inputs admitted after step/start.
|
|
632
|
+
*
|
|
633
|
+
* `requestHeader` replaces the latest logged envelope for pressure and node
|
|
634
|
+
* pricing; the node set always describes the current session surface. Every
|
|
635
|
+
* call clones those positional nodes, so measurement is O(surface).
|
|
636
|
+
*
|
|
637
|
+
* @param session - session to replay through its current durable tail.
|
|
638
|
+
* @param requestHeader - optional effective request envelope replacing the latest logged header.
|
|
639
|
+
* @returns a detached deeply immutable pressure and surface measurement.
|
|
640
|
+
*/
|
|
641
|
+
measure(session, requestHeader) {
|
|
642
|
+
const state = this._sync(session);
|
|
643
|
+
const header = requestHeader === void 0 ? state.header : canonicalHeader(requestHeader);
|
|
644
|
+
const pricing = this._routeImagePricing(header);
|
|
645
|
+
const fileText = this._fileRequestText();
|
|
646
|
+
const surface = priceSurface(state.surface, pricing, fileText);
|
|
647
|
+
const anchor = state.anchor;
|
|
648
|
+
let baseline;
|
|
649
|
+
let surfaceDeltaTokens;
|
|
650
|
+
if (anchor !== void 0 && optionalHeaderEquals(anchor.header, header)) {
|
|
651
|
+
const anchorSurfaceTokens = priceSurface(anchor.nodes, pricing, fileText).surfaceTokens + anchor.assistantTokens;
|
|
652
|
+
const estimatedAnchorTokens = estimateToolsTokens(header) + anchorSurfaceTokens;
|
|
653
|
+
const usage = anchor.usage;
|
|
654
|
+
baseline = usage !== void 0 && usageTokens(usage) >= estimatedAnchorTokens ? {
|
|
655
|
+
kind: "usage",
|
|
656
|
+
tokens: usageTokens(usage),
|
|
657
|
+
usage
|
|
658
|
+
} : {
|
|
659
|
+
kind: "estimated",
|
|
660
|
+
tokens: estimatedAnchorTokens
|
|
661
|
+
};
|
|
662
|
+
surfaceDeltaTokens = surface.surfaceTokens - anchorSurfaceTokens;
|
|
663
|
+
} else if (header === void 0 && surface.surfaceTokens === 0) {
|
|
664
|
+
baseline = {
|
|
665
|
+
kind: "none",
|
|
666
|
+
tokens: 0
|
|
667
|
+
};
|
|
668
|
+
surfaceDeltaTokens = 0;
|
|
669
|
+
} else {
|
|
670
|
+
baseline = {
|
|
671
|
+
kind: "estimated",
|
|
672
|
+
tokens: estimateToolsTokens(header) + surface.surfaceTokens
|
|
673
|
+
};
|
|
674
|
+
surfaceDeltaTokens = 0;
|
|
675
|
+
}
|
|
676
|
+
return deepFreeze(structuredClone({
|
|
677
|
+
logRevision: state.consumedEvents,
|
|
678
|
+
baseline,
|
|
679
|
+
surfaceDeltaTokens,
|
|
680
|
+
totalTokens: Math.max(0, baseline.tokens + surfaceDeltaTokens),
|
|
681
|
+
surfaceTokens: surface.surfaceTokens,
|
|
682
|
+
nodes: surface.nodes
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
/** Resolve the routed model's image pricing, when the llm service and route declare one. */
|
|
686
|
+
_routeImagePricing(header) {
|
|
687
|
+
const config = header?.config;
|
|
688
|
+
if (config === void 0) return void 0;
|
|
689
|
+
return this.ctx.get("llm")?.imageRequestPricing(config.provider, config.model);
|
|
690
|
+
}
|
|
691
|
+
/** Resolve request-time file projection when an LLM service is mounted. */
|
|
692
|
+
_fileRequestText() {
|
|
693
|
+
const llm = this.ctx.get("llm");
|
|
694
|
+
return llm === void 0 ? void 0 : (ref) => llm.fileRequestText(ref);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Heuristically price one model-visible message (instance face of the pure
|
|
698
|
+
* `estimateMessage` export from `estimate.ts`).
|
|
699
|
+
* @param message - message to price without mutation.
|
|
700
|
+
* @returns content and role-framing tokens under the fixed service heuristic.
|
|
701
|
+
*/
|
|
702
|
+
estimateMessage(message) {
|
|
703
|
+
return estimateMessage(message);
|
|
704
|
+
}
|
|
705
|
+
/** Catch one session's fold up to the current durable tail. */
|
|
706
|
+
_sync(session) {
|
|
707
|
+
let state = this.states.get(session);
|
|
708
|
+
if (state === void 0) {
|
|
709
|
+
state = {
|
|
710
|
+
consumedEvents: SessionLogOffset(0),
|
|
711
|
+
header: void 0,
|
|
712
|
+
surface: [],
|
|
713
|
+
stepStart: void 0,
|
|
714
|
+
anchor: void 0
|
|
715
|
+
};
|
|
716
|
+
this.states.set(session, state);
|
|
717
|
+
}
|
|
718
|
+
while (state.consumedEvents < session.seq) {
|
|
719
|
+
const event = session.eventAt(SessionSeq(state.consumedEvents));
|
|
720
|
+
this._foldEvent(state, event);
|
|
721
|
+
state.consumedEvents = SessionLogOffset(state.consumedEvents + 1);
|
|
722
|
+
}
|
|
723
|
+
return state;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Run every fallible step — surface plan and anchor validation — before
|
|
727
|
+
* mutating replay state, so a malformed event remains unread on every
|
|
728
|
+
* retry instead of half-applying.
|
|
729
|
+
*/
|
|
730
|
+
_foldEvent(state, event) {
|
|
731
|
+
let nextHeader = state.header;
|
|
732
|
+
let nextStepStart = state.stepStart;
|
|
733
|
+
let nextAnchor = state.anchor;
|
|
734
|
+
switch (event.type) {
|
|
735
|
+
case "image/offload": {
|
|
736
|
+
const offloaded = new Map(event.data.targets.map((target) => [target.seq, new Set(target.imageIndexes)]));
|
|
737
|
+
state.surface = state.surface.map((node) => {
|
|
738
|
+
const indexes = offloaded.get(node.seq);
|
|
739
|
+
if (indexes === void 0) return node;
|
|
740
|
+
return {
|
|
741
|
+
...node,
|
|
742
|
+
images: node.images.map((image, index) => indexes.has(index) ? {
|
|
743
|
+
...image,
|
|
744
|
+
offloaded: true
|
|
745
|
+
} : image)
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "request/header":
|
|
751
|
+
nextHeader = canonicalHeader(event.data.header);
|
|
752
|
+
break;
|
|
753
|
+
case "step/start":
|
|
754
|
+
if (state.stepStart !== void 0) throw new Error(`token meter: step/start at seq ${event.seq} arrived before turn ${state.stepStart.turn}/step ${state.stepStart.step} ended`);
|
|
755
|
+
nextStepStart = { ...event.data };
|
|
756
|
+
break;
|
|
757
|
+
case "step/end":
|
|
758
|
+
if (state.stepStart === void 0 || state.stepStart.turn !== event.data.turn || state.stepStart.step !== event.data.step) throw new Error(`token meter: step/end at seq ${event.seq} has no matching step/start event`);
|
|
759
|
+
nextStepStart = void 0;
|
|
760
|
+
break;
|
|
761
|
+
default: break;
|
|
762
|
+
}
|
|
763
|
+
const plan = isSurfaceEvent(event) ? planSurfaceTokens(state.surface, event) : void 0;
|
|
764
|
+
if (event.type === "assistant/message") {
|
|
765
|
+
const stepStart = state.stepStart;
|
|
766
|
+
if (stepStart === void 0 || stepStart.turn !== event.data.turn || stepStart.step !== event.data.step) throw new Error(`token meter: assistant/message at seq ${event.seq} has no matching step/start event`);
|
|
767
|
+
const eventTokens = plan.tokens;
|
|
768
|
+
if (event.data.usage !== void 0 && nextHeader !== void 0) nextAnchor = {
|
|
769
|
+
header: nextHeader,
|
|
770
|
+
nodes: [...state.surface],
|
|
771
|
+
assistantTokens: this._estimateProviderAssistant(event),
|
|
772
|
+
usage: event.data.usage
|
|
773
|
+
};
|
|
774
|
+
else nextAnchor = {
|
|
775
|
+
header: nextHeader,
|
|
776
|
+
nodes: [...state.surface],
|
|
777
|
+
assistantTokens: eventTokens,
|
|
778
|
+
usage: void 0
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
state.header = nextHeader;
|
|
782
|
+
state.stepStart = nextStepStart;
|
|
783
|
+
if (plan !== void 0) commitSurfaceTokens(state.surface, plan);
|
|
784
|
+
state.anchor = nextAnchor;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Reassemble provider output from the message's exact embedded stream.
|
|
788
|
+
*/
|
|
789
|
+
_estimateProviderAssistant(event) {
|
|
790
|
+
const providerContent = assembleAssistantStream(event.data.stream).blocks();
|
|
791
|
+
return providerContent.length === 0 ? 0 : estimateContent(providerContent) + 4;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
//#endregion
|
|
795
|
+
export { TokenMeter, TokenMeter as default };
|