@narumitw/pi-usage 0.60.7 → 0.60.9

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/docs/providers.md CHANGED
@@ -54,7 +54,8 @@ The extension queries the fixed usage endpoint only when both the selected model
54
54
  Custom and proxy origins fail before network access, redirects are rejected, and the credential is never sent to an override from Kimi Code's environment-specific development path.
55
55
 
56
56
  Plan buckets remain integer request counts and are rendered with their source-defined windows.
57
- Unknown units, duplicate windows, missing counts, invalid timestamps, and malformed rows remain unavailable rather than receiving guessed semantics.
57
+ Live responses omit both `used` and `remaining` on untouched rows and may report only `remaining` once usage starts, so a row with a positive `limit` and no counters means zero usage, and when only one counter is present the other is derived from `limit`.
58
+ Counters that are present but malformed, unknown units, duplicate windows, invalid timestamps, and malformed rows remain unavailable rather than receiving guessed semantics.
58
59
  Booster-wallet `amount` and `amountLeft` values use Kimi's first-party conversion of 1,000,000 fixed-point units per cent, while monthly values already arrive in cents.
59
60
  Wallet values retain their currency and stay separate from plan requests and percentages in reports and the statusline.
60
61
  Wallet fields remain unavailable unless the response supplies one consistent currency; missing monthly values are omitted, and an enabled zero cap is shown as zero.
package/package.json CHANGED
@@ -1,76 +1,75 @@
1
1
  {
2
- "name": "@narumitw/pi-usage",
3
- "version": "0.60.7",
4
- "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
- "type": "module",
6
- "license": "MIT",
7
- "private": false,
8
- "keywords": [
9
- "pi-package",
10
- "pi-extension",
11
- "pi",
12
- "usage",
13
- "quota",
14
- "balance",
15
- "deepseek",
16
- "fireworks",
17
- "baseten",
18
- "vercel-ai-gateway",
19
- "codex",
20
- "kimi",
21
- "kimi-coding",
22
- "moonshot",
23
- "moonshotai",
24
- "minimax",
25
- "token-plan",
26
- "copilot",
27
- "openrouter",
28
- "opencode",
29
- "zen",
30
- "zai",
31
- "glm",
32
- "xai",
33
- "grok"
34
- ],
35
- "files": [
36
- "src",
37
- "dist",
38
- "docs",
39
- "README.md",
40
- "LICENSE"
41
- ],
42
- "pi": {
43
- "extensions": [
44
- "./dist/index.ts"
45
- ]
46
- },
47
- "scripts": {
48
- "build": "node scripts/build-runtime.mjs",
49
- "check": "npm run build && biome check --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md && npm run typecheck",
50
- "format": "biome check --write --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md",
51
- "typecheck": "tsc --noEmit",
52
- "prepack": "npm run build"
53
- },
54
- "peerDependencies": {
55
- "@earendil-works/pi-ai": "*",
56
- "@earendil-works/pi-coding-agent": "*",
57
- "@earendil-works/pi-tui": "*"
58
- },
59
- "devDependencies": {
60
- "@biomejs/biome": "2.5.11",
61
- "@earendil-works/pi-ai": "0.85.0",
62
- "@earendil-works/pi-coding-agent": "0.85.0",
63
- "@earendil-works/pi-tui": "0.85.0",
64
- "@types/node": "26.4.1",
65
- "esbuild": "0.28.2",
66
- "typescript": "7.0.2"
67
- },
68
- "repository": {
69
- "type": "git",
70
- "url": "https://github.com/narumiruna/pi-extensions",
71
- "directory": "packages/pi-usage"
72
- },
73
- "dependencies": {
74
- "@narumitw/pi-tui-kit": "^0.61.0"
75
- }
2
+ "name": "@narumitw/pi-usage",
3
+ "version": "0.60.9",
4
+ "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "pi",
12
+ "usage",
13
+ "quota",
14
+ "balance",
15
+ "deepseek",
16
+ "fireworks",
17
+ "baseten",
18
+ "vercel-ai-gateway",
19
+ "codex",
20
+ "kimi",
21
+ "kimi-coding",
22
+ "moonshot",
23
+ "moonshotai",
24
+ "minimax",
25
+ "token-plan",
26
+ "copilot",
27
+ "openrouter",
28
+ "opencode",
29
+ "zen",
30
+ "zai",
31
+ "glm",
32
+ "xai",
33
+ "grok"
34
+ ],
35
+ "files": [
36
+ "src",
37
+ "dist",
38
+ "docs",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "pi": {
43
+ "extensions": [
44
+ "./dist/index.ts"
45
+ ]
46
+ },
47
+ "scripts": {
48
+ "build": "node scripts/build-runtime.mjs",
49
+ "check": "npm run build && biome check --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md && npm run typecheck",
50
+ "format": "biome check --write --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md",
51
+ "typecheck": "tsc --noEmit",
52
+ "prepack": "npm run build"
53
+ },
54
+ "peerDependencies": {
55
+ "@earendil-works/pi-ai": "*",
56
+ "@earendil-works/pi-coding-agent": "*",
57
+ "@earendil-works/pi-tui": "*"
58
+ },
59
+ "devDependencies": {
60
+ "@earendil-works/pi-ai": "0.86.0",
61
+ "@earendil-works/pi-coding-agent": "0.86.0",
62
+ "@earendil-works/pi-tui": "0.86.0",
63
+ "@types/node": "26.6.2",
64
+ "esbuild": "0.28.2",
65
+ "typescript": "7.0.2"
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "https://github.com/narumiruna/pi-extensions",
70
+ "directory": "packages/pi-usage"
71
+ },
72
+ "dependencies": {
73
+ "@narumitw/pi-tui-kit": "^0.61.0"
74
+ }
76
75
  }
@@ -1,14 +1,10 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- ExtensionContext,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
2
  import {
7
- codexFastAvailability,
8
- codexFastIsEffective,
9
- codexFastStatusLabel,
10
- correctCodexFastMessageCost,
11
- rewriteCodexFastPayload,
3
+ codexFastAvailability,
4
+ codexFastIsEffective,
5
+ codexFastStatusLabel,
6
+ correctCodexFastMessageCost,
7
+ rewriteCodexFastPayload,
12
8
  } from "./codex-fast.js";
13
9
  import { errorMessage } from "./core.js";
14
10
  import { isStaleExtensionContextError } from "./query.js";
@@ -21,200 +17,168 @@ type PendingFastRequest = { fastRequested: boolean; model: PiModel };
21
17
  export const FAST_USAGE_WARNING = "Fast is about 1.5× faster and uses more of your plan allowance.";
22
18
 
23
19
  export function registerCodexFastMode(
24
- pi: ExtensionAPI,
25
- settingsRuntime: UsageSettingsRuntime,
26
- refreshStatus: (ctx: ExtensionContext) => void,
27
- options: { registerSessionStart?: boolean } = {},
20
+ pi: ExtensionAPI,
21
+ settingsRuntime: UsageSettingsRuntime,
22
+ refreshStatus: (ctx: ExtensionContext) => void,
23
+ options: { registerSessionStart?: boolean } = {},
28
24
  ) {
29
- let sessionController = new AbortController();
30
- let generation = 0;
31
- const pendingFastRequests = new Map<string, PendingFastRequest>();
25
+ let sessionController = new AbortController();
26
+ let generation = 0;
27
+ const pendingFastRequests = new Map<string, PendingFastRequest>();
32
28
 
33
- const toggle = async (
34
- ctx: ExtensionCommandContext,
35
- enabled: boolean,
36
- callerSignal?: AbortSignal,
37
- ): Promise<boolean> => {
38
- const ownerGeneration = generation;
39
- const sessionId = ctx.sessionManager.getSessionId();
40
- const signal = callerSignal
41
- ? AbortSignal.any([callerSignal, sessionController.signal])
42
- : sessionController.signal;
43
- try {
44
- await settingsRuntime.update({ codexFastMode: enabled }, signal);
45
- } catch (error) {
46
- if (isAbortError(error) || isStaleExtensionContextError(error)) return false;
47
- ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
48
- return false;
49
- }
50
- if (
51
- signal.aborted ||
52
- ownerGeneration !== generation ||
53
- ctx.sessionManager.getSessionId() !== sessionId
54
- ) {
55
- return false;
56
- }
57
- refreshStatus(ctx);
58
- ctx.ui.notify(
59
- enabled
60
- ? `Codex Fast mode enabled. ${FAST_USAGE_WARNING}`
61
- : "Codex Fast mode disabled; standard routing will be used.",
62
- "info",
63
- );
64
- return true;
65
- };
29
+ const toggle = async (
30
+ ctx: ExtensionCommandContext,
31
+ enabled: boolean,
32
+ callerSignal?: AbortSignal,
33
+ ): Promise<boolean> => {
34
+ const ownerGeneration = generation;
35
+ const sessionId = ctx.sessionManager.getSessionId();
36
+ const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
37
+ try {
38
+ await settingsRuntime.update({ codexFastMode: enabled }, signal);
39
+ } catch (error) {
40
+ if (isAbortError(error) || isStaleExtensionContextError(error)) return false;
41
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
42
+ return false;
43
+ }
44
+ if (signal.aborted || ownerGeneration !== generation || ctx.sessionManager.getSessionId() !== sessionId) {
45
+ return false;
46
+ }
47
+ refreshStatus(ctx);
48
+ ctx.ui.notify(
49
+ enabled
50
+ ? `Codex Fast mode enabled. ${FAST_USAGE_WARNING}`
51
+ : "Codex Fast mode disabled; standard routing will be used.",
52
+ "info",
53
+ );
54
+ return true;
55
+ };
66
56
 
67
- pi.registerCommand("fast", {
68
- description: "Toggle Codex Fast mode",
69
- handler: async (args, ctx) => {
70
- if (args.trim()) {
71
- if (!ctx.hasUI) throw new Error("/fast does not accept arguments.");
72
- ctx.ui.notify("/fast does not accept arguments.", "warning");
73
- return;
74
- }
75
- if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
76
- const availability = codexFastAvailability(
77
- ctx.model,
78
- settingsRuntime.get().settings.codexFastMode,
79
- );
80
- if (availability.kind === "not-codex") {
81
- ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
82
- return;
83
- }
84
- if (availability.kind === "unavailable") {
85
- ctx.ui.notify(availability.reason, "warning");
86
- return;
87
- }
88
- if (settingsRuntime.get().kind === "invalid") {
89
- ctx.ui.notify(
90
- "pi-usage.json is invalid; repair it and run /reload before changing Fast mode.",
91
- "error",
92
- );
93
- return;
94
- }
95
- await toggle(ctx, !availability.enabled);
96
- },
97
- });
57
+ pi.registerCommand("fast", {
58
+ description: "Toggle Codex Fast mode",
59
+ handler: async (args, ctx) => {
60
+ if (args.trim()) {
61
+ if (!ctx.hasUI) throw new Error("/fast does not accept arguments.");
62
+ ctx.ui.notify("/fast does not accept arguments.", "warning");
63
+ return;
64
+ }
65
+ if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
66
+ const availability = codexFastAvailability(ctx.model, settingsRuntime.get().settings.codexFastMode);
67
+ if (availability.kind === "not-codex") {
68
+ ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
69
+ return;
70
+ }
71
+ if (availability.kind === "unavailable") {
72
+ ctx.ui.notify(availability.reason, "warning");
73
+ return;
74
+ }
75
+ if (settingsRuntime.get().kind === "invalid") {
76
+ ctx.ui.notify("pi-usage.json is invalid; repair it and run /reload before changing Fast mode.", "error");
77
+ return;
78
+ }
79
+ await toggle(ctx, !availability.enabled);
80
+ },
81
+ });
98
82
 
99
- const prepareSession = (ctx: ExtensionContext): Promise<void> => {
100
- const sessionId = ctx.sessionManager.getSessionId();
101
- generation += 1;
102
- sessionController.abort();
103
- pendingFastRequests.clear();
104
- sessionController = new AbortController();
105
- const ownerGeneration = generation;
106
- return (async () => {
107
- let state: Readonly<UsageSettingsState>;
108
- try {
109
- state = await settingsRuntime.reload(sessionController.signal);
110
- } catch (error) {
111
- if (sessionController.signal.aborted || ownerGeneration !== generation) return;
112
- if (ctx.hasUI) {
113
- ctx.ui.notify(
114
- `Could not load pi-usage.json; using defaults. ${errorMessage(error)}`,
115
- "warning",
116
- );
117
- }
118
- return;
119
- }
120
- if (
121
- sessionController.signal.aborted ||
122
- ownerGeneration !== generation ||
123
- ctx.sessionManager.getSessionId() !== sessionId
124
- ) {
125
- return;
126
- }
127
- if (ctx.hasUI && state.kind === "invalid") {
128
- ctx.ui.notify(
129
- `Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
130
- "warning",
131
- );
132
- }
133
- refreshStatus(ctx);
134
- })();
135
- };
83
+ const prepareSession = (ctx: ExtensionContext): Promise<void> => {
84
+ const sessionId = ctx.sessionManager.getSessionId();
85
+ generation += 1;
86
+ sessionController.abort();
87
+ pendingFastRequests.clear();
88
+ sessionController = new AbortController();
89
+ const ownerGeneration = generation;
90
+ return (async () => {
91
+ let state: Readonly<UsageSettingsState>;
92
+ try {
93
+ state = await settingsRuntime.reload(sessionController.signal);
94
+ } catch (error) {
95
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return;
96
+ if (ctx.hasUI) {
97
+ ctx.ui.notify(`Could not load pi-usage.json; using defaults. ${errorMessage(error)}`, "warning");
98
+ }
99
+ return;
100
+ }
101
+ if (
102
+ sessionController.signal.aborted ||
103
+ ownerGeneration !== generation ||
104
+ ctx.sessionManager.getSessionId() !== sessionId
105
+ ) {
106
+ return;
107
+ }
108
+ if (ctx.hasUI && state.kind === "invalid") {
109
+ ctx.ui.notify(`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`, "warning");
110
+ }
111
+ refreshStatus(ctx);
112
+ })();
113
+ };
136
114
 
137
- if (options.registerSessionStart !== false) {
138
- pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
139
- }
115
+ if (options.registerSessionStart !== false) {
116
+ pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
117
+ }
140
118
 
141
- pi.on("before_provider_request", (event, ctx) => {
142
- const rewritten = rewriteCodexFastPayload(
143
- event.payload,
144
- ctx.model,
145
- settingsRuntime.get().settings.codexFastMode,
146
- );
147
- const key = activeRequestKey(ctx);
148
- if (key && ctx.model) {
149
- pendingFastRequests.set(key, {
150
- fastRequested: isRecord(rewritten) && rewritten.service_tier === "priority",
151
- model: ctx.model,
152
- });
153
- }
154
- return rewritten;
155
- });
156
- pi.on("message_end", (event, ctx) => {
157
- const request = consumeFastRequest(ctx, event.message, pendingFastRequests);
158
- if (request === NO_FAST_REQUEST) return undefined;
159
- const message = correctCodexFastMessageCost(
160
- event.message,
161
- request.model,
162
- request.fastRequested,
163
- );
164
- return message ? { message: message as never } : undefined;
165
- });
166
- pi.on("session_shutdown", async () => {
167
- generation += 1;
168
- sessionController.abort();
169
- pendingFastRequests.clear();
170
- await settingsRuntime.flush();
171
- });
119
+ pi.on("before_provider_request", (event, ctx) => {
120
+ const rewritten = rewriteCodexFastPayload(event.payload, ctx.model, settingsRuntime.get().settings.codexFastMode);
121
+ const key = activeRequestKey(ctx);
122
+ if (key && ctx.model) {
123
+ pendingFastRequests.set(key, {
124
+ fastRequested: isRecord(rewritten) && rewritten.service_tier === "priority",
125
+ model: ctx.model,
126
+ });
127
+ }
128
+ return rewritten;
129
+ });
130
+ pi.on("message_end", (event, ctx) => {
131
+ const request = consumeFastRequest(ctx, event.message, pendingFastRequests);
132
+ if (request === NO_FAST_REQUEST) return undefined;
133
+ const message = correctCodexFastMessageCost(event.message, request.model, request.fastRequested);
134
+ return message ? { message: message as never } : undefined;
135
+ });
136
+ pi.on("session_shutdown", async () => {
137
+ generation += 1;
138
+ sessionController.abort();
139
+ pendingFastRequests.clear();
140
+ await settingsRuntime.flush();
141
+ });
172
142
 
173
- return {
174
- prepareSession,
175
- availability(model: PiModel | undefined) {
176
- return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
177
- },
178
- decorateStatus(model: PiModel | undefined, status: string) {
179
- return codexFastStatusLabel(
180
- status,
181
- codexFastIsEffective(model, settingsRuntime.get().settings.codexFastMode),
182
- );
183
- },
184
- toggle,
185
- };
143
+ return {
144
+ prepareSession,
145
+ availability(model: PiModel | undefined) {
146
+ return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
147
+ },
148
+ decorateStatus(model: PiModel | undefined, status: string) {
149
+ return codexFastStatusLabel(status, codexFastIsEffective(model, settingsRuntime.get().settings.codexFastMode));
150
+ },
151
+ toggle,
152
+ };
186
153
  }
187
154
 
188
155
  function activeRequestKey(ctx: ExtensionContext): string | undefined {
189
- const model = ctx.model;
190
- return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : undefined;
156
+ const model = ctx.model;
157
+ return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : undefined;
191
158
  }
192
159
 
193
160
  function consumeFastRequest(
194
- ctx: ExtensionContext,
195
- message: unknown,
196
- pending: Map<string, PendingFastRequest>,
161
+ ctx: ExtensionContext,
162
+ message: unknown,
163
+ pending: Map<string, PendingFastRequest>,
197
164
  ): PendingFastRequest | typeof NO_FAST_REQUEST {
198
- if (!isRecord(message) || message.role !== "assistant") return NO_FAST_REQUEST;
199
- const key = messageRequestKey(ctx, message);
200
- if (!key) return NO_FAST_REQUEST;
201
- const request = pending.get(key);
202
- pending.delete(key);
203
- return request ?? NO_FAST_REQUEST;
165
+ if (!isRecord(message) || message.role !== "assistant") return NO_FAST_REQUEST;
166
+ const key = messageRequestKey(ctx, message);
167
+ if (!key) return NO_FAST_REQUEST;
168
+ const request = pending.get(key);
169
+ pending.delete(key);
170
+ return request ?? NO_FAST_REQUEST;
204
171
  }
205
172
 
206
- function messageRequestKey(
207
- ctx: ExtensionContext,
208
- message: Record<string, unknown>,
209
- ): string | undefined {
210
- if (typeof message.provider !== "string" || typeof message.model !== "string") return undefined;
211
- return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
173
+ function messageRequestKey(ctx: ExtensionContext, message: Record<string, unknown>): string | undefined {
174
+ if (typeof message.provider !== "string" || typeof message.model !== "string") return undefined;
175
+ return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
212
176
  }
213
177
 
214
178
  function isRecord(value: unknown): value is Record<string, unknown> {
215
- return typeof value === "object" && value !== null && !Array.isArray(value);
179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
216
180
  }
217
181
 
218
182
  function isAbortError(error: unknown): boolean {
219
- return error instanceof Error && error.name === "AbortError";
183
+ return error instanceof Error && error.name === "AbortError";
220
184
  }