@qilin-agent/token-meter 0.0.0-stage → 3.1.4
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 +1010 -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 +73 -0
- package/lib/types/index.js +260 -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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure client-safe token-projection vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* @module @qilin-agent/token-meter/projection
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Durable cumulative provider usage for a complete session log.
|
|
8
|
+
*
|
|
9
|
+
* The four buckets are disjoint. In particular, reasoning tokens are already
|
|
10
|
+
* included in `outputTokens` and are not accumulated again.
|
|
11
|
+
*/
|
|
12
|
+
export interface TokenUsageProjection {
|
|
13
|
+
uncachedInputTokens: number;
|
|
14
|
+
outputTokens: number;
|
|
15
|
+
cacheReadTokens: number;
|
|
16
|
+
cacheWriteTokens: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Approximate context occupancy for a status display.
|
|
20
|
+
*
|
|
21
|
+
* The fields, when present, are deliberately NOT one atomic request
|
|
22
|
+
* observation: each is a last-wins record of a different moment. Switching
|
|
23
|
+
* models can therefore pair a fresh capacity with the previous route's
|
|
24
|
+
* pressure until the next request reports usage. This is an intentional trade
|
|
25
|
+
* — the value is a user-facing reference, not a billing or gating input. See
|
|
26
|
+
* the token-meter README for the full rationale.
|
|
27
|
+
*/
|
|
28
|
+
export interface ContextPressureProjection {
|
|
29
|
+
/**
|
|
30
|
+
* Provider-reported prompt size of the most recent request: uncached input
|
|
31
|
+
* plus cache reads and writes. Response output is excluded, so this does not
|
|
32
|
+
* grow as the current turn streams. Absent until a provider reports usage.
|
|
33
|
+
*/
|
|
34
|
+
pressureTokens?: number;
|
|
35
|
+
/**
|
|
36
|
+
* What the NEXT request's prompt would cost: {@link pressureTokens} plus the
|
|
37
|
+
* heuristic repricing of everything the surface gained or lost since that
|
|
38
|
+
* sample. Only the delta is estimated, so the figure stays anchored to the
|
|
39
|
+
* provider while still reacting the moment a compaction shadows a span —
|
|
40
|
+
* which `pressureTokens` alone cannot do, since compaction reports no usage
|
|
41
|
+
* of its own. Absent until a provider reports usage.
|
|
42
|
+
*/
|
|
43
|
+
projectedTokens?: number;
|
|
44
|
+
/** Newest recorded route capacity; absent when no adapter advertised one. */
|
|
45
|
+
contextWindow?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Heuristic composition of the next request's context: what the prompt is
|
|
49
|
+
* made of, not what it costs. All three figures use the meter's fixed
|
|
50
|
+
* density estimate, so they will not sum to the provider-anchored
|
|
51
|
+
* `projectedTokens`: the estimator systematically underprices CJK text and
|
|
52
|
+
* JSON schemas, which is exactly the error the anchoring in
|
|
53
|
+
* {@link ContextPressureProjection.projectedTokens} keeps out of the occupancy
|
|
54
|
+
* figure. Present these as approximations of composition, never as a total.
|
|
55
|
+
*/
|
|
56
|
+
export interface ContextBreakdownProjection {
|
|
57
|
+
/** Heuristic tokens of the last nonempty surviving system prompt in surface order; 0 when none exists. */
|
|
58
|
+
systemTokens: number;
|
|
59
|
+
/** Heuristic tokens of the newest request envelope's tool schemas; 0 before any request. */
|
|
60
|
+
toolsTokens: number;
|
|
61
|
+
/** Heuristic tokens of every other visible surface node, including superseded system prompts. */
|
|
62
|
+
messageTokens: number;
|
|
63
|
+
}
|
|
64
|
+
declare module '@qilin-agent/session-projection/types' {
|
|
65
|
+
interface SessionProjectionMap {
|
|
66
|
+
/** Provider-reported usage accumulated across the complete durable log. */
|
|
67
|
+
tokenUsage: TokenUsageProjection;
|
|
68
|
+
/** Newest request pressure paired with the newest known route capacity. */
|
|
69
|
+
contextPressure: ContextPressureProjection;
|
|
70
|
+
/** Heuristic system/tools/message composition of the next request. */
|
|
71
|
+
contextBreakdown: ContextBreakdownProjection;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-projected surface pricing: replaces attachment-block heuristics with
|
|
3
|
+
* the image and file representations sent to the routed model.
|
|
4
|
+
*
|
|
5
|
+
* @module @qilin-agent/token-meter/route-pricing
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentBlock, LlmImageRequestPricing } from '@qilin-agent/llm';
|
|
8
|
+
import type { MeterSurfaceNode } from './surface-fold.ts';
|
|
9
|
+
import type { TokenSurfaceNode } from './types.ts';
|
|
10
|
+
type FileAttachmentRef = Extract<ContentBlock, {
|
|
11
|
+
type: 'file';
|
|
12
|
+
}>['attachment'];
|
|
13
|
+
/** One surface priced for a request route: public nodes plus their total. */
|
|
14
|
+
export interface PricedSurface {
|
|
15
|
+
/** Positional nodes carrying both the route price and the fixed-heuristic price. */
|
|
16
|
+
readonly nodes: TokenSurfaceNode[];
|
|
17
|
+
/** Sum of the route prices across the surface. */
|
|
18
|
+
readonly surfaceTokens: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Price one ordered surface under its model-request attachment projection.
|
|
22
|
+
* @param nodes - the fold's current or snapshotted surface, in model-visible order.
|
|
23
|
+
* @param pricing - the routed model's image pricing, or undefined to keep the fixed heuristic.
|
|
24
|
+
* @param fileText - exact file handle projection used by the mounted LLM service.
|
|
25
|
+
* @returns detached public nodes and their route-priced total.
|
|
26
|
+
* @throws when the pricing answers a different occurrence count than it was
|
|
27
|
+
* asked — misalignment would silently misprice nodes, so it must fail loud.
|
|
28
|
+
*/
|
|
29
|
+
export declare function priceSurface(nodes: readonly MeterSurfaceNode[], pricing: LlmImageRequestPricing | undefined, fileText?: (ref: FileAttachmentRef) => string): PricedSurface;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=route-pricing.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-projected surface pricing: replaces attachment-block heuristics with
|
|
3
|
+
* the image and file representations sent to the routed model.
|
|
4
|
+
*
|
|
5
|
+
* @module @qilin-agent/token-meter/route-pricing
|
|
6
|
+
*/
|
|
7
|
+
import { estimateContent } from "./estimate.js";
|
|
8
|
+
/**
|
|
9
|
+
* Price one ordered surface under its model-request attachment projection.
|
|
10
|
+
* @param nodes - the fold's current or snapshotted surface, in model-visible order.
|
|
11
|
+
* @param pricing - the routed model's image pricing, or undefined to keep the fixed heuristic.
|
|
12
|
+
* @param fileText - exact file handle projection used by the mounted LLM service.
|
|
13
|
+
* @returns detached public nodes and their route-priced total.
|
|
14
|
+
* @throws when the pricing answers a different occurrence count than it was
|
|
15
|
+
* asked — misalignment would silently misprice nodes, so it must fail loud.
|
|
16
|
+
*/
|
|
17
|
+
export function priceSurface(nodes, pricing, fileText) {
|
|
18
|
+
const images = pricing === undefined ? [] : nodes.flatMap(node => node.images);
|
|
19
|
+
const hasFiles = fileText !== undefined && nodes.some(node => node.files.length > 0);
|
|
20
|
+
if ((pricing === undefined || images.length === 0) && !hasFiles) {
|
|
21
|
+
let surfaceTokens = 0;
|
|
22
|
+
const publicNodes = nodes.map((node) => {
|
|
23
|
+
surfaceTokens += node.heuristicTokens;
|
|
24
|
+
return { seq: node.seq, tokens: node.heuristicTokens, heuristicTokens: node.heuristicTokens };
|
|
25
|
+
});
|
|
26
|
+
return { nodes: publicNodes, surfaceTokens };
|
|
27
|
+
}
|
|
28
|
+
const prices = pricing === undefined ? [] : pricing.priceImages(images);
|
|
29
|
+
if (pricing !== undefined && prices.length !== images.length) {
|
|
30
|
+
throw new Error(`token meter: route image pricing answered ${prices.length} prices for ${images.length} occurrences`);
|
|
31
|
+
}
|
|
32
|
+
let cursor = 0;
|
|
33
|
+
let surfaceTokens = 0;
|
|
34
|
+
const publicNodes = nodes.map((node) => {
|
|
35
|
+
let tokens = node.heuristicTokens;
|
|
36
|
+
if (fileText !== undefined && node.files.length > 0) {
|
|
37
|
+
tokens -= node.fileStructuralTokens;
|
|
38
|
+
for (const file of node.files) {
|
|
39
|
+
tokens += estimateContent([{ type: 'text', text: fileText(file) }]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (pricing !== undefined && node.images.length > 0) {
|
|
43
|
+
tokens -= node.imageStructuralTokens;
|
|
44
|
+
for (let occurrence = 0; occurrence < node.images.length; occurrence += 1) {
|
|
45
|
+
// oxlint-disable-next-line typescript/no-non-null-assertion -- length equality is asserted above
|
|
46
|
+
const price = prices[cursor];
|
|
47
|
+
cursor += 1;
|
|
48
|
+
tokens += price.visualTokens + estimateContent([{ type: 'text', text: price.text }]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
surfaceTokens += tokens;
|
|
52
|
+
return { seq: node.seq, tokens, heuristicTokens: node.heuristicTokens };
|
|
53
|
+
});
|
|
54
|
+
return { nodes: publicNodes, surfaceTokens };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=route-pricing.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positional pricing shared by measurement and the context-breakdown fold:
|
|
3
|
+
* measurement retains attachment details for route pricing; breakdown keeps
|
|
4
|
+
* only retained node identities, heuristic prices, and system classification.
|
|
5
|
+
* The occupancy projection uses the scalar shadow-price protocol instead.
|
|
6
|
+
*
|
|
7
|
+
* The fold is a plan/commit pair: {@link planSurfaceTokens} runs every
|
|
8
|
+
* fallible step read-only and {@link commitSurfaceTokens} mutates in place,
|
|
9
|
+
* so a throw leaves the caller's state untouched and the same malformed
|
|
10
|
+
* event fails identically on every retry.
|
|
11
|
+
* Nodes also carry durable attachment occurrences and their structural prices,
|
|
12
|
+
* so `measure()` can price the request representation sent to the model.
|
|
13
|
+
*
|
|
14
|
+
* @module @qilin-agent/token-meter/surface-fold
|
|
15
|
+
*/
|
|
16
|
+
import type { SessionSeq, SurfaceEvent } from '@qilin-agent/session';
|
|
17
|
+
import type { ContentBlock, ImageBlock } from '@qilin-agent/llm';
|
|
18
|
+
type FileAttachmentRef = Extract<ContentBlock, {
|
|
19
|
+
type: 'file';
|
|
20
|
+
}>['attachment'];
|
|
21
|
+
/** One priced surface node with the image occurrences route pricing replaces. */
|
|
22
|
+
export interface MeterSurfaceNode {
|
|
23
|
+
/** Durable sequence number of the surface event. */
|
|
24
|
+
readonly seq: SessionSeq;
|
|
25
|
+
/** Fixed-heuristic price of the node's exact message. */
|
|
26
|
+
readonly heuristicTokens: number;
|
|
27
|
+
/** Structural JSON price replaced when the routed request projects images. */
|
|
28
|
+
readonly imageStructuralTokens: number;
|
|
29
|
+
/** Structural JSON price replaced when request assembly projects files to text. */
|
|
30
|
+
readonly fileStructuralTokens: number;
|
|
31
|
+
/** Durable image blocks in message order, `offloaded` marks included; empty for image-free nodes. */
|
|
32
|
+
readonly images: readonly ImageBlock[];
|
|
33
|
+
/** Durable file occurrences in message order; empty for file-free nodes. */
|
|
34
|
+
readonly files: readonly FileAttachmentRef[];
|
|
35
|
+
}
|
|
36
|
+
/** One validated surface transition that has not mutated the priced surface yet. */
|
|
37
|
+
export interface SurfaceTokenPlan<Node = MeterSurfaceNode> {
|
|
38
|
+
/** Heuristic price of the event's own message; 0 when it derives none. */
|
|
39
|
+
readonly tokens: number;
|
|
40
|
+
/** Signed change in the surface total: `tokens` minus anything shadowed. */
|
|
41
|
+
readonly deltaTokens: number;
|
|
42
|
+
/** The priced node the commit inserts for this event. */
|
|
43
|
+
readonly node: Node;
|
|
44
|
+
/** Commit position: `append`, or the inclusive replaced index range. */
|
|
45
|
+
readonly target: 'append' | {
|
|
46
|
+
readonly startIdx: number;
|
|
47
|
+
readonly endIdx: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate and price one surface event without mutating the surface.
|
|
52
|
+
* @param nodes - the priced surface preceding this event, in model-visible order.
|
|
53
|
+
* @param event - the surface event to place.
|
|
54
|
+
* @returns the plan for {@link commitSurfaceTokens}.
|
|
55
|
+
* @throws when a replacement names a range absent from `nodes` — committed
|
|
56
|
+
* logs are surface-validated at append time, so an unresolvable range is log
|
|
57
|
+
* corruption and must fail loud rather than skip the event.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planSurfaceTokens(nodes: readonly Pick<MeterSurfaceNode, 'seq' | 'heuristicTokens'>[], event: SurfaceEvent): SurfaceTokenPlan;
|
|
60
|
+
/**
|
|
61
|
+
* Apply one validated plan to the priced surface in place; infallible, so it
|
|
62
|
+
* cannot leave a half-applied surface behind.
|
|
63
|
+
* @param nodes - the exact priced surface the plan was built against.
|
|
64
|
+
* @param plan - the transition returned by {@link planSurfaceTokens}.
|
|
65
|
+
*/
|
|
66
|
+
export declare function commitSurfaceTokens<Node>(nodes: Node[], plan: SurfaceTokenPlan<Node>): void;
|
|
67
|
+
export {};
|
|
68
|
+
//# sourceMappingURL=surface-fold.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positional pricing shared by measurement and the context-breakdown fold:
|
|
3
|
+
* measurement retains attachment details for route pricing; breakdown keeps
|
|
4
|
+
* only retained node identities, heuristic prices, and system classification.
|
|
5
|
+
* The occupancy projection uses the scalar shadow-price protocol instead.
|
|
6
|
+
*
|
|
7
|
+
* The fold is a plan/commit pair: {@link planSurfaceTokens} runs every
|
|
8
|
+
* fallible step read-only and {@link commitSurfaceTokens} mutates in place,
|
|
9
|
+
* so a throw leaves the caller's state untouched and the same malformed
|
|
10
|
+
* event fails identically on every retry.
|
|
11
|
+
* Nodes also carry durable attachment occurrences and their structural prices,
|
|
12
|
+
* so `measure()` can price the request representation sent to the model.
|
|
13
|
+
*
|
|
14
|
+
* @module @qilin-agent/token-meter/surface-fold
|
|
15
|
+
*/
|
|
16
|
+
import { deriveEventMessage } from '@qilin-agent/session';
|
|
17
|
+
import { estimateMessage, estimateStructuralBlock } from "./estimate.js";
|
|
18
|
+
/** Collect projected attachment occurrences and their structural prices. */
|
|
19
|
+
function collectProjectedAttachments(blocks, images, files) {
|
|
20
|
+
let imageTokens = 0;
|
|
21
|
+
let fileTokens = 0;
|
|
22
|
+
for (const block of blocks) {
|
|
23
|
+
if (block.type === 'image') {
|
|
24
|
+
images.push(block);
|
|
25
|
+
imageTokens += estimateStructuralBlock(block);
|
|
26
|
+
}
|
|
27
|
+
else if (block.type === 'file') {
|
|
28
|
+
files.push(block.attachment);
|
|
29
|
+
fileTokens += estimateStructuralBlock(block);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { imageTokens, fileTokens };
|
|
33
|
+
}
|
|
34
|
+
/** Build one priced node from a surface event's derived message. */
|
|
35
|
+
function analyzeNode(seq, message) {
|
|
36
|
+
if (message === null) {
|
|
37
|
+
return {
|
|
38
|
+
seq,
|
|
39
|
+
heuristicTokens: 0,
|
|
40
|
+
imageStructuralTokens: 0,
|
|
41
|
+
fileStructuralTokens: 0,
|
|
42
|
+
images: [],
|
|
43
|
+
files: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const heuristicTokens = estimateMessage(message);
|
|
47
|
+
const images = [];
|
|
48
|
+
const files = [];
|
|
49
|
+
const structural = collectProjectedAttachments(message.content, images, files);
|
|
50
|
+
return {
|
|
51
|
+
seq,
|
|
52
|
+
heuristicTokens,
|
|
53
|
+
imageStructuralTokens: structural.imageTokens,
|
|
54
|
+
fileStructuralTokens: structural.fileTokens,
|
|
55
|
+
images,
|
|
56
|
+
files,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate and price one surface event without mutating the surface.
|
|
61
|
+
* @param nodes - the priced surface preceding this event, in model-visible order.
|
|
62
|
+
* @param event - the surface event to place.
|
|
63
|
+
* @returns the plan for {@link commitSurfaceTokens}.
|
|
64
|
+
* @throws when a replacement names a range absent from `nodes` — committed
|
|
65
|
+
* logs are surface-validated at append time, so an unresolvable range is log
|
|
66
|
+
* corruption and must fail loud rather than skip the event.
|
|
67
|
+
*/
|
|
68
|
+
export function planSurfaceTokens(nodes, event) {
|
|
69
|
+
const node = analyzeNode(event.seq, deriveEventMessage(event));
|
|
70
|
+
const tokens = node.heuristicTokens;
|
|
71
|
+
const op = event.surfaceOp;
|
|
72
|
+
if (op === 'append') {
|
|
73
|
+
return { tokens, deltaTokens: tokens, node, target: 'append' };
|
|
74
|
+
}
|
|
75
|
+
const startIdx = nodes.findIndex(candidate => candidate.seq === op.startSeq);
|
|
76
|
+
const endIdx = nodes.findIndex(candidate => candidate.seq === op.endSeq);
|
|
77
|
+
if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
|
|
78
|
+
throw new Error(`token surface: replace at seq ${event.seq} has invalid current range ${op.startSeq}-${op.endSeq}`);
|
|
79
|
+
}
|
|
80
|
+
const removed = nodes
|
|
81
|
+
.slice(startIdx, endIdx + 1)
|
|
82
|
+
.reduce((total, candidate) => total + candidate.heuristicTokens, 0);
|
|
83
|
+
return { tokens, deltaTokens: tokens - removed, node, target: { startIdx, endIdx } };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Apply one validated plan to the priced surface in place; infallible, so it
|
|
87
|
+
* cannot leave a half-applied surface behind.
|
|
88
|
+
* @param nodes - the exact priced surface the plan was built against.
|
|
89
|
+
* @param plan - the transition returned by {@link planSurfaceTokens}.
|
|
90
|
+
*/
|
|
91
|
+
export function commitSurfaceTokens(nodes, plan) {
|
|
92
|
+
if (plan.target === 'append') {
|
|
93
|
+
nodes.push(plan.node);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
nodes.splice(plan.target.startIdx, plan.target.endIdx - plan.target.startIdx + 1, plan.node);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=surface-fold.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context-pressure projection's O(1) surface-token fold.
|
|
3
|
+
*
|
|
4
|
+
* Its checkpoint keeps scalar totals rather than the retained surface used
|
|
5
|
+
* by context breakdown. Replacements ride the compact
|
|
6
|
+
* seam's shadow-price protocol: the metering event immediately before a
|
|
7
|
+
* surface `replace` (`compaction/summary` or `compaction/prune`) states the
|
|
8
|
+
* heuristic price of the exact replaced range, so the fold keeps a running
|
|
9
|
+
* total plus at most one pending claim and never retains per-node prices.
|
|
10
|
+
* The counts are exact by construction: producers derive them from the same
|
|
11
|
+
* fixed estimator this module prices appends with. A replacement without an
|
|
12
|
+
* armed claim folds with zero delta because bounded state cannot reconstruct
|
|
13
|
+
* the replaced range; this preserves replay at the cost of possible drift.
|
|
14
|
+
*
|
|
15
|
+
* @module @qilin-agent/token-meter/surface-projection
|
|
16
|
+
*/
|
|
17
|
+
import { SessionSeq } from '@qilin-agent/session';
|
|
18
|
+
import type { SessionEvent } from '@qilin-agent/session';
|
|
19
|
+
/**
|
|
20
|
+
* One armed shadow price: the heuristic tokens of the surface range the
|
|
21
|
+
* IMMEDIATELY following event replaces. Plain JSON — it is part of the
|
|
22
|
+
* persisted unit state while armed.
|
|
23
|
+
*/
|
|
24
|
+
export interface ShadowPriceClaim {
|
|
25
|
+
/** Declared inclusive first surface-node seq of the priced range. */
|
|
26
|
+
start: SessionSeq;
|
|
27
|
+
/** Declared inclusive last surface-node seq of the priced range. */
|
|
28
|
+
end: SessionSeq;
|
|
29
|
+
/** Heuristic tokens of the priced range under the fixed estimator. */
|
|
30
|
+
tokens: number;
|
|
31
|
+
}
|
|
32
|
+
/** One event's effect on a running surface-token total. */
|
|
33
|
+
export interface SurfaceTokensFold {
|
|
34
|
+
/** Signed change in the surface total; 0 for events off the surface. */
|
|
35
|
+
readonly deltaTokens: number;
|
|
36
|
+
/** Claim to carry into the next event; undefined when none survives. */
|
|
37
|
+
readonly claim: ShadowPriceClaim | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fold one committed event onto a running surface-token total.
|
|
41
|
+
*
|
|
42
|
+
* A shadow-price event arms a claim; any other event expires it, and a
|
|
43
|
+
* surface `replace` consumes the claim naming its exact range — the
|
|
44
|
+
* producers append the metering event and the replacement synchronously
|
|
45
|
+
* adjacent, so a surviving claim always prices the very next event.
|
|
46
|
+
* A replace with no claim folds with zero delta because the bounded state
|
|
47
|
+
* cannot reconstruct the replaced range. An armed claim for another range
|
|
48
|
+
* still fails because the adjacent events contradict each other.
|
|
49
|
+
* @param claim - the claim armed by the immediately preceding event, if any.
|
|
50
|
+
* @param event - the next committed session event.
|
|
51
|
+
* @returns the signed token delta and the claim state after this event.
|
|
52
|
+
* @throws when a replacement arrives with an armed claim for a different
|
|
53
|
+
* range — the metering event was adjacent, so this is a live producer's
|
|
54
|
+
* shadow-price contract violation, not historical data, and must fail
|
|
55
|
+
* loud rather than let the total drift.
|
|
56
|
+
*/
|
|
57
|
+
export declare function foldSurfaceProjection(claim: ShadowPriceClaim | undefined, event: SessionEvent): SurfaceTokensFold;
|
|
58
|
+
//# sourceMappingURL=surface-projection.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context-pressure projection's O(1) surface-token fold.
|
|
3
|
+
*
|
|
4
|
+
* Its checkpoint keeps scalar totals rather than the retained surface used
|
|
5
|
+
* by context breakdown. Replacements ride the compact
|
|
6
|
+
* seam's shadow-price protocol: the metering event immediately before a
|
|
7
|
+
* surface `replace` (`compaction/summary` or `compaction/prune`) states the
|
|
8
|
+
* heuristic price of the exact replaced range, so the fold keeps a running
|
|
9
|
+
* total plus at most one pending claim and never retains per-node prices.
|
|
10
|
+
* The counts are exact by construction: producers derive them from the same
|
|
11
|
+
* fixed estimator this module prices appends with. A replacement without an
|
|
12
|
+
* armed claim folds with zero delta because bounded state cannot reconstruct
|
|
13
|
+
* the replaced range; this preserves replay at the cost of possible drift.
|
|
14
|
+
*
|
|
15
|
+
* @module @qilin-agent/token-meter/surface-projection
|
|
16
|
+
*/
|
|
17
|
+
import { deriveEventMessage, isSurfaceEvent, SessionSeq } from '@qilin-agent/session';
|
|
18
|
+
import { estimateMessage } from "./estimate.js";
|
|
19
|
+
/**
|
|
20
|
+
* Fold one committed event onto a running surface-token total.
|
|
21
|
+
*
|
|
22
|
+
* A shadow-price event arms a claim; any other event expires it, and a
|
|
23
|
+
* surface `replace` consumes the claim naming its exact range — the
|
|
24
|
+
* producers append the metering event and the replacement synchronously
|
|
25
|
+
* adjacent, so a surviving claim always prices the very next event.
|
|
26
|
+
* A replace with no claim folds with zero delta because the bounded state
|
|
27
|
+
* cannot reconstruct the replaced range. An armed claim for another range
|
|
28
|
+
* still fails because the adjacent events contradict each other.
|
|
29
|
+
* @param claim - the claim armed by the immediately preceding event, if any.
|
|
30
|
+
* @param event - the next committed session event.
|
|
31
|
+
* @returns the signed token delta and the claim state after this event.
|
|
32
|
+
* @throws when a replacement arrives with an armed claim for a different
|
|
33
|
+
* range — the metering event was adjacent, so this is a live producer's
|
|
34
|
+
* shadow-price contract violation, not historical data, and must fail
|
|
35
|
+
* loud rather than let the total drift.
|
|
36
|
+
*/
|
|
37
|
+
export function foldSurfaceProjection(claim, event) {
|
|
38
|
+
if (event.type === 'compaction/summary' || event.type === 'compaction/prune') {
|
|
39
|
+
const { shadowedRange, shadowedTokenCount } = event.data;
|
|
40
|
+
return {
|
|
41
|
+
deltaTokens: 0,
|
|
42
|
+
claim: {
|
|
43
|
+
start: SessionSeq(shadowedRange.start),
|
|
44
|
+
end: SessionSeq(shadowedRange.end),
|
|
45
|
+
tokens: shadowedTokenCount,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!isSurfaceEvent(event))
|
|
50
|
+
return { deltaTokens: 0, claim: undefined };
|
|
51
|
+
const message = deriveEventMessage(event);
|
|
52
|
+
const tokens = message === null ? 0 : estimateMessage(message);
|
|
53
|
+
const op = event.surfaceOp;
|
|
54
|
+
if (op === 'append')
|
|
55
|
+
return { deltaTokens: tokens, claim: undefined };
|
|
56
|
+
// Sessions recorded before the shadow-price protocol log replacements with
|
|
57
|
+
// no adjacent metering event; the bounded state cannot reconstruct the
|
|
58
|
+
// replaced range's price, so fold those neutrally — historical replay
|
|
59
|
+
// degrades to drift instead of failing.
|
|
60
|
+
if (claim === undefined)
|
|
61
|
+
return { deltaTokens: 0, claim: undefined };
|
|
62
|
+
if (claim.start !== op.startSeq || claim.end !== op.endSeq) {
|
|
63
|
+
throw new Error(`token surface: replace at seq ${event.seq} over range ${op.startSeq}-${op.endSeq} has no adjacent shadow price`
|
|
64
|
+
+ ` (armed claim covers ${claim.start}-${claim.end})`);
|
|
65
|
+
}
|
|
66
|
+
return { deltaTokens: tokens - claim.tokens, claim: undefined };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=surface-projection.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SessionEvent } from '@qilin-agent/session/types';
|
|
2
|
+
/** One provider/model route that contributed a billed request attempt. */
|
|
3
|
+
export interface TurnTokenUsageRoute {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly model: string;
|
|
6
|
+
}
|
|
7
|
+
/** Exact provider-reported token accounting for every attempt in one completed Turn. */
|
|
8
|
+
export interface TurnTokenUsage {
|
|
9
|
+
/** Sum of uncached prompt input across all attempts. */
|
|
10
|
+
readonly uncachedInputTokens: number;
|
|
11
|
+
readonly outputTokens: number;
|
|
12
|
+
/** Exact aggregate prompt plus output total across all attempts. */
|
|
13
|
+
readonly totalTokens: number;
|
|
14
|
+
/** Present only when every attempt reported the bucket. */
|
|
15
|
+
readonly cacheReadTokens?: number;
|
|
16
|
+
/** Present only when every attempt reported the bucket. */
|
|
17
|
+
readonly cacheWriteTokens?: number;
|
|
18
|
+
/** Output subset, present only when every attempt reported it. */
|
|
19
|
+
readonly reasoningTokens?: number;
|
|
20
|
+
/** Present only when every billed attempt has provider/model attribution. */
|
|
21
|
+
readonly routes?: readonly TurnTokenUsageRoute[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fold one complete Turn's durable attempt lifecycle into exact token accounting.
|
|
25
|
+
*
|
|
26
|
+
* No attempt is inferred from a usage sample. Any missing lifecycle boundary,
|
|
27
|
+
* incomplete attempt usage, unsafe count, or contradictory exact total makes
|
|
28
|
+
* the whole disclosure unavailable.
|
|
29
|
+
* @param events - Turn-local durable events from `turn/start` through `turn/end`.
|
|
30
|
+
* @returns exact aggregate usage, or undefined when it cannot be proven.
|
|
31
|
+
*/
|
|
32
|
+
export declare function deriveTurnTokenUsage(events: readonly SessionEvent[]): TurnTokenUsage | undefined;
|
|
33
|
+
//# sourceMappingURL=turn-usage.d.ts.map
|