@rejacky/opencode-insights 0.4.0 → 0.4.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.
|
@@ -16,6 +16,19 @@ type SessionAverage = {
|
|
|
16
16
|
totalTtftMs: number;
|
|
17
17
|
messageCount: number;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Per-message metrics for input-box (current round).
|
|
21
|
+
* AVG = totalTokens(=output+reasoning)/duration where duration = firstTokenAt→endAt (fallback completed-created if hydrated).
|
|
22
|
+
* TTFT = firstTokenAt - requestStartAt. TPS = live estimated tokens / active burst duration (5s window) for this messageID only.
|
|
23
|
+
* Session totals (Token Usage sidebar) remain in SessionTokenUsage/sessionAverageByID (deprecated for prompt-right).
|
|
24
|
+
*/
|
|
25
|
+
type MessageMetrics = {
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
ttftMs: number | undefined;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
completedAt: number;
|
|
31
|
+
};
|
|
19
32
|
type AssistantResponseUsage = {
|
|
20
33
|
inputTokens?: number | undefined;
|
|
21
34
|
outputTokens?: number | undefined;
|
|
@@ -37,7 +50,10 @@ type PromptRightMetric = "tps" | "avg" | "ttft" | "used" | "cache" | "input" | "
|
|
|
37
50
|
declare const DEFAULT_PROMPT_RIGHT_METRICS: PromptRightMetric[];
|
|
38
51
|
type MetricsState = {
|
|
39
52
|
streamSamplesBySession: Record<string, StreamSample[]>;
|
|
53
|
+
streamSamplesByMessageID: Record<string, StreamSample[]>;
|
|
40
54
|
messageTimingByID: Record<string, MessageTiming>;
|
|
55
|
+
messageMetricsByID: Record<string, MessageMetrics>;
|
|
56
|
+
latestMessageIDBySession: Record<string, string>;
|
|
41
57
|
sessionAverageByID: Record<string, SessionAverage>;
|
|
42
58
|
latestResponseUsageBySession: Record<string, AssistantResponseUsage>;
|
|
43
59
|
responseUsageByMessageID: Record<string, {
|
|
@@ -47,6 +63,10 @@ type MetricsState = {
|
|
|
47
63
|
sessionTokenUsageByID: Record<string, SessionTokenUsage>;
|
|
48
64
|
};
|
|
49
65
|
declare function createMetricsState(): MetricsState;
|
|
66
|
+
/**
|
|
67
|
+
* Estimated tokens for live TPS: ceil(bytes/5). Differs from per-message AVG which uses real output+reasoning tokens.
|
|
68
|
+
* Multibyte (CJK/emoji) inflates bytes but may over/undercount vs real tokenizer.
|
|
69
|
+
*/
|
|
50
70
|
declare function estimateStreamTokens(delta: string): number;
|
|
51
71
|
declare function recordAssistantMessage(state: MetricsState, input: {
|
|
52
72
|
sessionID: string;
|
|
@@ -163,4 +183,4 @@ declare class SqliteCaptureStore implements CaptureStore {
|
|
|
163
183
|
declare function createCaptureStore(options?: InsightsOptions): CaptureStore;
|
|
164
184
|
declare function resolveRetentionDays(value: unknown): number;
|
|
165
185
|
|
|
166
|
-
export { type AssistantResponseUsage as A,
|
|
186
|
+
export { type AssistantResponseUsage as A, recordAssistantDelta as B, type CaptureRecord as C, DEFAULT_PROMPT_RIGHT_METRICS as D, recordAssistantMessage as E, recordToolActivity as F, type GoUsageConfig as G, renderMetricsText as H, type InsightsConfig as I, JsonlCaptureStore as J, renderPromptRightMetricsText as K, renderResponseMetricsText as L, type MessageMetrics as M, renderSessionTokenUsage as N, resolveCapturePath as O, type PromptRightMetric as P, resolveCopilotToken as Q, resolveInsightsConfigPath as R, type SessionAverage as S, resolveLegacyInsightsConfigPath as T, resolveRetentionDays as U, type CaptureKind as a, type CaptureStore as b, type CopilotUsageConfig as c, type InsightsOptions as d, type MessageTiming as e, type MetricsState as f, type SessionTokenUsage as g, SqliteCaptureStore as h, type SqliteDb as i, type StreamSample as j, createCaptureStore as k, createMetricsState as l, defaultDataDir as m, estimateStreamTokens as n, extractEventType as o, getSessionTokenUsage as p, insightsOptionsFromConfig as q, normalizeChatHeadersCapture as r, normalizeChatMessageCapture as s, normalizeChatParamsCapture as t, normalizeEventCapture as u, normalizeExperimentalChatMessagesTransformCapture as v, normalizeExperimentalChatSystemTransformCapture as w, normalizeToolCapture as x, openDatabase as y, readInsightsConfig as z };
|
|
@@ -6,7 +6,10 @@ var SINGLE_SAMPLE_MS = 1e3;
|
|
|
6
6
|
function createMetricsState() {
|
|
7
7
|
return {
|
|
8
8
|
streamSamplesBySession: {},
|
|
9
|
+
streamSamplesByMessageID: {},
|
|
9
10
|
messageTimingByID: {},
|
|
11
|
+
messageMetricsByID: {},
|
|
12
|
+
latestMessageIDBySession: {},
|
|
10
13
|
sessionAverageByID: {},
|
|
11
14
|
latestResponseUsageBySession: {},
|
|
12
15
|
responseUsageByMessageID: {},
|
|
@@ -27,6 +30,7 @@ function recordAssistantMessage(state, input) {
|
|
|
27
30
|
lastTokenAt: existing?.lastTokenAt,
|
|
28
31
|
lastToolCallAt: existing?.lastToolCallAt
|
|
29
32
|
};
|
|
33
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
const usage = compactUsage({
|
|
@@ -42,13 +46,33 @@ function recordAssistantMessage(state, input) {
|
|
|
42
46
|
state.responseUsageByMessageID[input.messageID] = { sessionID: input.sessionID, usage };
|
|
43
47
|
rebuildSessionTokenUsage(state, input.sessionID);
|
|
44
48
|
}
|
|
49
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
45
50
|
const timing = state.messageTimingByID[input.messageID];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
51
|
+
const totalTokens = (input.outputTokens ?? 0) + (input.reasoningTokens ?? 0);
|
|
52
|
+
let durationMs;
|
|
53
|
+
let ttftMs;
|
|
54
|
+
if (timing?.sessionID === input.sessionID && typeof (timing.firstTokenAt ?? timing.firstResponseAt) === "number") {
|
|
55
|
+
const first = timing.firstTokenAt ?? timing.firstResponseAt;
|
|
56
|
+
const endAt = input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt;
|
|
57
|
+
durationMs = typeof endAt === "number" ? Math.max(endAt - first, 1) : void 0;
|
|
58
|
+
ttftMs = Math.max(first - timing.requestStartAt, 0);
|
|
59
|
+
} else {
|
|
60
|
+
durationMs = Math.max(input.completedAt - input.createdAt, 1);
|
|
61
|
+
ttftMs = void 0;
|
|
62
|
+
}
|
|
63
|
+
if (totalTokens > 0 && durationMs) {
|
|
64
|
+
state.messageMetricsByID[input.messageID] = {
|
|
65
|
+
totalTokens,
|
|
66
|
+
durationMs,
|
|
67
|
+
ttftMs,
|
|
68
|
+
createdAt: input.createdAt,
|
|
69
|
+
completedAt: input.completedAt
|
|
70
|
+
};
|
|
71
|
+
if (ttftMs !== void 0) {
|
|
72
|
+
const firstForSession = timing?.firstTokenAt ?? timing?.firstResponseAt;
|
|
73
|
+
const endForSession = timing ? input.finish === "tool-calls" ? timing.lastToolCallAt ?? input.completedAt : input.completedAt : input.completedAt;
|
|
74
|
+
const sessionDuration = timing && firstForSession && typeof endForSession === "number" ? Math.max(endForSession - firstForSession, 1) : durationMs;
|
|
75
|
+
const sessionTtft2 = ttftMs;
|
|
52
76
|
const totals = state.sessionAverageByID[input.sessionID] ?? {
|
|
53
77
|
totalTokens: 0,
|
|
54
78
|
totalDurationMs: 0,
|
|
@@ -57,13 +81,27 @@ function recordAssistantMessage(state, input) {
|
|
|
57
81
|
};
|
|
58
82
|
state.sessionAverageByID[input.sessionID] = {
|
|
59
83
|
totalTokens: totals.totalTokens + totalTokens,
|
|
60
|
-
totalDurationMs: totals.totalDurationMs +
|
|
61
|
-
totalTtftMs: totals.totalTtftMs +
|
|
84
|
+
totalDurationMs: totals.totalDurationMs + sessionDuration,
|
|
85
|
+
totalTtftMs: totals.totalTtftMs + sessionTtft2,
|
|
62
86
|
messageCount: totals.messageCount + 1
|
|
63
87
|
};
|
|
88
|
+
} else {
|
|
89
|
+
const totals = state.sessionAverageByID[input.sessionID] ?? {
|
|
90
|
+
totalTokens: 0,
|
|
91
|
+
totalDurationMs: 0,
|
|
92
|
+
totalTtftMs: 0,
|
|
93
|
+
messageCount: 0
|
|
94
|
+
};
|
|
95
|
+
state.sessionAverageByID[input.sessionID] = {
|
|
96
|
+
totalTokens: totals.totalTokens + totalTokens,
|
|
97
|
+
totalDurationMs: totals.totalDurationMs + durationMs,
|
|
98
|
+
totalTtftMs: totals.totalTtftMs,
|
|
99
|
+
messageCount: totals.messageCount
|
|
100
|
+
};
|
|
64
101
|
}
|
|
65
102
|
}
|
|
66
103
|
delete state.messageTimingByID[input.messageID];
|
|
104
|
+
delete state.streamSamplesByMessageID[input.messageID];
|
|
67
105
|
pruneSamples(state, input.completedAt);
|
|
68
106
|
}
|
|
69
107
|
function recordAssistantDelta(state, input) {
|
|
@@ -71,10 +109,15 @@ function recordAssistantDelta(state, input) {
|
|
|
71
109
|
at: input.at,
|
|
72
110
|
tokens: estimateStreamTokens(input.delta)
|
|
73
111
|
};
|
|
112
|
+
state.streamSamplesByMessageID[input.messageID] = [
|
|
113
|
+
...(state.streamSamplesByMessageID[input.messageID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
|
|
114
|
+
sample
|
|
115
|
+
];
|
|
74
116
|
state.streamSamplesBySession[input.sessionID] = [
|
|
75
117
|
...(state.streamSamplesBySession[input.sessionID] ?? []).filter((item) => input.at - item.at <= STREAM_WINDOW_MS),
|
|
76
118
|
sample
|
|
77
119
|
];
|
|
120
|
+
state.latestMessageIDBySession[input.sessionID] = input.messageID;
|
|
78
121
|
const timing = state.messageTimingByID[input.messageID];
|
|
79
122
|
if (timing) {
|
|
80
123
|
state.messageTimingByID[input.messageID] = timing.firstTokenAt ? { ...timing, lastTokenAt: input.at } : {
|
|
@@ -86,6 +129,9 @@ function recordAssistantDelta(state, input) {
|
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
|
|
132
|
+
if (state.streamSamplesByMessageID[messageID]?.length) {
|
|
133
|
+
delete state.streamSamplesByMessageID[messageID];
|
|
134
|
+
}
|
|
89
135
|
if (state.streamSamplesBySession[sessionID]?.length) {
|
|
90
136
|
delete state.streamSamplesBySession[sessionID];
|
|
91
137
|
}
|
|
@@ -100,8 +146,10 @@ function recordToolActivity(state, sessionID, messageID, at = Date.now()) {
|
|
|
100
146
|
}
|
|
101
147
|
function renderMetricsText(state, sessionID, options = {}) {
|
|
102
148
|
const live = liveTps(state, sessionID, options) ?? "-";
|
|
103
|
-
const
|
|
104
|
-
const
|
|
149
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
150
|
+
const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
|
|
151
|
+
const avg = hasPerMessage ? messageAverage(state, sessionID) ?? "-" : sessionAverage(state, sessionID) ?? "-";
|
|
152
|
+
const ttft = hasPerMessage ? messageTtft(state, sessionID) ?? "-" : sessionTtft(state, sessionID) ?? "-";
|
|
105
153
|
return `TPS ${live} | AVG ${avg} | TTFT ${ttft}`;
|
|
106
154
|
}
|
|
107
155
|
function renderResponseMetricsText(state, sessionID) {
|
|
@@ -121,10 +169,14 @@ function renderPromptRightMetricsText(state, sessionID, options = {}) {
|
|
|
121
169
|
const usage = state.latestResponseUsageBySession[sessionID];
|
|
122
170
|
const used = usage ? sumTokens(usage) : void 0;
|
|
123
171
|
const cacheRate = usage ? cacheReadRate(usage) : void 0;
|
|
172
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
173
|
+
const hasPerMessage = !!msgID && !!state.messageMetricsByID[msgID];
|
|
174
|
+
const avgVal = hasPerMessage ? messageAverage(state, sessionID) : sessionAverage(state, sessionID);
|
|
175
|
+
const ttftVal = hasPerMessage ? messageTtft(state, sessionID) : sessionTtft(state, sessionID);
|
|
124
176
|
const values = {
|
|
125
177
|
tps: `TPS ${liveTps(state, sessionID, options) ?? "-"}`,
|
|
126
|
-
avg: `AVG ${
|
|
127
|
-
ttft: `TTFT ${
|
|
178
|
+
avg: `AVG ${avgVal ?? "-"}`,
|
|
179
|
+
ttft: `TTFT ${ttftVal ?? "-"}`,
|
|
128
180
|
used: `${used === void 0 ? "-" : formatTokenCount(used)} used`,
|
|
129
181
|
cache: `${cacheRate === void 0 ? "-" : formatPercent(cacheRate)} cache`,
|
|
130
182
|
input: `${usage?.inputTokens === void 0 ? "-" : formatTokenCount(usage.inputTokens)} in`,
|
|
@@ -160,6 +212,11 @@ function renderSessionTokenUsage(state, sessionID, subagentTokens = 0) {
|
|
|
160
212
|
return lines.join("\n");
|
|
161
213
|
}
|
|
162
214
|
function pruneSamples(state, now = Date.now()) {
|
|
215
|
+
for (const [messageID, samples] of Object.entries(state.streamSamplesByMessageID)) {
|
|
216
|
+
const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
217
|
+
if (next.length > 0) state.streamSamplesByMessageID[messageID] = next;
|
|
218
|
+
else delete state.streamSamplesByMessageID[messageID];
|
|
219
|
+
}
|
|
163
220
|
for (const [sessionID, samples] of Object.entries(state.streamSamplesBySession)) {
|
|
164
221
|
const next = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
165
222
|
if (next.length > 0) state.streamSamplesBySession[sessionID] = next;
|
|
@@ -188,6 +245,20 @@ function rebuildSessionTokenUsage(state, sessionID) {
|
|
|
188
245
|
usage.totalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
189
246
|
state.sessionTokenUsageByID[sessionID] = usage;
|
|
190
247
|
}
|
|
248
|
+
function messageAverage(state, sessionID) {
|
|
249
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
250
|
+
if (!msgID) return void 0;
|
|
251
|
+
const m = state.messageMetricsByID[msgID];
|
|
252
|
+
if (!m || m.totalTokens <= 0 || m.durationMs <= 0) return void 0;
|
|
253
|
+
return formatRate(m.totalTokens / (m.durationMs / 1e3), "AVG");
|
|
254
|
+
}
|
|
255
|
+
function messageTtft(state, sessionID) {
|
|
256
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
257
|
+
if (!msgID) return void 0;
|
|
258
|
+
const m = state.messageMetricsByID[msgID];
|
|
259
|
+
if (!m || m.ttftMs === void 0 || m.ttftMs < 0) return void 0;
|
|
260
|
+
return formatTtft(m.ttftMs / 1e3);
|
|
261
|
+
}
|
|
191
262
|
function sessionAverage(state, sessionID) {
|
|
192
263
|
const totals = state.sessionAverageByID[sessionID];
|
|
193
264
|
if (!totals || totals.totalTokens <= 0 || totals.totalDurationMs <= 0) return void 0;
|
|
@@ -201,7 +272,8 @@ function sessionTtft(state, sessionID) {
|
|
|
201
272
|
function liveTps(state, sessionID, options = {}) {
|
|
202
273
|
if (options.idle) return void 0;
|
|
203
274
|
const now = options.now ?? Date.now();
|
|
204
|
-
const
|
|
275
|
+
const msgID = state.latestMessageIDBySession[sessionID];
|
|
276
|
+
const samples = (msgID ? state.streamSamplesByMessageID[msgID] : void 0) ?? state.streamSamplesBySession[sessionID] ?? [];
|
|
205
277
|
const relevant = samples.filter((sample) => now - sample.at <= STREAM_WINDOW_MS);
|
|
206
278
|
if (relevant.length === 0) return void 0;
|
|
207
279
|
const lastSample = relevant.at(-1);
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { TuiPlugin } from '@opencode-ai/plugin/tui';
|
|
3
|
-
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as
|
|
3
|
+
export { A as AssistantResponseUsage, a as CaptureKind, C as CaptureRecord, b as CaptureStore, c as CopilotUsageConfig, D as DEFAULT_PROMPT_RIGHT_METRICS, G as GoUsageConfig, I as InsightsConfig, d as InsightsOptions, J as JsonlCaptureStore, M as MessageMetrics, e as MessageTiming, f as MetricsState, P as PromptRightMetric, S as SessionAverage, g as SessionTokenUsage, h as SqliteCaptureStore, i as SqliteDb, j as StreamSample, k as createCaptureStore, l as createMetricsState, m as defaultDataDir, n as estimateStreamTokens, o as extractEventType, p as getSessionTokenUsage, q as insightsOptionsFromConfig, r as normalizeChatHeadersCapture, s as normalizeChatMessageCapture, t as normalizeChatParamsCapture, u as normalizeEventCapture, v as normalizeExperimentalChatMessagesTransformCapture, w as normalizeExperimentalChatSystemTransformCapture, x as normalizeToolCapture, y as openDatabase, z as readInsightsConfig, B as recordAssistantDelta, E as recordAssistantMessage, F as recordToolActivity, H as renderMetricsText, K as renderPromptRightMetricsText, L as renderResponseMetricsText, N as renderSessionTokenUsage, O as resolveCapturePath, Q as resolveCopilotToken, R as resolveInsightsConfigPath, T as resolveLegacyInsightsConfigPath, U as resolveRetentionDays } from './capture-An0IKDqr.js';
|
|
4
4
|
|
|
5
5
|
type SessionActivity = {
|
|
6
6
|
toolCalls: number;
|
package/dist/index.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
renderPromptRightMetricsText,
|
|
25
25
|
renderSessionTokenUsage,
|
|
26
26
|
resolveCopilotToken
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-2HZVJIOC.js";
|
|
28
28
|
|
|
29
29
|
// src/tui.tsx
|
|
30
30
|
import { createTextAttributes, StyledText } from "@opentui/core";
|
|
@@ -815,7 +815,11 @@ var tui = async (api, options) => {
|
|
|
815
815
|
hydratedSessions.add(sessionID);
|
|
816
816
|
try {
|
|
817
817
|
const response = await api.client.session.messages({ sessionID });
|
|
818
|
-
const messages = response.data ?? []
|
|
818
|
+
const messages = (response.data ?? []).slice().sort((a, b) => {
|
|
819
|
+
const aCreated = a.info.time?.created;
|
|
820
|
+
const bCreated = b.info.time?.created;
|
|
821
|
+
return (typeof aCreated === "number" ? aCreated : 0) - (typeof bCreated === "number" ? bCreated : 0);
|
|
822
|
+
});
|
|
819
823
|
for (const message of messages) {
|
|
820
824
|
const info = message.info;
|
|
821
825
|
const providerID = info.providerID;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@rejacky/opencode-insights",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "OpenCode plugin for local request capture, TPS metrics, and subagent status visibility.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "opencode-insights contributors",
|