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.
@@ -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;