@pify/usage 0.10.0 → 0.11.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/README.md +2 -0
- package/extensions/usage.ts +5 -2
- package/package.json +1 -1
- package/src/ratelimit.ts +40 -0
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ Folded from each message's `usage.cost`, which pi already computes. It survives
|
|
|
20
20
|
|
|
21
21
|
When [`@pify/subagent`](https://github.com/pifydev/subagent), [`@pify/swarm`](https://github.com/pifydev/swarm) or [`@pify/workflow`](https://github.com/pifydev/workflow) run child agents, those children are separate in-memory sessions whose spend never reaches this branch — a session that delegates used to look cheaper than it was. Those packages now add every child message's cost to a process-wide tally, and the footer shows it as a separate ` · agents $0.12` beside the session's own figure (the `/usage` session block has the same line, with tokens). It is kept apart on purpose: the session numbers stay what the branch says, and the agents figure is what the delegation cost on top. The tally survives `/reload` and starts over with a new session.
|
|
22
22
|
|
|
23
|
+
On a Claude subscription (OAuth Pro/Max) the footer also carries the wall you are approaching: the headers `/usage quota` reads were already captured on every response, but only that command showed them, so the weekly or 5-hour limit used to arrive as a stalled turn. Now a `⚠ 7d 8% · resets 14:05` segment appears once the binding window is below 15%, or whenever Anthropic marks the subscription as anything but allowed, and stays out of the way otherwise. Other providers carry no such headers, so they never show it.
|
|
24
|
+
|
|
23
25
|
The gauge on the right is how full the context window is — the third mid-session question, next to tokens and cost. It was computed for the `/usage` dashboard but shown only there; now it is live. It appears once there is a window to measure against (so not under `-p`) and turns to `⚠` past 90%, the point where "how full" stops being trivia and becomes a decision. For the breakdown of what filled it, run `/context`.
|
|
24
26
|
|
|
25
27
|
## `/usage`
|
package/extensions/usage.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { childSpendTotal, onChildSpend, resetChildSpend } from "../src/child-cos
|
|
|
35
35
|
import { QUOTA_PROVIDERS, fetchQuota, quotaReport, type QuotaResult } from "../src/quota.ts";
|
|
36
36
|
import { redact } from "../src/redact.ts";
|
|
37
37
|
import { scanSessions } from "../src/sessions.ts";
|
|
38
|
-
import { parseRateLimit, formatRateLimit, type RateLimitSnapshot } from "../src/ratelimit.ts";
|
|
38
|
+
import { parseRateLimit, formatRateLimit, type RateLimitSnapshot, footerQuotaSegment } from "../src/ratelimit.ts";
|
|
39
39
|
import { emptyTotals, isRecord, type UsageTotals } from "../src/types.ts";
|
|
40
40
|
|
|
41
41
|
type UiContext = ExtensionContext;
|
|
@@ -83,7 +83,10 @@ export default function usage(pi: ExtensionAPI) {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
const gauge = contextGauge(contextInfo(ctx).pct);
|
|
86
|
-
|
|
86
|
+
// The subscription wall, from the headers already captured for this
|
|
87
|
+
// provider: quiet while healthy, a short warning once it is close.
|
|
88
|
+
const quota = footerQuotaSegment(rateLimits.get((ctx.model as { provider?: string } | null)?.provider ?? ""));
|
|
89
|
+
ctx.ui.setStatus("usage", [base, gauge, quota].filter(Boolean).join(" · "));
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|
package/package.json
CHANGED
package/src/ratelimit.ts
CHANGED
|
@@ -166,6 +166,46 @@ function formatReset(snap: RateLimitSnapshot): string {
|
|
|
166
166
|
return raw;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/** A reset time short enough for the footer: HH:MM today, otherwise MM-DD HH:MM; "" when unknown or unwieldy. */
|
|
170
|
+
export function shortReset(snap: RateLimitSnapshot, now: number = Date.now()): string {
|
|
171
|
+
const raw = snap.resets;
|
|
172
|
+
if (!raw) return "";
|
|
173
|
+
if (snap.source === "anthropic" && /^\d{9,}$/.test(raw)) {
|
|
174
|
+
const d = new Date(Number.parseInt(raw, 10) * 1000);
|
|
175
|
+
if (!Number.isFinite(d.getTime())) return "";
|
|
176
|
+
const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
177
|
+
const sameDay = new Date(now).toDateString() === d.toDateString();
|
|
178
|
+
return sameDay ? hm : `${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${hm}`;
|
|
179
|
+
}
|
|
180
|
+
return raw.length <= 12 ? raw : "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Below this share of the binding window left, the footer starts warning. */
|
|
184
|
+
export const FOOTER_WARN_FRACTION = 0.15;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The footer's quota warning, or "" while the session is healthy. A Claude
|
|
188
|
+
* subscription user used to learn about the 5h/7d wall only when a turn
|
|
189
|
+
* stalled, or by opening `/usage quota` — the headers were captured either
|
|
190
|
+
* way. This shows the binding window once it is nearly gone, or whenever
|
|
191
|
+
* Anthropic marks the subscription as anything but "allowed". Non-Anthropic
|
|
192
|
+
* providers carry none of these fields, so it stays empty for them.
|
|
193
|
+
*/
|
|
194
|
+
export function footerQuotaSegment(snap: RateLimitSnapshot | null | undefined, now: number = Date.now()): string {
|
|
195
|
+
if (!snap) return "";
|
|
196
|
+
const binding =
|
|
197
|
+
snap.unifiedBinding ?? (snap.unified7dRemaining !== null ? "7d" : snap.unified5hRemaining !== null ? "5h" : null);
|
|
198
|
+
const remaining = binding === "7d" ? snap.unified7dRemaining : binding === "5h" ? snap.unified5hRemaining : null;
|
|
199
|
+
const walled = Boolean(snap.unifiedStatus && snap.unifiedStatus !== "allowed");
|
|
200
|
+
const low = remaining !== null && remaining < FOOTER_WARN_FRACTION;
|
|
201
|
+
if (!walled && !low) return "";
|
|
202
|
+
const parts = [`⚠ ${binding ?? "subscription"}`];
|
|
203
|
+
if (remaining !== null) parts.push(`${Math.max(0, Math.round(remaining * 100))}%`);
|
|
204
|
+
if (walled) parts.push(snap.unifiedStatus!.replace(/_/g, " "));
|
|
205
|
+
const reset = shortReset(snap, now);
|
|
206
|
+
return reset ? `${parts.join(" ")} · resets ${reset}` : parts.join(" ");
|
|
207
|
+
}
|
|
208
|
+
|
|
169
209
|
/** One human line for the /usage quota report; null when nothing was captured. */
|
|
170
210
|
export function formatRateLimit(snap: RateLimitSnapshot | null): string | null {
|
|
171
211
|
if (!snap) return null;
|