pi-myusage 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pi-myusage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # ๐Ÿ“Š pi-myusage โ€” Provider Usage, API Balance, and Codex Fast Mode for Pi
2
+
3
+ [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
+
5
+ Inspect usage windows and API balances for Pi's active provider account, query every other configured provider, and toggle persistent Fast routing for OpenAI Codex models.
6
+ The extension keeps each provider's native quota, allowance, and billing semantics instead of treating unlike values as equivalent, and resolves credentials current-model-first so any number of custom providers in `models.json` โ€” with plain API keys or OAuth โ€” report correctly.
7
+
8
+ ## โœจ Features
9
+
10
+ - Shows the active provider's usage windows or balance on a single statusline line, each window carrying its own reset countdown.
11
+ - `/usage` reports every provider that has both a usage adapter and resolvable credentials; unconfigured providers are skipped instead of surfacing auth noise.
12
+ - Toggles persistent Fast and Ultrafast routing for `gpt-*` models on any OpenAI-compatible provider through `/fast` and `/ultrafast`.
13
+ - Matches adapters from the active model itself (provider id aliases, base URL origin, id family) โ€” no fixed provider-id assumptions, no official-origin refusals.
14
+ - Resolves credentials through Pi's runtime auth chain (`getApiKeyAndHeaders` โ†’ provider auth โ†’ stored credentials), with a 30-second memo for static keys only; OAuth paths always re-resolve so token refresh keeps working.
15
+ - Refreshes after every completed turn (bounded to one fetch per 30 s), on model switch, on session start, and on a configurable timer; in-flight requests are deduplicated and aborted on shutdown.
16
+ - Multi-request providers (Cline, OpenRouter) fetch independent endpoints in parallel; failed lookups degrade to per-provider snapshots instead of failing the report.
17
+
18
+ ## ๐Ÿ“ฆ Install
19
+
20
+ Requires Pi 0.84.0 or newer (`getApiKeyAndHeaders`, `before_provider_request` payload replacement).
21
+
22
+ ```bash
23
+ pi install npm:pi-myusage
24
+ ```
25
+
26
+ Try without installing permanently:
27
+
28
+ ```bash
29
+ pi -e npm:pi-myusage
30
+ ```
31
+
32
+ Or point Pi at a local checkout โ€” the package ships raw TypeScript and needs no build step:
33
+
34
+ ```bash
35
+ pi install ./pi-myusage
36
+ ```
37
+
38
+ ## ๐Ÿš€ Quick start
39
+
40
+ Start Pi and wait a moment: the statusline shows the active provider's windows, for example `GLM 5h 56% (2m) ยท 7d 82% (3d 4h)` or `$12.34 ยท DeepSeek`.
41
+ Run `/usage` for every configured provider's details, `/usage refresh` to force a fresh fetch, and `/fast` (on a supported Codex model) to toggle Fast routing.
42
+
43
+ ## ๐Ÿ’ฌ Commands
44
+
45
+ | Command | Purpose |
46
+ | --- | --- |
47
+ | `/usage` | Report usage for every provider with an adapter and configured credentials. |
48
+ | `/usage refresh` | Force a fresh fetch of the active provider. |
49
+ | `/usage fast` | Same as `/fast`. |
50
+ | `/usage ultrafast` | Same as `/ultrafast`. |
51
+ | `/fast` | Toggle persistent Fast routing (`service_tier: priority`) for the active `gpt-*` model. |
52
+ | `/ultrafast` | Toggle persistent Ultrafast routing (`service_tier: ultrafast`), the separate Cerebras-backed tier. |
53
+
54
+ ## โš™๏ธ Settings
55
+
56
+ Preferences live in `pi-myusage.json` under Pi's user agent directory, normally `~/.pi/agent/pi-myusage.json`.
57
+ It holds a single field โ€” `codexFastMode`: `false`, `"fast"`, or `"ultrafast"` โ€” written by `/fast` and `/ultrafast` and read at every session start (`true` from older versions is read as `"fast"`).
58
+ The statusline auto-refresh interval is fixed at 300 seconds; usage always refreshes after each completed turn as well.
59
+
60
+ ### Fast and Ultrafast modes
61
+
62
+ Two independent service tiers, mutually exclusive, each toggled by its own command:
63
+
64
+ - **Fast** (`/fast`) sends `service_tier: "priority"` โ€” about 1.5ร— faster (up to 2.5ร— on `gpt-5.6-sol`), uses more of your plan allowance; message costs are corrected ร—2 (ร—2.5 for `gpt-5.5`) on the official Codex endpoint.
65
+ - **Ultrafast** (`/ultrafast`) sends `service_tier: "ultrafast"` โ€” the separate Cerebras-backed tier (up to 14ร— faster). No model restriction is imposed here: any `gpt-*` model can request it, and OpenAI falls back or errors per account entitlement and model support. Costs are shown as returned โ€” no multiplier is applied.
66
+
67
+ Both apply to every `gpt-*` model on an OpenAI-family API โ€” `openai-completions`, `openai-responses`, or `openai-codex-responses` โ€” including relays and gateways. Disabling either sends an explicit `service_tier: "default"`.
68
+ The statusline appends `ยท fast` or `ยท ultrafast` whenever the active mode is effective โ€” even when no usage window is displayed.
69
+ Non-gpt models and non-OpenAI APIs are never rewritten.
70
+
71
+ ## ๐Ÿ“‹ Provider semantics
72
+
73
+ Usage is provider-specific: a subscription window, a prepaid balance, and rated spend are not interchangeable.
74
+
75
+ | Provider | Reported data |
76
+ | --- | --- |
77
+ | OpenAI Codex | 5h/7d subscription windows with per-window resets, credits balance |
78
+ | Anthropic Claude | OAuth subscription windows (5h/7d/per-model); API keys report pay-as-you-go status |
79
+ | Kimi / Moonshot | Coding Plan usage windows, or platform API balance (USD/CNY) |
80
+ | OpenRouter | Per-key usage limit and account credit balance |
81
+ | OpenCode Zen | Rolling, weekly, and monthly plan windows |
82
+ | MiniMax Global/China | Token Plan windows (JWT keys) or pay-as-you-go balance |
83
+ | GLM / Zhipu / Z.AI | Coding Plan quota windows (5h/weekly, any `TOKENS_LIMIT` window echoed), MCP allowance, plan level |
84
+ | DeepSeek | Exact current CNY and USD API balances |
85
+ | xAI / Grok | OAuth subscription windows and credits; API keys report userinfo status |
86
+ | Google Vertex AI | Status only โ€” billing lives in GCP, quota is not queryable via model credentials |
87
+ | Gemini API / AI Studio | Key validation and model count โ€” no public quota endpoint exists |
88
+ | Vercel AI Gateway | Credit balance and lifetime spend |
89
+ | CLIProxyAPI | Per-api-user request counts via the management API (`CLIPROXY_MANAGEMENT_KEY`), else proxy status |
90
+ | GitHub Copilot | Plan entitlements and quota snapshots |
91
+ | Fireworks | Rated trailing 30-day spend |
92
+ | Baseten | Trailing 30-day Model APIs spend after credits |
93
+ | Cline | ClinePass five-hour/weekly/monthly utilization windows and credit balance (micro-USD) |
94
+
95
+ ## ๐Ÿงญ Matching and credentials
96
+
97
+ The adapter is chosen from the **current model**: provider id aliases first, then base URL origin, then id families (for example any `glm-*` provider without a base URL).
98
+ Credentials resolve through Pi's runtime chain and are memoized for 30 s only when static โ€” OAuth results always re-resolve.
99
+ A provider id whose base URL points elsewhere never sends its credential to the official origin (the `glm-` prefix rule requires no base URL for exactly this reason).
100
+
101
+ ## ๐Ÿ“Š Statusline behavior
102
+
103
+ The status item key is `pi-myusage`.
104
+ All usage windows render on one line with per-window reset suffixes; providers without windows render balances (`$0.50 ยท Cline`, `ยฅ12.34 ยท DeepSeek`).
105
+ The line refreshes after each completed turn, on model switch, and on the interval timer; chain-timeout failures are reported without pinning the cache, so the next trigger retries immediately.
106
+ With [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions) installed, the chip lands on footer line 1 next to the token stats โ€” the key deliberately avoids that extension's reserved `pi-usage` status key, which it erases when its own usage pull is empty.
107
+
108
+ ## ๐Ÿ”’ Security and privacy
109
+
110
+ Credentials resolve in memory, are never logged or persisted by this extension, and error text is scrubbed of token-like fragments before display.
111
+ Every provider call is a read-only `GET` to the provider's official usage endpoint with `redirect: "error"`, carrying only that provider's resolved credential.
112
+ Usage endpoints are queried with exactly the credential the model itself uses โ€” nothing is sent anywhere the active provider doesn't already send it.
113
+ Pi extensions run with the user's process privileges; install only trusted extensions.
114
+
115
+ ## ๐Ÿšง Limitations
116
+
117
+ - Providers without a public quota API (Vertex, Gemini, CLIProxyAPI without a management key) degrade to status metrics instead of inventing numbers.
118
+ - Provider reports are snapshots and may lag the provider's own accounting.
119
+ - Fireworks and Baseten expose rated 30-day spend only; balances and caps live in their web consoles.
120
+ - MiniMax window counts are historically ambiguous (used vs remaining); contradictory payloads are reported as unavailable rather than guessed.
121
+ - Cline balance is micro-USD; the dashboard's credit figure equals `balance / 1,000,000`.
122
+ - Kimi Coding and Cline usage-limits rely on provider-owned endpoints that may change without notice.
123
+ - A later-loaded extension can still rewrite the final provider payload, so arbitrary third-party payload conflicts cannot be prevented.
124
+
125
+ ## ๐Ÿ—‚๏ธ Package layout
126
+
127
+ ```text
128
+ pi-myusage/
129
+ โ”œโ”€โ”€ index.ts # Pi entrypoint: commands, events, timers, statusline, Fast routing
130
+ โ”œโ”€โ”€ src/
131
+ โ”‚ โ”œโ”€โ”€ adapters/ # One file per provider (17 adapters)
132
+ โ”‚ โ”œโ”€โ”€ auth.ts # Credential resolution chain and memo
133
+ โ”‚ โ”œโ”€โ”€ match.ts # Adapter registry and current-model-first matching
134
+ โ”‚ โ”œโ”€โ”€ codex-fast.ts # Fast tier rewrite and cost correction (pure logic)
135
+ โ”‚ โ”œโ”€โ”€ format.ts # Window summaries, bars, relative times
136
+ โ”‚ โ”œโ”€โ”€ http.ts # fetchJson with timeout/abort, safe error redaction
137
+ โ”‚ โ”œโ”€โ”€ settings.ts # pi-myusage.json load/save
138
+ โ”‚ โ””โ”€โ”€ types.ts # Snapshot, metric, adapter, target contracts
139
+ โ””โ”€โ”€ test/selfcheck.ts # Assert-based self-check for pure logic
140
+ ```
141
+
142
+ ## ๐Ÿ”Ž Verify
143
+
144
+ ```bash
145
+ npm run verify # tsc --noEmit (strict, noUnused*) + tsx test/selfcheck.ts
146
+ ```
147
+
148
+ ## ๐Ÿ“„ License
149
+
150
+ MIT.
151
+ See [`LICENSE`](./LICENSE).
package/index.ts ADDED
@@ -0,0 +1,307 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
+ import type { Api, Model } from "@earendil-works/pi-ai";
4
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import {
7
+ codexFastAvailability,
8
+ correctCodexFastMessage,
9
+ fastModeIsEffective,
10
+ fastSuffix,
11
+ rewriteServiceTierPayload,
12
+ tierForMode,
13
+ type FastMode,
14
+ } from "./src/codex-fast.ts";
15
+ import { compactStatus, detailLines } from "./src/format.ts";
16
+ import { safeError } from "./src/http.ts";
17
+ import { adapterFor, configuredTargets, currentTarget, withCredentials } from "./src/match.ts";
18
+ import type { ProviderTarget, UsageAdapter, UsageSnapshot } from "./src/types.ts";
19
+
20
+ const REFRESH_INTERVAL_S = 300;
21
+
22
+ function fastModePath(): string {
23
+ return join(getAgentDir(), "pi-myusage.json");
24
+ }
25
+
26
+ function loadFastMode(): FastMode {
27
+ try {
28
+ const raw = (JSON.parse(readFileSync(fastModePath(), "utf8")) as { codexFastMode?: unknown }).codexFastMode;
29
+ if (raw === true || raw === "fast") return "fast";
30
+ if (raw === "ultrafast") return "ultrafast";
31
+ return false;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function saveFastMode(mode: FastMode): void {
38
+ const path = fastModePath();
39
+ mkdirSync(join(path, ".."), { recursive: true });
40
+ writeFileSync(path, `${JSON.stringify({ codexFastMode: mode }, null, 2)}\n`);
41
+ }
42
+
43
+ const STATUS_ID = "pi-myusage";
44
+
45
+ interface CacheEntry {
46
+ snapshot: UsageSnapshot;
47
+ at: number;
48
+ }
49
+
50
+ interface PendingFast {
51
+ tier: string;
52
+ model: Model<Api>;
53
+ }
54
+
55
+ export default function usagePlus(pi: ExtensionAPI) {
56
+ let fastMode = loadFastMode();
57
+ const cache = new Map<string, CacheEntry>();
58
+ const inflight = new Map<string, Promise<UsageSnapshot | undefined>>();
59
+ const pendingFast = new Map<string, PendingFast>();
60
+ const liveControllers = new Set<AbortController>();
61
+ let timer: ReturnType<typeof setInterval> | undefined;
62
+ let lastContext: ExtensionContext | undefined;
63
+ let postTurnTimer: ReturnType<typeof setTimeout> | undefined;
64
+ let lastPostTurnFetch = 0;
65
+
66
+ const POST_TURN_DELAY_MS = 1_500;
67
+ const POST_TURN_MIN_GAP_MS = 30_000;
68
+
69
+ function schedulePostTurnRefresh(): void {
70
+ if (postTurnTimer) return;
71
+ postTurnTimer = setTimeout(() => {
72
+ postTurnTimer = undefined;
73
+ const ctx = lastContext;
74
+ if (!ctx) return;
75
+ if (Date.now() - lastPostTurnFetch < POST_TURN_MIN_GAP_MS) return;
76
+ lastPostTurnFetch = Date.now();
77
+ void refresh(ctx, true).catch(() => {});
78
+ }, POST_TURN_DELAY_MS);
79
+ postTurnTimer.unref?.();
80
+ }
81
+
82
+ const modelKey = (model: Model<Api> | undefined): string | undefined =>
83
+ model ? `${model.provider}:${model.id}` : undefined;
84
+
85
+ function render(ctx: ExtensionContext, snapshot: UsageSnapshot | undefined): void {
86
+ let text = compactStatus(snapshot);
87
+ const suffix = fastModeIsEffective(ctx.model, fastMode) ? fastSuffix(fastMode) : undefined;
88
+ if (suffix) text = text ? `${text} ยท ${suffix}` : suffix;
89
+ ctx.ui.setStatus(STATUS_ID, text);
90
+ }
91
+
92
+ async function fetchSnapshot(ctx: ExtensionContext, force: boolean): Promise<UsageSnapshot | undefined> {
93
+ const target = currentTarget(ctx);
94
+ const adapter = adapterFor(target);
95
+ if (!target || !adapter) return undefined;
96
+ const key = `${target.providerId}:${target.model?.id ?? ""}`;
97
+ const cached = cache.get(key);
98
+ if (!force && cached && Date.now() - cached.at < REFRESH_INTERVAL_S * 1000) return cached.snapshot;
99
+
100
+ const existing = inflight.get(key);
101
+ if (existing) return existing;
102
+
103
+ const controller = new AbortController();
104
+ const chainSignal = AbortSignal.any([controller.signal, AbortSignal.timeout(20_000)]);
105
+ liveControllers.add(controller);
106
+ const request = (async (): Promise<UsageSnapshot | undefined> => {
107
+ try {
108
+ const prepared = await withCredentials(ctx, target);
109
+ const snapshot = await adapter.fetch({ target: prepared, signal: chainSignal, force });
110
+ cache.set(key, { snapshot, at: Date.now() });
111
+ return snapshot;
112
+ } catch (error) {
113
+ if (controller.signal.aborted) return undefined;
114
+ const snapshot: UsageSnapshot = {
115
+ adapterId: adapter.id,
116
+ sourceProviderId: target.providerId,
117
+ displayName: adapter.label,
118
+ state: "unavailable",
119
+ fetchedAt: new Date().toISOString(),
120
+ accounts: [],
121
+ error: safeError(error),
122
+ };
123
+ if (!chainSignal.aborted) cache.set(key, { snapshot, at: Date.now() });
124
+ return snapshot;
125
+ } finally {
126
+ inflight.delete(key);
127
+ liveControllers.delete(controller);
128
+ if (lastContext && modelKey(lastContext.model) === key) {
129
+ render(lastContext, cache.get(key)?.snapshot);
130
+ }
131
+ }
132
+ })();
133
+ inflight.set(key, request);
134
+ return request;
135
+ }
136
+
137
+ async function refresh(ctx: ExtensionContext, force = false): Promise<UsageSnapshot | undefined> {
138
+ try {
139
+ lastContext = ctx;
140
+ const key = modelKey(ctx.model);
141
+ const snapshot = await fetchSnapshot(ctx, force);
142
+ if (!snapshot) {
143
+ render(ctx, undefined);
144
+ return undefined;
145
+ }
146
+ if (modelKey(ctx.model) === key) render(ctx, snapshot);
147
+ return snapshot;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ async function refreshConfigured(ctx: ExtensionContext): Promise<UsageSnapshot[]> {
154
+ const prepared = await Promise.all(
155
+ configuredTargets(ctx).map(async (target) => {
156
+ const adapter = adapterFor(target);
157
+ if (!adapter) return undefined;
158
+ try {
159
+ return { target: await withCredentials(ctx, target), adapter };
160
+ } catch {
161
+ return undefined;
162
+ }
163
+ }),
164
+ );
165
+ const entries = prepared
166
+ .filter(
167
+ (entry): entry is { target: ProviderTarget; adapter: UsageAdapter } =>
168
+ entry !== undefined && Boolean(entry.target.credentials?.apiKey),
169
+ )
170
+ .slice(0, 20);
171
+ const results = await Promise.all(
172
+ entries.map(async ({ target, adapter }) => {
173
+ try {
174
+ return await adapter.fetch({ target, signal: AbortSignal.timeout(20_000), force: true });
175
+ } catch (error) {
176
+ return {
177
+ adapterId: adapter.id,
178
+ sourceProviderId: target.providerId,
179
+ displayName: adapter.label,
180
+ state: "unavailable" as const,
181
+ fetchedAt: new Date().toISOString(),
182
+ accounts: [],
183
+ error: safeError(error),
184
+ } satisfies UsageSnapshot;
185
+ }
186
+ }),
187
+ );
188
+ return results;
189
+ }
190
+
191
+ function startTimers(ctx: ExtensionContext): void {
192
+ if (timer) clearInterval(timer);
193
+ timer = setInterval(() => {
194
+ if (lastContext) void refresh(lastContext).catch(() => {});
195
+ }, REFRESH_INTERVAL_S * 1000);
196
+ timer.unref?.();
197
+ lastContext = ctx;
198
+ }
199
+
200
+ async function usageCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
201
+ const action = args.trim().split(/\s+/)[0] ?? "";
202
+ if (action === "refresh") {
203
+ const snapshot = await refresh(ctx, true);
204
+ if (snapshot) ctx.ui.notify(detailLines(snapshot).join("\n"), "info");
205
+ else ctx.ui.notify("Active provider has no usage adapter.", "warning");
206
+ return;
207
+ }
208
+ if (action === "fast" || action === "ultrafast") {
209
+ await fastCommand(action, ctx);
210
+ return;
211
+ }
212
+ if (action) {
213
+ ctx.ui.notify("Usage: /usage [refresh|fast]", "warning");
214
+ return;
215
+ }
216
+ const snapshots = await refreshConfigured(ctx);
217
+ if (!snapshots.length) {
218
+ ctx.ui.notify("No configured providers with usage support found.", "warning");
219
+ return;
220
+ }
221
+ ctx.ui.notify(snapshots.map(detailLines).map((lines) => lines.join("\n")).join("\n\n"), "info");
222
+ }
223
+
224
+ async function fastCommand(mode: "fast" | "ultrafast", ctx: ExtensionCommandContext): Promise<void> {
225
+ const availability = codexFastAvailability(ctx.model, fastMode);
226
+ if (availability.kind === "unavailable") {
227
+ ctx.ui.notify(availability.reason, "warning");
228
+ return;
229
+ }
230
+ fastMode = fastMode === mode ? false : mode;
231
+ try {
232
+ saveFastMode(fastMode);
233
+ } catch (error) {
234
+ ctx.ui.notify(`Could not save fast mode: ${safeError(error)}`, "error");
235
+ return;
236
+ }
237
+ const label = mode === "ultrafast" ? "Ultrafast" : "Fast";
238
+ ctx.ui.notify(
239
+ fastMode === mode
240
+ ? `${label} enabled (persistent). ${label === "Ultrafast" ? "Up to 14ร— faster." : "~1.5ร— faster, uses more of your plan allowance."}`
241
+ : `${label} disabled; standard routing.`,
242
+ "info",
243
+ );
244
+ await refresh(ctx);
245
+ }
246
+
247
+ pi.registerCommand("usage", {
248
+ description: "Provider usage, quota and balance (/usage [refresh|fast|ultrafast])",
249
+ handler: usageCommand,
250
+ });
251
+ pi.registerCommand("fast", {
252
+ description: "Toggle persistent Fast routing (service_tier: priority)",
253
+ handler: (_args, ctx) => fastCommand("fast", ctx),
254
+ });
255
+ pi.registerCommand("ultrafast", {
256
+ description: "Toggle persistent Ultrafast routing (service_tier: ultrafast)",
257
+ handler: (_args, ctx) => fastCommand("ultrafast", ctx),
258
+ });
259
+
260
+ pi.on("session_start", async (_event, ctx) => {
261
+ fastMode = loadFastMode();
262
+ cache.clear();
263
+ startTimers(ctx);
264
+ await refresh(ctx);
265
+ });
266
+
267
+ pi.on("agent_start", async (_event, ctx) => {
268
+ await refresh(ctx);
269
+ });
270
+
271
+ pi.on("model_select", async (_event, ctx) => {
272
+ await refresh(ctx);
273
+ });
274
+
275
+ pi.on("before_provider_request", (event, ctx) => {
276
+ const rewritten = rewriteServiceTierPayload(event.payload, ctx.model, fastMode);
277
+ const key = ctx.model ? `${ctx.sessionManager.getSessionId()}:${ctx.model.provider}/${ctx.model.id}` : undefined;
278
+ if (key && ctx.model) {
279
+ pendingFast.set(key, { tier: tierForMode(fastMode), model: ctx.model });
280
+ }
281
+ return rewritten;
282
+ });
283
+
284
+ pi.on("message_end", (event, ctx) => {
285
+ const message = event.message as Record<string, unknown> | undefined;
286
+ if (!message || message.role !== "assistant") return undefined;
287
+ if (typeof message.provider !== "string" || typeof message.model !== "string") return undefined;
288
+ schedulePostTurnRefresh();
289
+ const key = `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
290
+ const pending = pendingFast.get(key);
291
+ pendingFast.delete(key);
292
+ if (!pending) return undefined;
293
+ return correctCodexFastMessage(event.message, pending.model, pending.tier) as { message: never } | undefined;
294
+ });
295
+
296
+ pi.on("session_shutdown", async () => {
297
+ if (postTurnTimer) clearTimeout(postTurnTimer);
298
+ postTurnTimer = undefined;
299
+ for (const controller of liveControllers) controller.abort();
300
+ liveControllers.clear();
301
+ if (timer) clearInterval(timer);
302
+ timer = undefined;
303
+ lastContext = undefined;
304
+ pendingFast.clear();
305
+ inflight.clear();
306
+ });
307
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "pi-myusage",
3
+ "version": "0.1.0",
4
+ "description": "Provider usage, quota and balance extension for Pi: current-model-first matching, 17 providers, persistent Codex Fast routing.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi-extension",
10
+ "pi",
11
+ "usage",
12
+ "quota",
13
+ "balance"
14
+ ],
15
+ "files": [
16
+ "index.ts",
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "pi": {
22
+ "extensions": [
23
+ "./index.ts"
24
+ ]
25
+ },
26
+ "peerDependencies": {
27
+ "@earendil-works/pi-ai": "*",
28
+ "@earendil-works/pi-coding-agent": "*",
29
+ "@earendil-works/pi-tui": "*"
30
+ },
31
+ "devDependencies": {
32
+ "@earendil-works/pi-ai": "^0.87.1",
33
+ "@earendil-works/pi-coding-agent": "^0.87.1",
34
+ "@earendil-works/pi-tui": "^0.87.1",
35
+ "@types/node": "^24.10.0",
36
+ "tsx": "^4.20.6",
37
+ "typescript": "^5.9.3"
38
+ },
39
+ "scripts": {
40
+ "check": "tsc --noEmit",
41
+ "test": "tsx test/selfcheck.ts",
42
+ "verify": "npm run check && npm test"
43
+ }
44
+ }