@glaicer/supercode-token-usage-panel 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/dist/usage-model.js +36 -26
  2. package/package.json +1 -1
@@ -6,9 +6,9 @@
6
6
  * deltas. Exposes only ready-to-render rows plus a state flag.
7
7
  *
8
8
  * OpenCode v2 shape: one assistant message = one model step, so per-step tokens
9
- * live on `message.tokens` (there are no `step-finish` parts) and TTFT is
10
- * `message.time.streamed - message.time.created` (`time.streamed` is set by
11
- * `session.step.streamed`, the first streamed token).
9
+ * live on `message.tokens` (there are no `step-finish` parts). `time.streamed`
10
+ * marks the end of the provider stream; first output comes from content-start
11
+ * events or the timestamp of the first reasoning/tool content item.
12
12
  */
13
13
 
14
14
  export const USAGE_SECTION_TITLE = "Token Usage";
@@ -180,7 +180,7 @@ function addTotals(target, source) {
180
180
  function finishedStep(message) {
181
181
  return message.tokens !== undefined || message.time.completed !== undefined;
182
182
  }
183
- function completedMetrics(messages) {
183
+ function completedMetrics(messages, firstOutputs) {
184
184
  const result = emptyMetrics();
185
185
  for (const info of messages) {
186
186
  if (info.type !== "assistant") continue;
@@ -198,24 +198,16 @@ function completedMetrics(messages) {
198
198
  if (info.tokens && !stepHasTool) {
199
199
  addCalibration(result.calibrations, modelKey(info), stepChars, info.tokens.output + info.tokens.reasoning);
200
200
  }
201
-
202
- // Without streamed timing there is no first-token sample and no decode
203
- // baseline, so the step contributes steps/calibration only.
204
201
  const streamed = info.time.streamed;
205
202
  if (!positive(streamed ?? 0)) continue;
206
- const visible = info.content.some(item => item.type === "text" || item.type === "reasoning");
207
- if (!visible) continue;
208
- const ttft = streamed - info.time.created;
209
- if (!positive(ttft)) continue;
203
+ const head = info.content[0];
204
+ const first = firstOutputs.get(info.id) ?? (head && head.type !== "text" ? head.time?.created : undefined);
205
+ if (first === undefined || first < info.time.created || first > streamed) continue;
206
+ const ttft = first - info.time.created;
210
207
  result.ttftMs += ttft;
211
208
  result.ttftCount++;
212
209
  const generated = info.tokens ? info.tokens.output + info.tokens.reasoning : 0;
213
- const tools = info.content.reduce((sum, item) => {
214
- if (item.type !== "tool" || item.state.status !== "completed") return sum;
215
- const duration = (item.time.completed ?? 0) - (item.time.ran ?? item.time.created);
216
- return positive(duration) ? sum + duration : sum;
217
- }, 0);
218
- const decode = info.time.completed - info.time.created - ttft - tools;
210
+ const decode = streamed - first;
219
211
  if (!positive(generated) || !positive(decode)) continue;
220
212
  result.generated += generated;
221
213
  result.decodeMs += decode;
@@ -282,10 +274,10 @@ async function listAllMessages(client, sessionID) {
282
274
  } while (cursor);
283
275
  return collected.sort((a, b) => (a.time.created ?? 0) - (b.time.created ?? 0));
284
276
  }
285
- async function fetchContribution(client, session) {
277
+ async function fetchContribution(client, session, firstOutputs) {
286
278
  let metrics = emptyMetrics();
287
279
  try {
288
- metrics = completedMetrics(await listAllMessages(client, session.id));
280
+ metrics = completedMetrics(await listAllMessages(client, session.id), firstOutputs);
289
281
  } catch {
290
282
  // Totals stay usable when diagnostic history cannot be read.
291
283
  }
@@ -297,7 +289,7 @@ async function fetchContribution(client, session) {
297
289
  } : {})
298
290
  };
299
291
  }
300
- async function fetchBranch(client, root) {
292
+ async function fetchBranch(client, root, firstOutputs) {
301
293
  const contributions = new Map();
302
294
  const incompleteBranches = new Set();
303
295
  const queue = [root];
@@ -306,7 +298,7 @@ async function fetchBranch(client, root) {
306
298
  const session = queue.shift();
307
299
  if (visited.has(session.id)) continue;
308
300
  visited.add(session.id);
309
- contributions.set(session.id, await fetchContribution(client, session));
301
+ contributions.set(session.id, await fetchContribution(client, session, firstOutputs));
310
302
  try {
311
303
  const children = (await client.session.list({
312
304
  parentID: session.id
@@ -342,7 +334,7 @@ function belongsToBranch(sessionID, rootID, contributions) {
342
334
  * all descendants (subagents). The walk counts each session once and keeps
343
335
  * resolved aggregates when a branch fails to resolve.
344
336
  */
345
- async function fetchFamily(client, sessionID) {
337
+ async function fetchFamily(client, sessionID, firstOutputs) {
346
338
  let root = await client.session.get({
347
339
  sessionID
348
340
  });
@@ -356,7 +348,7 @@ async function fetchFamily(client, sessionID) {
356
348
  if (!parent) throw new Error("parent session unavailable");
357
349
  root = parent;
358
350
  }
359
- return fetchBranch(client, root);
351
+ return fetchBranch(client, root, firstOutputs);
360
352
  }
361
353
 
362
354
  /**
@@ -393,6 +385,7 @@ export function createUsageModel(api, sessionId, solid) {
393
385
  const tombstones = new Set();
394
386
  /** Content items whose stream already ended; stale deltas must not restart them. */
395
387
  const endedParts = new Set();
388
+ const firstOutputs = new Map();
396
389
  let timer;
397
390
  let ttftTurnShown = false;
398
391
  let members = new Set();
@@ -499,7 +492,7 @@ export function createUsageModel(api, sessionId, solid) {
499
492
  sessionID,
500
493
  startRevision: appliedRevision
501
494
  };
502
- void fetchFamily(api.client, sessionID).then(family => {
495
+ void fetchFamily(api.client, sessionID, firstOutputs).then(family => {
503
496
  if (!isFresh(freshness)) return;
504
497
  setRemote(previous => {
505
498
  const contributions = new Map(family.contributions);
@@ -564,6 +557,7 @@ export function createUsageModel(api, sessionId, solid) {
564
557
  failedMembers.clear();
565
558
  tombstones.clear();
566
559
  endedParts.clear();
560
+ firstOutputs.clear();
567
561
  setRemote(undefined);
568
562
  solid.untrack(() => {
569
563
  const turn = scanTurnState(sessionID);
@@ -600,7 +594,7 @@ export function createUsageModel(api, sessionId, solid) {
600
594
  sessionID: memberID
601
595
  }).then(data => {
602
596
  if (!data) throw new Error("session unavailable");
603
- return fetchContribution(api.client, data);
597
+ return fetchContribution(api.client, data, firstOutputs);
604
598
  }).then(contribution => {
605
599
  if (memberRequests.get(memberID) !== memberRequest) return;
606
600
  mergeContributions(new Map([[memberID, contribution]]), freshness);
@@ -619,7 +613,7 @@ export function createUsageModel(api, sessionId, solid) {
619
613
  sessionID: branchID
620
614
  }).then(root => {
621
615
  if (!root) throw new Error("session unavailable");
622
- return fetchBranch(api.client, root);
616
+ return fetchBranch(api.client, root, firstOutputs);
623
617
  }).then(branch => {
624
618
  setRemote(previous => {
625
619
  if (!isCurrent(previous, freshness) || branchRequests.get(branchID) !== branchRequest || !previous.contributions) {
@@ -757,6 +751,7 @@ export function createUsageModel(api, sessionId, solid) {
757
751
  assistantMessageID,
758
752
  started
759
753
  } = event.data;
754
+ if (members.has(sessionID)) firstOutputs.delete(assistantMessageID);
760
755
  if (sessionID !== sessionId()) return;
761
756
  if (ttftTurnShown) return;
762
757
  ttftTurnShown = true;
@@ -779,6 +774,18 @@ export function createUsageModel(api, sessionId, solid) {
779
774
  if (sessionID !== sessionId()) return;
780
775
  if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
781
776
  });
777
+ const onOutputStarted = event => {
778
+ const {
779
+ sessionID,
780
+ assistantMessageID
781
+ } = event.data;
782
+ if (!members.has(sessionID)) return;
783
+ if (!firstOutputs.has(assistantMessageID)) firstOutputs.set(assistantMessageID, event.created);
784
+ if (sessionID === sessionId() && liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
785
+ };
786
+ const offTextStarted = api.data.on("session.text.started", onOutputStarted);
787
+ const offReasoningStarted = api.data.on("session.reasoning.started", onOutputStarted);
788
+ const offToolInputStarted = api.data.on("session.tool.input.started", onOutputStarted);
782
789
  const onStepSettled = (sessionID, assistantMessageID) => {
783
790
  if (sessionID === sessionId()) {
784
791
  if (liveTtft()?.messageID === assistantMessageID) clearLiveTtft();
@@ -882,6 +889,9 @@ export function createUsageModel(api, sessionId, solid) {
882
889
  offStepStarted();
883
890
  offExecutionStarted();
884
891
  offStepStreamed();
892
+ offTextStarted();
893
+ offReasoningStarted();
894
+ offToolInputStarted();
885
895
  offStepEnded();
886
896
  offStepFailed();
887
897
  offTextDelta();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@glaicer/supercode-token-usage-panel",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "OpenCode TUI sidebar section: cumulative token usage and cost of the current session family.",
6
6
  "type": "module",
7
7
  "license": "MIT",