@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure folds for durable provider-reported token usage and context occupancy.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { lastAssistantStreamChunk } from '@qilin-agent/llm';
|
|
6
|
+
import { SessionSeq } from '@qilin-agent/session';
|
|
7
|
+
import { foldSurfaceProjection } from "./surface-projection.js";
|
|
8
|
+
const zeroBuckets = () => ({
|
|
9
|
+
uncachedInputTokens: 0,
|
|
10
|
+
outputTokens: 0,
|
|
11
|
+
cacheReadTokens: 0,
|
|
12
|
+
cacheWriteTokens: 0,
|
|
13
|
+
});
|
|
14
|
+
const bucketsFrom = (usage) => ({
|
|
15
|
+
uncachedInputTokens: usage.inputTokens,
|
|
16
|
+
outputTokens: usage.outputTokens,
|
|
17
|
+
cacheReadTokens: usage.cacheReadTokens ?? 0,
|
|
18
|
+
cacheWriteTokens: usage.cacheWriteTokens ?? 0,
|
|
19
|
+
});
|
|
20
|
+
const bucketsEqual = (left, right) => left.uncachedInputTokens === right.uncachedInputTokens
|
|
21
|
+
&& left.outputTokens === right.outputTokens
|
|
22
|
+
&& left.cacheReadTokens === right.cacheReadTokens
|
|
23
|
+
&& left.cacheWriteTokens === right.cacheWriteTokens;
|
|
24
|
+
const addReplacing = (totals, previous, next) => ({
|
|
25
|
+
uncachedInputTokens: totals.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) + next.uncachedInputTokens,
|
|
26
|
+
outputTokens: totals.outputTokens - (previous?.outputTokens ?? 0) + next.outputTokens,
|
|
27
|
+
cacheReadTokens: totals.cacheReadTokens - (previous?.cacheReadTokens ?? 0) + next.cacheReadTokens,
|
|
28
|
+
cacheWriteTokens: totals.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) + next.cacheWriteTokens,
|
|
29
|
+
});
|
|
30
|
+
const projectionSchema = z.object({
|
|
31
|
+
uncachedInputTokens: z.number().int().nonnegative(),
|
|
32
|
+
outputTokens: z.number().int().nonnegative(),
|
|
33
|
+
cacheReadTokens: z.number().int().nonnegative(),
|
|
34
|
+
cacheWriteTokens: z.number().int().nonnegative(),
|
|
35
|
+
}).strict();
|
|
36
|
+
/**
|
|
37
|
+
* The token-usage unit's state schema — the one definition of the state
|
|
38
|
+
* shape; the state type is inferred from it.
|
|
39
|
+
*/
|
|
40
|
+
const tokenUsageStateSchema = z.object({
|
|
41
|
+
totals: projectionSchema,
|
|
42
|
+
last: z.object({
|
|
43
|
+
turn: z.number().int().nonnegative(),
|
|
44
|
+
step: z.number().int().nonnegative(),
|
|
45
|
+
buckets: projectionSchema,
|
|
46
|
+
}).nullable(),
|
|
47
|
+
}).strict();
|
|
48
|
+
const pressureSchema = z.object({
|
|
49
|
+
pressureTokens: z.number().int().nonnegative().optional(),
|
|
50
|
+
projectedTokens: z.number().int().nonnegative().optional(),
|
|
51
|
+
contextWindow: z.number().int().positive().optional(),
|
|
52
|
+
}).strict().transform(({ pressureTokens, projectedTokens, contextWindow }) => ({
|
|
53
|
+
...pressureTokens === undefined ? {} : { pressureTokens },
|
|
54
|
+
...projectedTokens === undefined ? {} : { projectedTokens },
|
|
55
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
56
|
+
}));
|
|
57
|
+
/** Prompt-side pressure of one request: input plus cache traffic, no output. */
|
|
58
|
+
const pressureFrom = (usage) => usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
59
|
+
/** The usage one durable Assistant settlement reports for its attempt, if any. */
|
|
60
|
+
function usageOf(event) {
|
|
61
|
+
if (event.type === 'assistant/message' && event.data.usage !== undefined)
|
|
62
|
+
return event.data.usage;
|
|
63
|
+
if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt')
|
|
64
|
+
return undefined;
|
|
65
|
+
return lastAssistantStreamChunk(event.data.stream, 'usage')?.usage;
|
|
66
|
+
}
|
|
67
|
+
/** The context-pressure state schema and source of its inferred type. */
|
|
68
|
+
const contextPressureStateSchema = z.object({
|
|
69
|
+
contextWindow: z.number().int().positive().optional(),
|
|
70
|
+
pressureTokens: z.number().int().nonnegative().optional(),
|
|
71
|
+
surfaceTokens: z.number().int().nonnegative(),
|
|
72
|
+
sampledSurfaceTokens: z.number().int().nonnegative().optional(),
|
|
73
|
+
claim: z.object({
|
|
74
|
+
start: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
75
|
+
end: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).transform(SessionSeq),
|
|
76
|
+
tokens: z.number().int().nonnegative(),
|
|
77
|
+
}).optional(),
|
|
78
|
+
}).strict();
|
|
79
|
+
/**
|
|
80
|
+
* Token-meter's session projection unit.
|
|
81
|
+
*
|
|
82
|
+
* Each v2 Assistant settlement contributes the last usage sample embedded in
|
|
83
|
+
* its stream. `llm/retry-started` closes the replacement slot so the retried
|
|
84
|
+
* attempt adds to the total.
|
|
85
|
+
*/
|
|
86
|
+
export const tokenUsageProjectionDefinition = {
|
|
87
|
+
key: 'tokenUsage',
|
|
88
|
+
stateVersion: 2,
|
|
89
|
+
stateSchema: tokenUsageStateSchema,
|
|
90
|
+
init: () => ({ totals: zeroBuckets(), last: null }),
|
|
91
|
+
apply: (state, event) => {
|
|
92
|
+
if (event.type === 'llm/retry-started') {
|
|
93
|
+
return state.last?.turn === event.data.turn && state.last.step === event.data.step
|
|
94
|
+
? { ...state, last: null }
|
|
95
|
+
: state;
|
|
96
|
+
}
|
|
97
|
+
if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt') {
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
const sample = usageOf(event);
|
|
101
|
+
if (sample === undefined)
|
|
102
|
+
return state;
|
|
103
|
+
const { turn, step } = event.data;
|
|
104
|
+
const usage = sample;
|
|
105
|
+
const buckets = bucketsFrom(usage);
|
|
106
|
+
const previous = state.last !== null
|
|
107
|
+
&& state.last.turn === turn
|
|
108
|
+
&& state.last.step === step
|
|
109
|
+
? state.last.buckets
|
|
110
|
+
: undefined;
|
|
111
|
+
if (previous !== undefined && bucketsEqual(previous, buckets))
|
|
112
|
+
return state;
|
|
113
|
+
return {
|
|
114
|
+
totals: addReplacing(state.totals, previous, buckets),
|
|
115
|
+
last: { turn, step, buckets },
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
wire: { viewSchema: projectionSchema, view: state => state.totals },
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Token-meter's context-occupancy projection unit.
|
|
122
|
+
*
|
|
123
|
+
* Independent last-wins slots: the newest usage sample supplies the provider
|
|
124
|
+
* numerator, the newest `request/context` record the denominator. Both are
|
|
125
|
+
* whole values, so replay order alone decides the result and no cross-field
|
|
126
|
+
* consistency is claimed — the pair is explicitly not one atomic request
|
|
127
|
+
* observation (see {@link ContextPressureProjection}).
|
|
128
|
+
*
|
|
129
|
+
* `pressureTokens` is prompt-side only, so it holds still while a turn streams
|
|
130
|
+
* and steps forward once the next request reports its usage. Because nothing
|
|
131
|
+
* but a request reports usage, it also cannot see a compaction: the fold
|
|
132
|
+
* therefore carries a running surface total alongside it and publishes
|
|
133
|
+
* `projectedTokens` — the sample plus the surface's signed movement since it
|
|
134
|
+
* was taken — so occupancy answers for the next request rather than the last
|
|
135
|
+
* one. The total rides {@link foldSurfaceProjection}, so the state stays O(1)
|
|
136
|
+
* and a replacement shrinks it by its logged shadow price. A replacement
|
|
137
|
+
* without a claim preserves the previous total. A usage sample is stamped
|
|
138
|
+
* BEFORE the same event joins the surface, so an `assistant/message` anchors
|
|
139
|
+
* against the surface its own request saw.
|
|
140
|
+
*/
|
|
141
|
+
export const contextPressureProjectionDefinition = {
|
|
142
|
+
key: 'contextPressure',
|
|
143
|
+
stateVersion: 5,
|
|
144
|
+
stateSchema: contextPressureStateSchema,
|
|
145
|
+
init: () => ({ surfaceTokens: 0 }),
|
|
146
|
+
apply: (state, event) => {
|
|
147
|
+
const fold = foldSurfaceProjection(state.claim, event);
|
|
148
|
+
let next = state;
|
|
149
|
+
if (event.type === 'request/context') {
|
|
150
|
+
const contextWindow = event.data.contextWindow;
|
|
151
|
+
if (contextWindow !== state.contextWindow) {
|
|
152
|
+
if (contextWindow !== undefined) {
|
|
153
|
+
next = { ...next, contextWindow };
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const { contextWindow: _removed, ...withoutContextWindow } = next;
|
|
157
|
+
next = withoutContextWindow;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const usage = usageOf(event);
|
|
162
|
+
if (usage !== undefined) {
|
|
163
|
+
const pressureTokens = pressureFrom(usage);
|
|
164
|
+
if (pressureTokens !== next.pressureTokens || next.sampledSurfaceTokens !== next.surfaceTokens) {
|
|
165
|
+
next = { ...next, pressureTokens, sampledSurfaceTokens: next.surfaceTokens };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (fold.deltaTokens !== 0) {
|
|
169
|
+
next = { ...next, surfaceTokens: next.surfaceTokens + fold.deltaTokens };
|
|
170
|
+
}
|
|
171
|
+
// A defined fold.claim is always freshly built, so presence decides claim
|
|
172
|
+
// bookkeeping: no claim before or after this event leaves `next` as is.
|
|
173
|
+
if (state.claim === undefined && fold.claim === undefined)
|
|
174
|
+
return next;
|
|
175
|
+
const { claim: _expired, ...withoutClaim } = next;
|
|
176
|
+
return fold.claim === undefined ? withoutClaim : { ...withoutClaim, claim: fold.claim };
|
|
177
|
+
},
|
|
178
|
+
wire: {
|
|
179
|
+
viewSchema: pressureSchema,
|
|
180
|
+
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
|
|
181
|
+
...contextWindow === undefined ? {} : { contextWindow },
|
|
182
|
+
...pressureTokens === undefined ? {} : { pressureTokens },
|
|
183
|
+
...pressureTokens === undefined || sampledSurfaceTokens === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=usage-projection.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/token-meter",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Replay-aware token measurement service (ctx.tokenMeter) for the QiLin",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/llm/token-meter"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./lib/types/client.d.ts",
|
|
26
|
+
"default": "./lib/types/client.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
"./estimate": {
|
|
31
|
+
"types": "./lib/types/estimate.d.ts",
|
|
32
|
+
"default": "./lib/types/estimate.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/index.js",
|
|
37
|
+
"lib/types/**/*.js",
|
|
38
|
+
"lib/types/**/*.d.ts"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
43
|
+
"@qilin-agent/llm-retry": "3.1.3",
|
|
44
|
+
"@qilin-agent/compaction": "3.1.3",
|
|
45
|
+
"@qilin-agent/llm": "3.1.3",
|
|
46
|
+
"@qilin-agent/session": "3.1.3",
|
|
47
|
+
"@qilin-agent/compaction-image-offload": "3.1.3",
|
|
48
|
+
"@qilin-agent/session-projection": "3.1.3"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"zod": "^4.4.3",
|
|
52
|
+
"@qilin-agent/schemastery": "~3.18.3",
|
|
53
|
+
"@qilin-agent/util-values": "3.1.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@qilin-agent/compaction": "3.1.3",
|
|
57
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
58
|
+
"@qilin-agent/llm-retry": "3.1.3",
|
|
59
|
+
"@qilin-agent/llm": "3.1.3",
|
|
60
|
+
"@qilin-agent/session": "3.1.3",
|
|
61
|
+
"@qilin-agent/compaction-image-offload": "3.1.3",
|
|
62
|
+
"@qilin-agent/session-projection": "3.1.3"
|
|
63
|
+
}
|
|
6
64
|
}
|