@tmustier/pi-usage-extension 0.7.2 → 0.8.0
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/CHANGELOG.md +7 -0
- package/README.md +15 -12
- package/data.ts +28 -4
- package/index.ts +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Cache-miss taxonomy split into three behaviours**, each with its own alarm: resuming a conversation after a break (cache expired), switching models mid-conversation (the previous model's cache doesn't transfer), and true mid-session prefix changes (no break, compaction, or model switch to explain them). Validated against a second public corpus (badlogicgames/pi-mono, 628 sessions).
|
|
7
|
+
- **Insights sections reordered facts-first**: “Where it went” (structure) now comes before “Worth attention” (alarms).
|
|
8
|
+
- **pi's built-in test providers excluded**: `faux-provider` and `fake-provider` never call a real API and no longer appear in tables, graphs, totals, or insights.
|
|
9
|
+
|
|
3
10
|
## [0.7.2] - 2026-07-17
|
|
4
11
|
|
|
5
12
|
### Changed
|
package/README.md
CHANGED
|
@@ -67,17 +67,7 @@ In Pi, run:
|
|
|
67
67
|
|
|
68
68
|
### Insights
|
|
69
69
|
|
|
70
|
-
The **Insights** view has two sections:
|
|
71
|
-
|
|
72
|
-
**Worth attention** — alarms that only appear when a wasteful pattern is material for the period. When nothing is flagged, the section shows an explicit all-clear.
|
|
73
|
-
|
|
74
|
-
| Alarm | Fires when |
|
|
75
|
-
|---|---|
|
|
76
|
-
| Re-sends after a break (cache TTL) | ≥ 2% of the period's cost (and ≥ $1) went to messages that re-sent a large conversation from scratch after a > 5 min idle gap — provider caches expire after a few minutes idle |
|
|
77
|
-
| Mid-session re-sends (prefix change) | ≥ 2% of cost (and ≥ $1) went to large-context cache misses with **no** idle gap — the request prefix changed mid-session. Messages right after a pi compaction are excluded, since compaction legitimately rewrites the prefix |
|
|
78
|
-
| Session concentration | the top 5 sessions account for ≥ 35% of the period's cost |
|
|
79
|
-
| Upfront tax | ≥ 8% of cost was the first message of a session (session starts pay for their whole prompt uncached) |
|
|
80
|
-
| Cache leverage floor | fewer than 5 cached tokens served per fresh token paid (shown only above $5 / 1M fresh tokens, to avoid noise) |
|
|
70
|
+
The **Insights** view has two sections, facts first:
|
|
81
71
|
|
|
82
72
|
**Where it went** — always-on structural lenses:
|
|
83
73
|
|
|
@@ -85,9 +75,22 @@ The **Insights** view has two sections:
|
|
|
85
75
|
|---|---|
|
|
86
76
|
| Context tax | share of cost spent at ≥ 150k context, with the average per-message cost vs messages under 100k |
|
|
87
77
|
| Project mix | top 3 project directories by cost, derived from each session's working directory (worktrees collapse into their repository; hidden when one project is ≥ 90% — you already know) |
|
|
88
|
-
| Reasoning share | share of output tokens that were
|
|
78
|
+
| Reasoning share | share of output tokens that were hidden reasoning (recorded by pi 0.80.3+ only; hidden below 5%) |
|
|
89
79
|
| Burn trend | your last 7 days of spend vs your prior 4-week weekly pace (same on every tab) |
|
|
90
80
|
|
|
81
|
+
**Worth attention** — alarms that only appear when a wasteful pattern is material for the period. When nothing is flagged, the section shows an explicit all-clear. Large-context cache misses are split into three distinct behaviours:
|
|
82
|
+
|
|
83
|
+
| Alarm | Fires when |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Resuming after a break | ≥ 2% of the period's cost (and ≥ $1) went to messages that re-sent a large conversation from scratch after a > 5 min idle gap — provider caches expire after a few minutes idle |
|
|
86
|
+
| Switching models mid-conversation | ≥ 2% of cost (and ≥ $1) went to large-context misses right after the provider/model changed mid-session — the previous model's cache doesn't transfer |
|
|
87
|
+
| Mid-session re-sends (prefix change) | ≥ 2% of cost (and ≥ $1) went to large-context misses with **no** idle gap, compaction, or model switch to explain them — something rewrote the request prefix |
|
|
88
|
+
| Session concentration | the top 5 sessions account for ≥ 35% of the period's cost |
|
|
89
|
+
| Upfront tax | ≥ 8% of cost was the first message of a session (session starts pay for their whole prompt uncached) |
|
|
90
|
+
| Cache leverage floor | fewer than 5 cached tokens served per fresh token paid (shown only above $5 / 1M fresh tokens, to avoid noise) |
|
|
91
|
+
|
|
92
|
+
pi's built-in test providers (`faux-provider`, `fake-provider`) never call a real API and are excluded from all statistics, graphs, and insights.
|
|
93
|
+
|
|
91
94
|
**Unit:** insights are weighted by recorded API cost (USD). Periods with no recorded cost show an explicit empty state rather than silently switching to a different unit.
|
|
92
95
|
|
|
93
96
|
A note on reading them: cache-miss and context numbers are **observed cost, not promised savings** — big-context messages often do bigger work. Treat the alarms as "look here", not as an invoice for waste.
|
package/data.ts
CHANGED
|
@@ -76,9 +76,11 @@ interface PeriodRawData {
|
|
|
76
76
|
sessionCosts: Map<string, number>;
|
|
77
77
|
/** Cost of each session's first-ever message falling in this period. */
|
|
78
78
|
upfrontCost: number;
|
|
79
|
-
/** Cache misses after >TTL_GAP_MS idle —
|
|
79
|
+
/** Cache misses after >TTL_GAP_MS idle — resuming after the cache expired. */
|
|
80
80
|
ttlMissCost: number;
|
|
81
|
-
/** Cache misses
|
|
81
|
+
/** Cache misses right after a mid-session model switch (no idle gap). */
|
|
82
|
+
modelSwitchMissCost: number;
|
|
83
|
+
/** Cache misses with no idle gap, compaction, or model switch — true prefix changes. */
|
|
82
84
|
prefixMissCost: number;
|
|
83
85
|
reasoningTokens: number;
|
|
84
86
|
outputTokens: number;
|
|
@@ -92,6 +94,8 @@ interface MessageMeta {
|
|
|
92
94
|
gapMs: number;
|
|
93
95
|
/** Context size of the previous assistant message in the same file; 0 when first. */
|
|
94
96
|
prevCtx: number;
|
|
97
|
+
/** True when the provider/model differs from the previous assistant message. */
|
|
98
|
+
modelSwitched: boolean;
|
|
95
99
|
/** True for the first deduped message of a session across all its files. */
|
|
96
100
|
isSessionStart: boolean;
|
|
97
101
|
}
|
|
@@ -514,6 +518,7 @@ function emptyPeriodRawData(): PeriodRawData {
|
|
|
514
518
|
sessionCosts: new Map(),
|
|
515
519
|
upfrontCost: 0,
|
|
516
520
|
ttlMissCost: 0,
|
|
521
|
+
modelSwitchMissCost: 0,
|
|
517
522
|
prefixMissCost: 0,
|
|
518
523
|
reasoningTokens: 0,
|
|
519
524
|
outputTokens: 0,
|
|
@@ -640,6 +645,9 @@ function addMessagesToUsageData(
|
|
|
640
645
|
const msg = messages[mi]!;
|
|
641
646
|
const mm = meta[mi]!;
|
|
642
647
|
|
|
648
|
+
// pi's built-in test providers never call a real API — keep them out of stats.
|
|
649
|
+
if (EXCLUDED_PROVIDERS.has(msg.provider)) continue;
|
|
650
|
+
|
|
643
651
|
// Day-indexed cost totals power the burn-trend insight.
|
|
644
652
|
if (msg.timestamp > 0) {
|
|
645
653
|
const dayIdx = Math.floor((msg.timestamp - todayMs) / DAY_MS);
|
|
@@ -704,6 +712,7 @@ function addMessagesToUsageData(
|
|
|
704
712
|
msg.cacheRead < Math.min(MISS_MAX_CACHE_READ, 0.3 * mm.prevCtx)
|
|
705
713
|
) {
|
|
706
714
|
if (mm.gapMs > TTL_GAP_MS) raw.ttlMissCost += msg.cost;
|
|
715
|
+
else if (mm.gapMs >= 0 && mm.modelSwitched) raw.modelSwitchMissCost += msg.cost;
|
|
707
716
|
else if (mm.gapMs >= 0) raw.prefixMissCost += msg.cost;
|
|
708
717
|
}
|
|
709
718
|
raw.reasoningTokens += msg.reasoning;
|
|
@@ -943,6 +952,7 @@ export async function collectUsageData(options: CollectUsageOptions = {}): Promi
|
|
|
943
952
|
meta.push({
|
|
944
953
|
gapMs: prev && prev.timestamp > 0 && m.timestamp > 0 ? m.timestamp - prev.timestamp : -1,
|
|
945
954
|
prevCtx: prev ? prev.input + prev.cacheRead + prev.cacheWrite : 0,
|
|
955
|
+
modelSwitched: prev !== null && (prev.provider !== m.provider || prev.model !== m.model),
|
|
946
956
|
isSessionStart: false,
|
|
947
957
|
});
|
|
948
958
|
}
|
|
@@ -1002,6 +1012,9 @@ const TREND_LOW_RATIO = 0.6;
|
|
|
1002
1012
|
const TTL_GAP_MS = 5 * 60_000;
|
|
1003
1013
|
const MISS_MIN_PREV_CONTEXT = 20_000;
|
|
1004
1014
|
const MISS_MAX_CACHE_READ = 5_000;
|
|
1015
|
+
/** pi's built-in test providers never send anything to a real API. */
|
|
1016
|
+
const EXCLUDED_PROVIDERS = new Set(["faux-provider", "fake-provider"]);
|
|
1017
|
+
|
|
1005
1018
|
const CACHE_MISS_ALARM_PERCENT = 2;
|
|
1006
1019
|
const CACHE_MISS_ALARM_MIN_COST = 1;
|
|
1007
1020
|
// Concentration / upfront alarms
|
|
@@ -1045,12 +1058,23 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
|
|
|
1045
1058
|
insights.push({
|
|
1046
1059
|
kind: "alarm",
|
|
1047
1060
|
stat: fmtMoney(raw.ttlMissCost),
|
|
1048
|
-
headline: `spent
|
|
1061
|
+
headline: `spent resuming conversations after a break (${fmtPercent(ttlPct)} of this period)`,
|
|
1049
1062
|
advice:
|
|
1050
1063
|
"Sent context is only reusable for a few minutes. After a longer pause, the next message pays to send the whole conversation again. Replying while a session is fresh avoids this.",
|
|
1051
1064
|
});
|
|
1052
1065
|
}
|
|
1053
1066
|
|
|
1067
|
+
const switchPct = (raw.modelSwitchMissCost / total) * 100;
|
|
1068
|
+
if (switchPct >= CACHE_MISS_ALARM_PERCENT && raw.modelSwitchMissCost >= CACHE_MISS_ALARM_MIN_COST) {
|
|
1069
|
+
insights.push({
|
|
1070
|
+
kind: "alarm",
|
|
1071
|
+
stat: fmtMoney(raw.modelSwitchMissCost),
|
|
1072
|
+
headline: `spent switching models mid-conversation (${fmtPercent(switchPct)} of this period)`,
|
|
1073
|
+
advice:
|
|
1074
|
+
"Changing model re-sends the whole conversation at full price — the previous model's saved context doesn't transfer. Switching between tasks instead of mid-conversation avoids this.",
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1054
1078
|
const prefixPct = (raw.prefixMissCost / total) * 100;
|
|
1055
1079
|
if (prefixPct >= CACHE_MISS_ALARM_PERCENT && raw.prefixMissCost >= CACHE_MISS_ALARM_MIN_COST) {
|
|
1056
1080
|
insights.push({
|
|
@@ -1058,7 +1082,7 @@ function computeInsights(raw: PeriodRawData, trend: TrendInfo | null): PeriodIns
|
|
|
1058
1082
|
stat: fmtMoney(raw.prefixMissCost),
|
|
1059
1083
|
headline: `spent re-sending conversations mid-session (${fmtPercent(prefixPct)} of this period)`,
|
|
1060
1084
|
advice:
|
|
1061
|
-
"These messages paid full price for context that had already been sent,
|
|
1085
|
+
"These messages paid full price for context that had already been sent — with no break, compaction, or model switch to explain it. Usually a tool or workflow is restarting or rewriting conversations. Worth a look if it stays high.",
|
|
1062
1086
|
});
|
|
1063
1087
|
}
|
|
1064
1088
|
|
package/index.ts
CHANGED
|
@@ -553,6 +553,11 @@ class UsageComponent {
|
|
|
553
553
|
|
|
554
554
|
const alarms = insights.filter((i) => i.kind === "alarm");
|
|
555
555
|
const structure = insights.filter((i) => i.kind === "structure");
|
|
556
|
+
// Facts first, flagged waste second.
|
|
557
|
+
if (structure.length > 0) {
|
|
558
|
+
lines.push(sectionHeader("Where it went", "accent"));
|
|
559
|
+
for (const insight of structure) renderOne(insight);
|
|
560
|
+
}
|
|
556
561
|
lines.push(sectionHeader("Worth attention", "warning"));
|
|
557
562
|
if (alarms.length > 0) {
|
|
558
563
|
for (const insight of alarms) renderOne(insight);
|
|
@@ -560,10 +565,6 @@ class UsageComponent {
|
|
|
560
565
|
lines.push(` ${th.fg("success", padLeft("✓", 6))} ${th.fg("dim", "no waste patterns flagged for this period")}`);
|
|
561
566
|
lines.push("");
|
|
562
567
|
}
|
|
563
|
-
if (structure.length > 0) {
|
|
564
|
-
lines.push(sectionHeader("Where it went", "accent"));
|
|
565
|
-
for (const insight of structure) renderOne(insight);
|
|
566
|
-
}
|
|
567
568
|
|
|
568
569
|
return lines;
|
|
569
570
|
}
|