@chenlongapps/opencode-token-usage 0.4.2 → 0.4.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.
@@ -1,4 +1,31 @@
1
1
  import { normalize } from "./usage.js";
2
+ export const LIVE_WINDOW_MS = 2_000;
3
+ export const LIVE_EWMA_ALPHA = 0.35;
4
+ export const LIVE_BYTES_PER_TOKEN = 4;
5
+ /** Raw live throughput from one step's recent observable deltas. */
6
+ export function estimatedLiveTps(samples) {
7
+ if (samples.length < 2)
8
+ return undefined;
9
+ const first = samples[0];
10
+ const last = samples[samples.length - 1];
11
+ const duration = last.time - first.time;
12
+ if (!Number.isFinite(duration) || duration <= 0)
13
+ return undefined;
14
+ let bytes = 0;
15
+ for (const sample of samples) {
16
+ if (!Number.isFinite(sample.time) || !Number.isFinite(sample.bytes) || sample.bytes < 0)
17
+ return undefined;
18
+ bytes += sample.bytes;
19
+ }
20
+ if (bytes <= 0)
21
+ return undefined;
22
+ return (bytes / LIVE_BYTES_PER_TOKEN) / (duration / 1_000);
23
+ }
24
+ export function smoothLiveTps(previous, raw) {
25
+ if (previous === undefined || !Number.isFinite(previous))
26
+ return raw;
27
+ return previous * (1 - LIVE_EWMA_ALPHA) + raw * LIVE_EWMA_ALPHA;
28
+ }
2
29
  const finite = (value) => typeof value === "number" && Number.isFinite(value);
3
30
  function historicalFirst(message) {
4
31
  const started = message.time?.created;
@@ -25,20 +52,18 @@ function historicalFirst(message) {
25
52
  }
26
53
  /** Exact completed-run TPS and the average of only measurable TTFT samples. */
27
54
  export function historicalPerformance(messages, observations = []) {
28
- const observed = new Map();
29
- for (const sample of observations) {
30
- if (finite(sample.started) && finite(sample.first) && sample.first >= sample.started)
31
- observed.set(sample.messageID, sample);
32
- }
55
+ return performanceWithObservations(preparePerformance(messages), observations);
56
+ }
57
+ /** The immutable part of a snapshot is scanned once, then reused for stream updates. */
58
+ export function preparePerformance(messages) {
33
59
  let output = 0;
34
60
  let duration = 0;
35
- let ttft = 0;
36
- let ttftSamples = 0;
37
- const messagesSeen = new Set();
61
+ let ttftSum = 0;
62
+ let ttftCount = 0;
63
+ const historical = new Map();
38
64
  for (const message of messages) {
39
65
  if (message.type !== "assistant")
40
66
  continue;
41
- messagesSeen.add(message.id);
42
67
  const created = message.time?.created;
43
68
  const streamed = message.time?.streamed;
44
69
  const tokens = normalize(message.tokens);
@@ -47,23 +72,35 @@ export function historicalPerformance(messages, observations = []) {
47
72
  output += generated;
48
73
  duration += streamed - created;
49
74
  }
50
- const runtime = observed.get(message.id);
51
- const sample = runtime ? runtime.first - runtime.started : historicalFirst(message);
75
+ const sample = historicalFirst(message);
52
76
  if (sample !== undefined && finite(sample) && sample >= 0) {
53
- ttft += sample;
54
- ttftSamples++;
77
+ historical.set(message.id, sample);
78
+ ttftSum += sample;
79
+ ttftCount++;
55
80
  }
56
81
  }
57
- // A live first-token event can arrive before the refreshed message snapshot.
82
+ return { ...(duration > 0 ? { tps: output / (duration / 1_000) } : {}), ttftSum, ttftCount, historical };
83
+ }
84
+ function performanceWithObservations(prepared, observations) {
85
+ const observed = new Map();
86
+ for (const sample of observations) {
87
+ if (finite(sample.started) && finite(sample.first) && sample.first >= sample.started)
88
+ observed.set(sample.messageID, sample);
89
+ }
90
+ let ttft = prepared.ttftSum;
91
+ let ttftSamples = prepared.ttftCount;
58
92
  for (const sample of observed.values()) {
59
- if (messagesSeen.has(sample.messageID))
60
- continue;
93
+ const old = prepared.historical.get(sample.messageID);
94
+ if (old !== undefined) {
95
+ ttft -= old;
96
+ ttftSamples--;
97
+ }
61
98
  ttft += sample.first - sample.started;
62
99
  ttftSamples++;
63
100
  }
64
101
  const result = {};
65
- if (duration > 0)
66
- result.tps = output / (duration / 1_000);
102
+ if (prepared.tps !== undefined)
103
+ result.tps = prepared.tps;
67
104
  if (ttftSamples > 0)
68
105
  result.ttft = ttft / ttftSamples;
69
106
  return result;
@@ -117,7 +154,8 @@ export class PerformanceMonitor {
117
154
  const now = this.now();
118
155
  const changed = new Set();
119
156
  for (const [key, step] of this.active) {
120
- if (step.finishedAt !== undefined && step.finishedAt + this.retention <= now) {
157
+ if ((step.finishedAt ?? step.reconciledAt) !== undefined
158
+ && (step.finishedAt ?? step.reconciledAt) + this.retention <= now) {
121
159
  this.active.delete(key);
122
160
  changed.add(step.sessionID);
123
161
  }
@@ -136,8 +174,9 @@ export class PerformanceMonitor {
136
174
  return;
137
175
  let next = Infinity;
138
176
  for (const step of this.active.values()) {
139
- if (step.finishedAt !== undefined)
140
- next = Math.min(next, step.finishedAt + this.retention);
177
+ const settled = step.finishedAt ?? step.reconciledAt;
178
+ if (settled !== undefined)
179
+ next = Math.min(next, settled + this.retention);
141
180
  }
142
181
  for (const sample of this.observed.values()) {
143
182
  if (sample.expiresAt !== undefined)
@@ -175,7 +214,7 @@ export class PerformanceMonitor {
175
214
  if (this.active.has(key))
176
215
  return false;
177
216
  this.active.set(key, {
178
- messageID, sessionID, started: data.started, bytes: 0, events: new Set(),
217
+ messageID, sessionID, started: data.started, bytes: 0, samples: [], events: new Set(),
179
218
  });
180
219
  return false;
181
220
  }
@@ -195,6 +234,8 @@ export class PerformanceMonitor {
195
234
  const sample = this.observed.get(key);
196
235
  if (sample)
197
236
  sample.expiresAt = step.finishedAt + this.retention;
237
+ if (step.reconciledAt !== undefined)
238
+ this.active.delete(key);
198
239
  this.scheduleCleanup();
199
240
  return false;
200
241
  }
@@ -207,7 +248,20 @@ export class PerformanceMonitor {
207
248
  return false;
208
249
  if (event.id)
209
250
  step.events.add(event.id);
210
- step.bytes += new TextEncoder().encode(data.delta).byteLength;
251
+ if (step.reconciledAt !== undefined) {
252
+ // A later delta means the snapshot did not represent the final stream.
253
+ delete step.reconciledAt;
254
+ const sample = this.observed.get(key);
255
+ if (sample)
256
+ delete sample.expiresAt;
257
+ this.scheduleCleanup();
258
+ }
259
+ const sampleBytes = new TextEncoder().encode(data.delta).byteLength;
260
+ step.bytes += sampleBytes;
261
+ step.samples.push({ time: event.created, bytes: sampleBytes });
262
+ const cutoff = event.created - LIVE_WINDOW_MS;
263
+ while (step.samples.length > 0 && step.samples[0].time < cutoff)
264
+ step.samples.shift();
211
265
  step.last = Math.max(step.last ?? event.created, event.created);
212
266
  if (step.first === undefined) {
213
267
  step.first = event.created;
@@ -225,28 +279,50 @@ export class PerformanceMonitor {
225
279
  represented.add(message.id);
226
280
  }
227
281
  for (const [key, step] of this.active) {
228
- if (sessions.has(step.sessionID) && represented.has(step.messageID)
229
- && (step.finishedAt !== undefined || step.streamed !== undefined))
230
- this.active.delete(key);
282
+ if (sessions.has(step.sessionID) && represented.has(step.messageID)) {
283
+ if (step.finishedAt !== undefined)
284
+ this.active.delete(key);
285
+ else {
286
+ // A snapshot can arrive before step.ended. Retain the step to attach
287
+ // the real expiry later, and set a fallback if that event is lost.
288
+ step.reconciledAt ??= this.now();
289
+ const sample = this.observed.get(key);
290
+ if (sample)
291
+ sample.expiresAt ??= step.reconciledAt + this.retention;
292
+ }
293
+ }
231
294
  }
295
+ this.scheduleCleanup();
232
296
  }
233
297
  summary(messages, sessionIDs) {
298
+ return this.summaryPrepared(preparePerformance(messages), sessionIDs);
299
+ }
300
+ summaryPrepared(prepared, sessionIDs) {
234
301
  const sessions = new Set(sessionIDs);
235
302
  const samples = [...this.observed.values()].filter(sample => sessions.has(sample.sessionID));
236
- const result = historicalPerformance(messages, samples);
237
- let bytes = 0;
238
- let duration = 0;
303
+ const result = performanceWithObservations(prepared, samples);
304
+ // Live throughput covers only observable deltas after the first one, over a
305
+ // short sliding window. TTFT keeps the started -> first wait separately, and
306
+ // completed provider usage still replaces this estimate via reconcile().
307
+ let weighted = 0;
308
+ let durationTotal = 0;
239
309
  for (const step of this.active.values()) {
240
- if (!sessions.has(step.sessionID) || step.bytes <= 0)
310
+ if (!sessions.has(step.sessionID) || step.reconciledAt !== undefined)
311
+ continue;
312
+ const raw = estimatedLiveTps(step.samples);
313
+ if (raw === undefined)
241
314
  continue;
242
- const last = step.streamed ?? step.last;
243
- if (!finite(last) || last <= step.started)
315
+ const first = step.samples[0];
316
+ const last = step.samples[step.samples.length - 1];
317
+ const duration = last.time - first.time;
318
+ if (!finite(duration) || duration <= 0)
244
319
  continue;
245
- bytes += step.bytes;
246
- duration += last - step.started;
320
+ step.liveTps = smoothLiveTps(step.liveTps, raw);
321
+ weighted += step.liveTps * duration;
322
+ durationTotal += duration;
247
323
  }
248
- if (bytes > 0 && duration > 0) {
249
- result.tps = (bytes / 4) / (duration / 1_000);
324
+ if (durationTotal > 0) {
325
+ result.tps = weighted / durationTotal;
250
326
  result.tpsEstimated = true;
251
327
  }
252
328
  return result;