@hank-warren/pi-stats 0.3.1 → 0.3.2
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/README.md +2 -2
- package/package.json +1 -1
- package/stats.ts +30 -4
- package/widget.ts +4 -4
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Run `/stats` in interactive mode. The dashboard temporarily replaces the editor
|
|
|
27
27
|
- `u`: force a full rescan and rebuild the session index
|
|
28
28
|
- `Escape`: close
|
|
29
29
|
|
|
30
|
-
The Overview compares current-session, selected-range, and all-time usage in one aligned table covering input, output, cache read, cache write, reasoning, cache-hit rate, and recorded cost, followed by a month-labelled activity heatmap and all-time session, streak, and favourite-model highlights. Reasoning is shown as a subset of output and is never added to the total twice.
|
|
30
|
+
The Overview compares current-session, selected-range, and all-time usage in one aligned table covering input, output, cache read, cache write, reasoning, cache-hit rate, and recorded cost, followed by a month-labelled activity heatmap and all-time session, streak, and favourite-model highlights. The heatmap always spans the width the terminal offers, up to a full year; weeks before your first recorded session render as empty cells and fill in as history accumulates. Reasoning is shown as a subset of output and is never added to the total twice.
|
|
31
31
|
|
|
32
32
|
The Models tab ranks models for the selected range with share bars and a totals row that reconciles with the range total. It attributes assistant usage to the provider response model and keeps otherwise unattributed nested usage in `Tools/summaries`.
|
|
33
33
|
|
|
@@ -70,7 +70,7 @@ Each variable is matched exactly: `PI_STATS_DISABLE_USAGE_SIDECARS=true` does no
|
|
|
70
70
|
|
|
71
71
|
**Guardian or other extension usage is missing.** Sidecar reading is skipped entirely when `PI_STATS_DISABLE_USAGE_SIDECARS=1`. Otherwise confirm the writing extension is enabled and that `<agent dir>/<extension>/usage.jsonl` exists; sidecars are only discovered one level below the agent directory.
|
|
72
72
|
|
|
73
|
-
**A model shows under `Tools/summaries` instead of its own row.** That bucket holds nested usage that carries no attributable response model, such as some compaction and branch-summary records. It is counted in the totals either way.
|
|
73
|
+
**A model shows under `Tools/summaries` instead of its own row.** That bucket holds nested usage that carries no attributable response model, such as some compaction and branch-summary records. It also absorbs Pi's own placeholder turns — assistant messages whose model is `<synthetic>` or missing — but only when they recorded no tokens and no cost; a placeholder that was actually billed keeps its own row so the spend stays visible. Everything in the bucket is counted in the totals either way.
|
|
74
74
|
|
|
75
75
|
**Costs read `$0.00` or look wrong.** Displayed cost is whatever Pi already recorded in the session; `pi-stats` performs no pricing lookup and contacts no billing API. Providers used through a subscription or proxy commonly record zero cost.
|
|
76
76
|
|
package/package.json
CHANGED
package/stats.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
type UsageTotals,
|
|
15
15
|
} from "./types.ts";
|
|
16
16
|
|
|
17
|
+
/** Bucket for usage that carries no attributable response model. */
|
|
18
|
+
const SUMMARY_MODEL = "Tools/summaries";
|
|
19
|
+
|
|
17
20
|
interface ParseResult {
|
|
18
21
|
ignored: boolean;
|
|
19
22
|
malformedLines: number;
|
|
@@ -73,6 +76,29 @@ function detailsModel(details: unknown): string | undefined {
|
|
|
73
76
|
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Pi records placeholder assistant messages for turns it generates itself; today they
|
|
81
|
+
* carry the literal model `<synthetic>`, and a missing model normalizes to `unknown`.
|
|
82
|
+
* Angle brackets are never valid in a provider model id, so they are a safe marker.
|
|
83
|
+
*/
|
|
84
|
+
function placeholderModel(model: string): boolean {
|
|
85
|
+
const bare = model.slice(model.lastIndexOf("/") + 1);
|
|
86
|
+
return bare === "unknown" || (bare.startsWith("<") && bare.endsWith(">"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unmetered(usage: UsageTotals): boolean {
|
|
90
|
+
return totalTokens(usage) === 0 && usage.reasoning === 0 && usage.cost === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Placeholder models that recorded no tokens and no cost would otherwise get their own
|
|
95
|
+
* zero-value model row. Roll them into the unattributed bucket so the call is still
|
|
96
|
+
* counted without inventing a model that was never billed.
|
|
97
|
+
*/
|
|
98
|
+
function attributedModel(model: string, usage: UsageTotals): string {
|
|
99
|
+
return placeholderModel(model) && unmetered(usage) ? SUMMARY_MODEL : model;
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
function nestedSubagentUsage(details: unknown): UsageTotals | undefined {
|
|
77
103
|
const record = object(details);
|
|
78
104
|
if (!record) return undefined;
|
|
@@ -135,7 +161,7 @@ function normalizeEntry(
|
|
|
135
161
|
if (role === "assistant") {
|
|
136
162
|
const usage = normalizeUsage(message.usage);
|
|
137
163
|
if (!usage) return undefined;
|
|
138
|
-
const model = modelName(message.provider, message.responseModel ?? message.model);
|
|
164
|
+
const model = attributedModel(modelName(message.provider, message.responseModel ?? message.model), usage);
|
|
139
165
|
return {
|
|
140
166
|
fingerprint: fingerprint([id, entry.timestamp, "assistant", model, usage]),
|
|
141
167
|
timestamp: timestamp(message.timestamp, entryTimestamp),
|
|
@@ -148,7 +174,7 @@ function normalizeEntry(
|
|
|
148
174
|
const toolName = typeof message.toolName === "string" ? message.toolName : "tool";
|
|
149
175
|
const usage = normalizeUsage(message.usage) ?? (toolName === "subagent" ? nestedSubagentUsage(message.details) : undefined);
|
|
150
176
|
if (!usage) return undefined;
|
|
151
|
-
const model = detailsModel(message.details) ??
|
|
177
|
+
const model = attributedModel(detailsModel(message.details) ?? SUMMARY_MODEL, usage);
|
|
152
178
|
const children = toolName === "subagent" ? childSessionFiles(message.details, sessionFile) : [];
|
|
153
179
|
return {
|
|
154
180
|
fingerprint: fingerprint([id, entry.timestamp, "tool", toolName, model, usage]),
|
|
@@ -167,9 +193,9 @@ function normalizeEntry(
|
|
|
167
193
|
const usage = normalizeUsage(entry.usage);
|
|
168
194
|
if (!usage) return undefined;
|
|
169
195
|
return {
|
|
170
|
-
fingerprint: fingerprint([id, entry.timestamp, entry.type,
|
|
196
|
+
fingerprint: fingerprint([id, entry.timestamp, entry.type, SUMMARY_MODEL, usage]),
|
|
171
197
|
timestamp: entryTimestamp,
|
|
172
|
-
model:
|
|
198
|
+
model: SUMMARY_MODEL,
|
|
173
199
|
usage,
|
|
174
200
|
kind: "summary",
|
|
175
201
|
};
|
package/widget.ts
CHANGED
|
@@ -374,10 +374,10 @@ export class StatsWidget implements Component {
|
|
|
374
374
|
today.setHours(12, 0, 0, 0);
|
|
375
375
|
const end = new Date(today);
|
|
376
376
|
end.setDate(end.getDate() + (6 - ((end.getDay() + 6) % 7)));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const weeks = Math.max(HEATMAP_MIN_WEEKS, Math.min(cells, HEATMAP_MAX_WEEKS
|
|
377
|
+
// Always claim the width the terminal offers, up to a year. Weeks before the first
|
|
378
|
+
// recorded day render as empty cells so a short history fills the grid in over time
|
|
379
|
+
// instead of leaving most of a wide row blank.
|
|
380
|
+
const weeks = Math.max(HEATMAP_MIN_WEEKS, Math.min(cells, HEATMAP_MAX_WEEKS));
|
|
381
381
|
const start = new Date(end);
|
|
382
382
|
start.setDate(start.getDate() - weeks * 7 + 1);
|
|
383
383
|
|