@syghar/dsh-ninfer-context 0.1.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.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # dsh-ninfer-context
2
+
3
+ This plugin is a compatible replacement for DSH's `token-meter` service. It
4
+ keeps the `tokenUsage`, `contextPressure`, and `contextBreakdown` projections
5
+ unchanged, but treats a completed `ninfer` usage report as the authoritative
6
+ context anchor even when it is lower than the fixed-density heuristic.
7
+
8
+ That makes the existing compaction engine and context-used widget agree with
9
+ the actual OpenAI-compatible usage returned by ninfer. Surface changes after
10
+ the last provider sample are still estimated until the next ninfer response;
11
+ the provider anchor is never fabricated before ninfer reports usage.
@@ -0,0 +1,4 @@
1
+ # Replace DSH's heuristic-only token meter. The service keeps the same
2
+ # tokenMeter/contextPressure contracts used by compaction and the stock UI.
3
+ - id: token-meter
4
+ name: '@syghar/dsh-ninfer-context'
package/lib/index.mjs ADDED
@@ -0,0 +1,344 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { BlockAssembler, deepFreeze } from "@deepseek-ai/dsh-llm";
4
+ import { canonicalHeader, deriveEventMessage, headerEquals, isSurfaceEvent } from "@deepseek-ai/dsh-session";
5
+ import { z as zod } from "zod";
6
+
7
+ const CHARS_PER_TOKEN = 4;
8
+ const BLOCK_OVERHEAD = 4;
9
+
10
+ function estimateContent(blocks) {
11
+ let tokens = 0;
12
+ for (const block of blocks) switch (block.type) {
13
+ case "text":
14
+ case "reasoning":
15
+ tokens += Math.ceil(block.text.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
16
+ break;
17
+ case "tool-call":
18
+ tokens += Math.ceil(block.name.length / CHARS_PER_TOKEN) + Math.ceil(block.arguments.length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
19
+ break;
20
+ case "tool-result":
21
+ tokens += estimateContent(block.content) + BLOCK_OVERHEAD;
22
+ break;
23
+ default:
24
+ tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
25
+ }
26
+ return tokens;
27
+ }
28
+
29
+ function estimateMessage(message) {
30
+ return estimateContent(message.content) + 4;
31
+ }
32
+
33
+ function estimateSystemTokens(header) {
34
+ return header?.system === undefined ? 0 : Math.ceil(header.system.length / CHARS_PER_TOKEN) + 4;
35
+ }
36
+
37
+ function estimateToolsTokens(header) {
38
+ return header?.tools === undefined || header.tools.length === 0
39
+ ? 0
40
+ : Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
41
+ }
42
+
43
+ function estimateHeader(header) {
44
+ return estimateSystemTokens(header) + estimateToolsTokens(header);
45
+ }
46
+
47
+ function foldSurfaceProjection(claim, event) {
48
+ if (event.type === "compaction/summary" || event.type === "compaction/prune") {
49
+ const { shadowedRange, shadowedTokenCount } = event.data;
50
+ return { deltaTokens: 0, claim: { start: shadowedRange.start, end: shadowedRange.end, tokens: shadowedTokenCount } };
51
+ }
52
+ if (!isSurfaceEvent(event)) return { deltaTokens: 0, claim: undefined };
53
+ const message = deriveEventMessage(event);
54
+ const tokens = message === null ? 0 : estimateMessage(message);
55
+ const op = event.surfaceOp;
56
+ if (op === "append") return { deltaTokens: tokens, claim: undefined };
57
+ if (claim === undefined) return { deltaTokens: 0, claim: undefined };
58
+ if (claim.start !== op.start || claim.end !== op.end) {
59
+ throw new Error(`ninfer context: replace at seq ${event.seq} over range ${op.start}-${op.end} has no adjacent shadow price`);
60
+ }
61
+ return { deltaTokens: tokens - claim.tokens, claim: undefined };
62
+ }
63
+
64
+ const contextBreakdownProjectionDefinition = {
65
+ key: "contextBreakdown",
66
+ schema: zod.object({
67
+ systemTokens: zod.number().int().nonnegative(),
68
+ toolsTokens: zod.number().int().nonnegative(),
69
+ messageTokens: zod.number().int().nonnegative()
70
+ }).strict(),
71
+ init: () => ({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 }),
72
+ apply: (state, event) => {
73
+ const fold = foldSurfaceProjection(state.claim, event);
74
+ let systemTokens = state.systemTokens;
75
+ let toolsTokens = state.toolsTokens;
76
+ if (event.type === "request/header") {
77
+ const header = canonicalHeader(event.data.header);
78
+ systemTokens = estimateSystemTokens(header);
79
+ toolsTokens = estimateToolsTokens(header);
80
+ }
81
+ if (systemTokens === state.systemTokens && toolsTokens === state.toolsTokens && fold.deltaTokens === 0 && fold.claim === undefined && state.claim === undefined) return state;
82
+ return {
83
+ systemTokens,
84
+ toolsTokens,
85
+ messageTokens: state.messageTokens + fold.deltaTokens,
86
+ ...(fold.claim === undefined ? {} : { claim: fold.claim })
87
+ };
88
+ },
89
+ view: ({ systemTokens, toolsTokens, messageTokens }) => ({ systemTokens, toolsTokens, messageTokens }),
90
+ stateVersion: 2
91
+ };
92
+
93
+ const zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
94
+ const bucketsFrom = (usage) => ({
95
+ uncachedInputTokens: usage.inputTokens,
96
+ outputTokens: usage.outputTokens,
97
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
98
+ cacheWriteTokens: usage.cacheWriteTokens ?? 0
99
+ });
100
+ const bucketsEqual = (left, right) => left.uncachedInputTokens === right.uncachedInputTokens && left.outputTokens === right.outputTokens && left.cacheReadTokens === right.cacheReadTokens && left.cacheWriteTokens === right.cacheWriteTokens;
101
+ const addReplacing = (totals, previous, next) => ({
102
+ uncachedInputTokens: totals.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) + next.uncachedInputTokens,
103
+ outputTokens: totals.outputTokens - (previous?.outputTokens ?? 0) + next.outputTokens,
104
+ cacheReadTokens: totals.cacheReadTokens - (previous?.cacheReadTokens ?? 0) + next.cacheReadTokens,
105
+ cacheWriteTokens: totals.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) + next.cacheWriteTokens
106
+ });
107
+ const projectionSchema = zod.object({
108
+ uncachedInputTokens: zod.number().int().nonnegative(),
109
+ outputTokens: zod.number().int().nonnegative(),
110
+ cacheReadTokens: zod.number().int().nonnegative(),
111
+ cacheWriteTokens: zod.number().int().nonnegative()
112
+ }).strict();
113
+ const pressureSchema = zod.object({
114
+ pressureTokens: zod.number().int().nonnegative().optional(),
115
+ projectedTokens: zod.number().int().nonnegative().optional(),
116
+ contextWindow: zod.number().int().positive().optional()
117
+ }).strict();
118
+ const pressureFrom = (usage) => usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
119
+ const usageOf = (event) => event.type === "assistant/chunk" && event.data.chunk.type === "usage"
120
+ ? event.data.chunk.usage
121
+ : event.type === "assistant/message" ? event.data.usage : undefined;
122
+
123
+ const tokenUsageProjectionDefinition = {
124
+ key: "tokenUsage",
125
+ schema: projectionSchema,
126
+ init: () => ({ totals: zeroBuckets(), last: null }),
127
+ apply: (state, event) => {
128
+ let turn;
129
+ let step;
130
+ let usage;
131
+ if (event.type === "assistant/chunk" && event.data.chunk.type === "usage") {
132
+ ({ turn, step } = event.data);
133
+ usage = event.data.chunk.usage;
134
+ } else if (event.type === "assistant/message" && event.data.usage !== undefined) {
135
+ ({ turn, step, usage } = event.data);
136
+ } else return state;
137
+ const buckets = bucketsFrom(usage);
138
+ const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last.buckets : undefined;
139
+ if (previous !== undefined && bucketsEqual(previous, buckets)) return state;
140
+ return { totals: addReplacing(state.totals, previous, buckets), last: { turn, step, buckets } };
141
+ },
142
+ view: (state) => state.totals,
143
+ stateVersion: 1
144
+ };
145
+
146
+ const contextPressureProjectionDefinition = {
147
+ key: "contextPressure",
148
+ schema: pressureSchema,
149
+ init: () => ({ surfaceTokens: 0 }),
150
+ apply: (state, event) => {
151
+ const fold = foldSurfaceProjection(state.claim, event);
152
+ let next = state;
153
+ if (event.type === "request/context") {
154
+ const contextWindow = event.data.contextWindow;
155
+ if (contextWindow !== state.contextWindow) {
156
+ if (contextWindow !== undefined) next = { ...next, contextWindow };
157
+ else {
158
+ const { contextWindow: _removed, ...withoutContextWindow } = next;
159
+ next = withoutContextWindow;
160
+ }
161
+ }
162
+ }
163
+ const usage = usageOf(event);
164
+ if (usage !== undefined) {
165
+ const pressureTokens = pressureFrom(usage);
166
+ if (pressureTokens !== next.pressureTokens || next.sampledSurfaceTokens !== next.surfaceTokens) {
167
+ next = { ...next, pressureTokens, sampledSurfaceTokens: next.surfaceTokens };
168
+ }
169
+ }
170
+ if (fold.deltaTokens !== 0) next = { ...next, surfaceTokens: next.surfaceTokens + fold.deltaTokens };
171
+ if (state.claim === undefined && fold.claim === undefined) return next;
172
+ const { claim: _expired, ...withoutClaim } = next;
173
+ return fold.claim === undefined ? withoutClaim : { ...withoutClaim, claim: fold.claim };
174
+ },
175
+ view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
176
+ ...(contextWindow === undefined ? {} : { contextWindow }),
177
+ ...(pressureTokens === undefined ? {} : { pressureTokens }),
178
+ ...(pressureTokens === undefined || sampledSurfaceTokens === undefined ? {} : { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) })
179
+ }),
180
+ stateVersion: 4
181
+ };
182
+
183
+ function foldSurfaceTokens(nodes, event) {
184
+ const message = deriveEventMessage(event);
185
+ const tokens = message === null ? 0 : estimateMessage(message);
186
+ const op = event.surfaceOp;
187
+ if (op === "append") return { tokens, nodes: [...nodes, { seq: event.seq, tokens }], deltaTokens: tokens };
188
+ const startIdx = nodes.findIndex((node) => node.seq === op.start);
189
+ const endIdx = nodes.findIndex((node) => node.seq === op.end);
190
+ if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) throw new Error(`ninfer context: replace at seq ${event.seq} has invalid current range ${op.start}-${op.end}`);
191
+ const removed = nodes.slice(startIdx, endIdx + 1).reduce((total, node) => total + node.tokens, 0);
192
+ const next = [...nodes];
193
+ next.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, tokens });
194
+ return { tokens, nodes: next, deltaTokens: tokens - removed };
195
+ }
196
+
197
+ function usageTokens(usage) {
198
+ return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens;
199
+ }
200
+
201
+ function optionalHeaderEquals(left, right) {
202
+ if (left === undefined || right === undefined) return left === right;
203
+ return headerEquals(left, right);
204
+ }
205
+
206
+ function validateConfigKeys(config) {
207
+ for (const key of Object.keys(config)) throw new Error(`TokenMeterConfig: unknown key "${key}" (no settings are supported)`);
208
+ }
209
+
210
+ function isAuthoritativeNinfer(header) {
211
+ return header?.config?.provider === "ninfer";
212
+ }
213
+
214
+ class TokenMeter extends Service {
215
+ static Config = z.object({});
216
+ states = new WeakMap();
217
+
218
+ constructor(ctx, config = {}) {
219
+ super(ctx, "tokenMeter");
220
+ validateConfigKeys(config);
221
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
222
+ projectionCtx.sessionProjections.register(tokenUsageProjectionDefinition);
223
+ projectionCtx.sessionProjections.register(contextPressureProjectionDefinition);
224
+ projectionCtx.sessionProjections.register(contextBreakdownProjectionDefinition);
225
+ });
226
+ ctx.on("session/event", (session) => {
227
+ if (this.states.has(session)) this._sync(session);
228
+ });
229
+ }
230
+
231
+ measure(session, requestHeader) {
232
+ const state = this._sync(session);
233
+ const header = requestHeader === undefined ? state.header : canonicalHeader(requestHeader);
234
+ const anchor = state.anchor;
235
+ let baseline;
236
+ let surfaceDeltaTokens;
237
+ if (anchor !== undefined && optionalHeaderEquals(anchor.header, header)) {
238
+ baseline = anchor.baseline;
239
+ surfaceDeltaTokens = state.surfaceTokens - anchor.surfaceTokens;
240
+ } else if (header === undefined && state.surfaceTokens === 0) {
241
+ baseline = { kind: "none", tokens: 0 };
242
+ surfaceDeltaTokens = 0;
243
+ } else {
244
+ baseline = { kind: "estimated", tokens: estimateHeader(header) + state.surfaceTokens };
245
+ surfaceDeltaTokens = 0;
246
+ }
247
+ return deepFreeze(structuredClone({
248
+ logRevision: state.consumedEvents,
249
+ baseline,
250
+ surfaceDeltaTokens,
251
+ totalTokens: Math.max(0, baseline.tokens + surfaceDeltaTokens),
252
+ surfaceTokens: state.surfaceTokens,
253
+ nodes: state.surface
254
+ }));
255
+ }
256
+
257
+ estimateMessage(message) {
258
+ return estimateMessage(message);
259
+ }
260
+
261
+ _sync(session) {
262
+ let state = this.states.get(session);
263
+ if (state === undefined) {
264
+ state = { consumedEvents: 0, header: undefined, surface: [], surfaceTokens: 0, stepStart: undefined, anchor: undefined };
265
+ this.states.set(session, state);
266
+ }
267
+ while (state.consumedEvents < session.events.length) {
268
+ const event = session.events[state.consumedEvents];
269
+ this._foldEvent(session, state, event);
270
+ state.consumedEvents += 1;
271
+ }
272
+ return state;
273
+ }
274
+
275
+ _foldEvent(session, state, event) {
276
+ let nextHeader = state.header;
277
+ let nextStepStart = state.stepStart;
278
+ let nextAnchor = state.anchor;
279
+ switch (event.type) {
280
+ case "request/header":
281
+ nextHeader = canonicalHeader(event.data.header);
282
+ break;
283
+ case "step/start":
284
+ if (state.stepStart !== undefined) throw new Error(`ninfer context: step/start at seq ${event.seq} arrived before turn ${state.stepStart.turn}/step ${state.stepStart.step} ended`);
285
+ nextStepStart = { ...event.data, surfaceTokens: state.surfaceTokens };
286
+ break;
287
+ case "step/end":
288
+ if (state.stepStart === undefined || state.stepStart.turn !== event.data.turn || state.stepStart.step !== event.data.step) throw new Error(`ninfer context: step/end at seq ${event.seq} has no matching step/start event`);
289
+ nextStepStart = undefined;
290
+ break;
291
+ default:
292
+ break;
293
+ }
294
+ const surface = isSurfaceEvent(event) ? foldSurfaceTokens(state.surface, event) : undefined;
295
+ if (event.type === "assistant/message") {
296
+ const stepStart = state.stepStart;
297
+ if (stepStart === undefined || stepStart.turn !== event.data.turn || stepStart.step !== event.data.step) throw new Error(`ninfer context: assistant/message at seq ${event.seq} has no matching step/start event`);
298
+ const eventTokens = surface.tokens;
299
+ if (event.data.usage !== undefined && nextHeader !== undefined) {
300
+ const providerAssistantTokens = this._estimateProviderAssistant(session, event, eventTokens);
301
+ const anchorSurfaceTokens = stepStart.surfaceTokens + providerAssistantTokens;
302
+ const providerTokens = usageTokens(event.data.usage);
303
+ const estimatedAnchorTokens = estimateHeader(nextHeader) + anchorSurfaceTokens;
304
+ nextAnchor = {
305
+ header: nextHeader,
306
+ surfaceTokens: anchorSurfaceTokens,
307
+ baseline: isAuthoritativeNinfer(nextHeader) || providerTokens >= estimatedAnchorTokens
308
+ ? { kind: "usage", tokens: providerTokens, usage: event.data.usage }
309
+ : { kind: "estimated", tokens: estimatedAnchorTokens }
310
+ };
311
+ } else {
312
+ const anchorSurfaceTokens = stepStart.surfaceTokens + eventTokens;
313
+ nextAnchor = { header: nextHeader, surfaceTokens: anchorSurfaceTokens, baseline: { kind: "estimated", tokens: estimateHeader(nextHeader) + anchorSurfaceTokens } };
314
+ }
315
+ }
316
+ state.header = nextHeader;
317
+ state.stepStart = nextStepStart;
318
+ if (surface !== undefined) {
319
+ state.surface = surface.nodes;
320
+ state.surfaceTokens += surface.deltaTokens;
321
+ }
322
+ state.anchor = nextAnchor;
323
+ }
324
+
325
+ _estimateProviderAssistant(session, event, durableEventTokens) {
326
+ const sourceSeqs = event.sourceEventSeqs;
327
+ if (sourceSeqs === undefined) return durableEventTokens;
328
+ const assembler = new BlockAssembler();
329
+ const seen = new Set();
330
+ for (const seq of sourceSeqs) {
331
+ if (seq >= event.seq) throw new Error(`ninfer context: assistant/message at seq ${event.seq} source seq ${seq} is not earlier`);
332
+ if (seen.has(seq)) throw new Error(`ninfer context: assistant/message at seq ${event.seq} repeats source seq ${seq}`);
333
+ seen.add(seq);
334
+ const sourceEvent = session.events[seq];
335
+ if (sourceEvent.type !== "assistant/chunk") throw new Error(`ninfer context: assistant/message at seq ${event.seq} source seq ${seq} is not assistant/chunk`);
336
+ if (sourceEvent.data.turn !== event.data.turn || sourceEvent.data.step !== event.data.step) throw new Error(`ninfer context: assistant/message at seq ${event.seq} source seq ${seq} belongs to another step`);
337
+ assembler.push(sourceEvent.data.chunk);
338
+ }
339
+ const providerContent = assembler.blocks();
340
+ return providerContent.length === 0 ? 0 : estimateContent(providerContent) + 4;
341
+ }
342
+ }
343
+
344
+ export { TokenMeter, TokenMeter as default };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@syghar/dsh-ninfer-context",
3
+ "description": "Provider-anchored DSH token meter for ninfer context usage",
4
+ "version": "0.1.0",
5
+ "author": "Sygmei",
6
+ "license": "MIT",
7
+ "files": ["lib", "cordis.patch.yml", "README.md"],
8
+ "repository": { "type": "git", "url": "git+https://github.com/Sygmei/dsh-plugins-collection.git", "directory": "plugins/dsh-ninfer-context" },
9
+ "homepage": "https://github.com/Sygmei/dsh-plugins-collection/tree/main/plugins/dsh-ninfer-context",
10
+ "bugs": { "url": "https://github.com/Sygmei/dsh-plugins-collection/issues" },
11
+ "publishConfig": { "access": "public" },
12
+ "type": "module",
13
+ "main": "lib/index.mjs",
14
+ "exports": {
15
+ ".": { "default": "./lib/index.mjs" },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
19
+ "dependencies": {
20
+ "@deepseek-ai/schemastery": "^3.18.1",
21
+ "zod": "^3.25.76"
22
+ },
23
+ "peerDependencies": {
24
+ "@deepseek-ai/cordis": "^4.0.1",
25
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
26
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
27
+ "@deepseek-ai/dsh-session-projection": "^0.1.0-rc.8"
28
+ }
29
+ }