@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.
- package/dist/usage-model.js +36 -26
- package/package.json +1 -1
package/dist/usage-model.js
CHANGED
|
@@ -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)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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.
|
|
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",
|