opencode-usage-insights 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/core.d.ts +259 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1227 -0
- package/dist/index.js.map +16 -0
- package/dist/line.d.ts +71 -0
- package/dist/panel-build.d.ts +46 -0
- package/dist/panel-data.d.ts +216 -0
- package/dist/panel.d.ts +39 -0
- package/dist/recorder.d.ts +69 -0
- package/dist/sidebar.d.ts +25 -0
- package/dist/stats/aggregates.d.ts +283 -0
- package/dist/stats/db.d.ts +99 -0
- package/dist/stats/maintenance.d.ts +49 -0
- package/dist/stats/paths.d.ts +35 -0
- package/dist/stats/queries.d.ts +239 -0
- package/dist/text.d.ts +25 -0
- package/dist/tui.d.ts +53 -0
- package/dist/tui.js +3515 -0
- package/dist/tui.js.map +22 -0
- package/package.json +84 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
/**
|
|
3
|
+
* Read side of the stats engine: every function takes an open database and
|
|
4
|
+
* returns plain rows for the TUI panel, the report script and the NDJSON
|
|
5
|
+
* exports. Aggregations stay in SQL; comparisons and percentiles that would
|
|
6
|
+
* need window functions or custom SQLite builds are computed in JS.
|
|
7
|
+
*/
|
|
8
|
+
/** Event kind written by the recorder when a compaction completes. */
|
|
9
|
+
export declare const COMPACTION_KIND = "compaction";
|
|
10
|
+
/**
|
|
11
|
+
* Idle gap above which a cache reuse loss is attributed to provider-side
|
|
12
|
+
* expiry rather than to a mid-session event. Heuristic: the empirical TTL
|
|
13
|
+
* evidence view narrows this down with real observations.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_TTL_HINT_MS: number;
|
|
16
|
+
export type Range = {
|
|
17
|
+
from?: number | undefined;
|
|
18
|
+
to?: number | undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Panel filters. They narrow every aggregation to one model, one provider or
|
|
22
|
+
* one session, and are applied with the same `WHERE` builder the range uses so
|
|
23
|
+
* a filtered view and a drill-down agree.
|
|
24
|
+
*/
|
|
25
|
+
export type Filters = {
|
|
26
|
+
/** Model id, variant excluded. */
|
|
27
|
+
modelID?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* One model *and* its variant, as `modelKey` builds it (`m1#max`). A variant
|
|
30
|
+
* is a different model to the provider, so a drill-down into one variant must
|
|
31
|
+
* not pick up its family.
|
|
32
|
+
*/
|
|
33
|
+
modelKey?: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* The provider the model ran through, as OpenCode configured it
|
|
36
|
+
* (`opencode-go`, `deepseek`, `deepinfra`). It is the identity every view
|
|
37
|
+
* groups by: a gateway may re-route the request, but the upstream that
|
|
38
|
+
* actually ran it is not observable.
|
|
39
|
+
*/
|
|
40
|
+
provider?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* One session, exactly. Used by the `session` range and by the overview's
|
|
43
|
+
* "this session" block, which must not pick up the steps of every other
|
|
44
|
+
* session that happened to run after it started.
|
|
45
|
+
*/
|
|
46
|
+
sessionID?: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
export type Usage = {
|
|
49
|
+
steps: number;
|
|
50
|
+
input: number;
|
|
51
|
+
cached: number;
|
|
52
|
+
cacheWrite: number;
|
|
53
|
+
output: number;
|
|
54
|
+
reasoning: number;
|
|
55
|
+
cost: number;
|
|
56
|
+
};
|
|
57
|
+
export declare const STEP_TIME = "COALESCE(created_at, recorded_at)";
|
|
58
|
+
/**
|
|
59
|
+
* The identity of a model *and* its variant, in SQL: `modelKey` says a variant
|
|
60
|
+
* is a different model to the provider, so a variant never shares a row with
|
|
61
|
+
* its family. The JS twin is `modelKey` below; both have to agree.
|
|
62
|
+
*/
|
|
63
|
+
export declare const MODEL_KEY_SQL = "COALESCE(model_id, '(unknown)') || '#' || COALESCE(variant, '')";
|
|
64
|
+
/** `modelKey(modelID, variant)`: the family, with the variant as a suffix. */
|
|
65
|
+
export declare function modelKey(modelID: string, variant: string | undefined): string;
|
|
66
|
+
/** Human label of a model key or a model id and variant. */
|
|
67
|
+
export declare function modelLabel(modelID: string, variant: string | undefined): string;
|
|
68
|
+
/**
|
|
69
|
+
* `WHERE` for a range plus the panel filters. Shared with the analysis layer so
|
|
70
|
+
* a filtered view and a drill-down agree on what is in scope.
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildWhere(range: Range | undefined, filters?: Filters | undefined, column?: string): {
|
|
73
|
+
where: string;
|
|
74
|
+
params: (string | number)[];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Share of the whole prompt that came from the cache. The denominator is what
|
|
78
|
+
* the provider saw: cache reads, fresh input and cache writes.
|
|
79
|
+
*/
|
|
80
|
+
export declare function cacheHitRatio(usage: Pick<Usage, "input" | "cached" | "cacheWrite">): number | undefined;
|
|
81
|
+
export type Overview = Usage & {
|
|
82
|
+
sessions: number;
|
|
83
|
+
prompts: number;
|
|
84
|
+
firstAt: number | undefined;
|
|
85
|
+
lastAt: number | undefined;
|
|
86
|
+
cacheHitRatio: number | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Tokens generated per second of streaming, over the whole selection: the
|
|
89
|
+
* produced tokens divided by the summed streaming windows. A weighted average,
|
|
90
|
+
* not the mean of the per-step rates the latency view reports as p50.
|
|
91
|
+
*/
|
|
92
|
+
tps: number | undefined;
|
|
93
|
+
};
|
|
94
|
+
export declare function overview(db: Database, range?: Range, filters?: Filters): Overview;
|
|
95
|
+
export type ProviderUsage = Usage & {
|
|
96
|
+
providerID: string;
|
|
97
|
+
firstAt: number | undefined;
|
|
98
|
+
lastAt: number | undefined;
|
|
99
|
+
};
|
|
100
|
+
export type ModelUsage = {
|
|
101
|
+
modelID: string;
|
|
102
|
+
variant: string | undefined;
|
|
103
|
+
providers: ProviderUsage[];
|
|
104
|
+
totals: Usage;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Usage per model, each with the providers that served it. This is the view
|
|
108
|
+
* that answers "through which provider is this model actually coming, over the
|
|
109
|
+
* selected period".
|
|
110
|
+
*/
|
|
111
|
+
export declare function models(db: Database, range?: Range, filters?: Filters): ModelUsage[];
|
|
112
|
+
export type TimelineBucket = {
|
|
113
|
+
at: number;
|
|
114
|
+
} & Usage;
|
|
115
|
+
/** Usage aggregated into fixed time buckets (for example one hour or day). */
|
|
116
|
+
export declare function timeline(db: Database, range: Range | undefined, bucketMs: number): TimelineBucket[];
|
|
117
|
+
export type DropReason = "compaction" | "expired" | "unknown";
|
|
118
|
+
export type CacheDropRow = {
|
|
119
|
+
sessionID: string;
|
|
120
|
+
messageID: string;
|
|
121
|
+
providerID: string | undefined;
|
|
122
|
+
modelID: string | undefined;
|
|
123
|
+
variant: string | undefined;
|
|
124
|
+
at: number;
|
|
125
|
+
tokens: number;
|
|
126
|
+
gapMs: number | undefined;
|
|
127
|
+
prevCached: number;
|
|
128
|
+
reason: DropReason;
|
|
129
|
+
};
|
|
130
|
+
export type TtlEvidenceRow = {
|
|
131
|
+
providerID: string;
|
|
132
|
+
modelID: string;
|
|
133
|
+
samples: number;
|
|
134
|
+
/** Largest idle gap that still hit the cache. */
|
|
135
|
+
maxHitGapMs: number | undefined;
|
|
136
|
+
/** Smallest idle gap after which reuse was lost. */
|
|
137
|
+
minDropGapMs: number | undefined;
|
|
138
|
+
};
|
|
139
|
+
export type LatencyStats = {
|
|
140
|
+
count: number;
|
|
141
|
+
p50: number | undefined;
|
|
142
|
+
p90: number | undefined;
|
|
143
|
+
p99: number | undefined;
|
|
144
|
+
};
|
|
145
|
+
export type LatencyReport = {
|
|
146
|
+
/** Milliseconds from request to completed response. */
|
|
147
|
+
wall: LatencyStats;
|
|
148
|
+
/** Milliseconds from request to the last streamed chunk (the streaming window). */
|
|
149
|
+
stream: LatencyStats;
|
|
150
|
+
/** Output-plus-reasoning tokens per second over that window. */
|
|
151
|
+
tps: LatencyStats;
|
|
152
|
+
};
|
|
153
|
+
/** Nearest-rank percentile over an unsorted sample. */
|
|
154
|
+
export declare function percentile(values: readonly number[], p: number): number | undefined;
|
|
155
|
+
export declare function latencyStats(values: number[]): LatencyStats;
|
|
156
|
+
export type SessionUsage = Usage & {
|
|
157
|
+
sessionID: string;
|
|
158
|
+
parentID: string | undefined;
|
|
159
|
+
title: string | undefined;
|
|
160
|
+
directory: string | undefined;
|
|
161
|
+
providers: string[];
|
|
162
|
+
models: string[];
|
|
163
|
+
prompts: number;
|
|
164
|
+
firstAt: number | undefined;
|
|
165
|
+
lastAt: number | undefined;
|
|
166
|
+
cacheHitRatio: number | undefined;
|
|
167
|
+
};
|
|
168
|
+
export declare function sessions(db: Database, options?: {
|
|
169
|
+
range?: Range;
|
|
170
|
+
filters?: Filters;
|
|
171
|
+
limit?: number;
|
|
172
|
+
sessionIDs?: readonly string[];
|
|
173
|
+
}): SessionUsage[];
|
|
174
|
+
/** Sessions whose parent is `parentID`: the subagents of one session. */
|
|
175
|
+
export declare function childSessionIDs(db: Database, parentID: string): string[];
|
|
176
|
+
/** Session usage rows for a specific set of sessions (subagent rollups). */
|
|
177
|
+
export declare function sessionUsageOf(db: Database, sessionIDs: readonly string[]): SessionUsage[];
|
|
178
|
+
export type TurnDrop = {
|
|
179
|
+
messageID: string;
|
|
180
|
+
tokens: number;
|
|
181
|
+
gapMs: number | undefined;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* One finished turn of a session, built from the recorded steps and prompts. It
|
|
185
|
+
* carries the same buckets the telemetry line uses, so the panel can render the
|
|
186
|
+
* per-prompt history with the line's own formatter.
|
|
187
|
+
*/
|
|
188
|
+
export type TurnUsage = {
|
|
189
|
+
sessionID: string;
|
|
190
|
+
/** Creation time of the prompt that opened the turn, when it was recorded. */
|
|
191
|
+
promptAt: number | undefined;
|
|
192
|
+
endAt: number | undefined;
|
|
193
|
+
/** Time between the previous prompt of the session and this turn's prompt. */
|
|
194
|
+
promptGapMs: number | undefined;
|
|
195
|
+
providerID: string | undefined;
|
|
196
|
+
modelID: string | undefined;
|
|
197
|
+
variant: string | undefined;
|
|
198
|
+
providerChanged: boolean;
|
|
199
|
+
modelChanged: boolean;
|
|
200
|
+
steps: number;
|
|
201
|
+
/** Raw buckets of the last step: the prompt the turn ended with. */
|
|
202
|
+
latest: Usage;
|
|
203
|
+
/** Raw buckets summed over every step of the turn. */
|
|
204
|
+
totals: Usage;
|
|
205
|
+
drops: TurnDrop[];
|
|
206
|
+
/** Finish reason of the turn's last step. */
|
|
207
|
+
finish: string | undefined;
|
|
208
|
+
/**
|
|
209
|
+
* Tokens generated per second of streaming over the turn's steps: produced
|
|
210
|
+
* tokens divided by their summed streaming windows. For a one-step turn it is
|
|
211
|
+
* that step's rate.
|
|
212
|
+
*/
|
|
213
|
+
tps: number | undefined;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Finished turns, newest first. Steps belong to the newest prompt recorded at or
|
|
217
|
+
* before them; a turn whose last step asked for tools is dropped, because it is
|
|
218
|
+
* still running — the same rule the telemetry line follows.
|
|
219
|
+
*
|
|
220
|
+
* For one session the read is bounded by a window: only the steps at or after
|
|
221
|
+
* the `limit`-th newest prompt are loaded, so a session with tens of thousands
|
|
222
|
+
* of steps costs the same as a short one. The extra prompt is kept as the
|
|
223
|
+
* reference the oldest returned turn measures its gap against.
|
|
224
|
+
*/
|
|
225
|
+
export declare function turns(db: Database, options?: {
|
|
226
|
+
sessionID?: string | undefined;
|
|
227
|
+
range?: Range | undefined;
|
|
228
|
+
filters?: Filters | undefined;
|
|
229
|
+
limit?: number | undefined;
|
|
230
|
+
}): TurnUsage[];
|
|
231
|
+
export type FilterOption = {
|
|
232
|
+
value: string;
|
|
233
|
+
steps: number;
|
|
234
|
+
};
|
|
235
|
+
/** Distinct models and providers, most used first. */
|
|
236
|
+
export declare function filterOptions(db: Database): {
|
|
237
|
+
models: FilterOption[];
|
|
238
|
+
providers: FilterOption[];
|
|
239
|
+
};
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text hygiene for everything that reaches a terminal.
|
|
3
|
+
*
|
|
4
|
+
* Some of what the plugin displays is not written by the user: a model writes
|
|
5
|
+
* a session title, an NDJSON export can come from anywhere. A control
|
|
6
|
+
* character in any of those is interpreted by the terminal that renders it
|
|
7
|
+
* (title changes, cursor moves, colours), so every such string is stripped of
|
|
8
|
+
* control characters before it is stored or drawn.
|
|
9
|
+
*
|
|
10
|
+
* The rule is deliberately blunt: drop C0 controls, DEL and the C1 block, keep
|
|
11
|
+
* everything else (the text is otherwise arbitrary Unicode), and cap the
|
|
12
|
+
* length so a hostile value cannot bloat the database or the frame. Layout is
|
|
13
|
+
* never touched: whitespace is preserved, because callers build indentation
|
|
14
|
+
* into the strings they truncate.
|
|
15
|
+
*/
|
|
16
|
+
/** Longest value kept by default: a title, an id or a path, not a document. */
|
|
17
|
+
export declare const TEXT_LIMIT = 512;
|
|
18
|
+
/**
|
|
19
|
+
* Strip control characters and cap the length. Newlines and tabs go too: a
|
|
20
|
+
* line is one line, and the renderers lay out their own spacing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sanitizeText(value: string, limit?: number): string;
|
|
23
|
+
/** `sanitizeText` without the layout, for a stored label: trimmed as well. */
|
|
24
|
+
export declare function sanitizeLabel(value: string, limit?: number): string;
|
|
25
|
+
export declare function isToken(value: string, limit?: number): boolean;
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
3
|
+
import { type LineFields, type LineLabels, type TurnStats, type TurnTotalsView } from "./core";
|
|
4
|
+
import { type PanelView, type RangeKey } from "./panel-data";
|
|
5
|
+
/**
|
|
6
|
+
* TUI side of the plugin. Two contributions:
|
|
7
|
+
*
|
|
8
|
+
* - `session.composer.top`: one telemetry line after every turn with the end
|
|
9
|
+
* time, the delta since the previous prompt, the provider and model that ran
|
|
10
|
+
* it, turn and session token totals, and cache warnings.
|
|
11
|
+
* - `session.panel` + the `/stats` slash command: the statistics panel over
|
|
12
|
+
* the database written by the server entry.
|
|
13
|
+
*
|
|
14
|
+
* The line is computed from the host's transcript alone; the database is opened
|
|
15
|
+
* lazily for the panel and the sidebar, read-only in spirit: the TUI never
|
|
16
|
+
* writes usage. When the database cannot be opened, the line still works and
|
|
17
|
+
* the panel explains itself.
|
|
18
|
+
*/
|
|
19
|
+
export type Options = {
|
|
20
|
+
/** Turn the TUI side off: no line and no panel. */
|
|
21
|
+
enabled?: boolean | undefined;
|
|
22
|
+
/** SQLite database path override; must match the server entry. */
|
|
23
|
+
db?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* How the turn segment counts prompt usage: `latest` (default) shows the
|
|
26
|
+
* prompt size the turn ended with, `sum` adds every step of the turn.
|
|
27
|
+
*/
|
|
28
|
+
turnTotals?: TurnTotalsView | undefined;
|
|
29
|
+
/** Telemetry line field toggles. */
|
|
30
|
+
fields?: Partial<LineFields> | undefined;
|
|
31
|
+
/** Telemetry line label overrides. */
|
|
32
|
+
labels?: Partial<LineLabels> | undefined;
|
|
33
|
+
/** Show the per-prompt history in the session sidebar (default true). */
|
|
34
|
+
sidebar?: boolean | undefined;
|
|
35
|
+
/** How many turns the sidebar history shows (default 5). */
|
|
36
|
+
sidebarTurns?: number | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Show the `/usage` statistics panel and its command (default true).
|
|
39
|
+
* `false` keeps the live line and the sidebar history.
|
|
40
|
+
*/
|
|
41
|
+
panel?: boolean | undefined;
|
|
42
|
+
};
|
|
43
|
+
export declare const __test: {
|
|
44
|
+
line: ((sessionID: string) => TurnStats | undefined) | undefined;
|
|
45
|
+
lineText: ((sessionID: string) => string) | undefined;
|
|
46
|
+
open: ((input?: string) => boolean) | undefined;
|
|
47
|
+
state: (() => {
|
|
48
|
+
view: PanelView;
|
|
49
|
+
range: RangeKey;
|
|
50
|
+
}) | undefined;
|
|
51
|
+
};
|
|
52
|
+
declare const _default: Plugin.Definition;
|
|
53
|
+
export default _default;
|