@latentminds/pi-quotas 0.2.6 → 0.3.1
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 +9 -2
- package/package.json +25 -5
- package/src/config.ts +43 -13
- package/src/extensions/command-quotas/command.ts +23 -1
- package/src/extensions/command-quotas/components/quotas-display.test.ts +29 -0
- package/src/extensions/command-quotas/components/quotas-display.ts +8 -2
- package/src/extensions/command-quotas/provider-commands.test.ts +9 -0
- package/src/extensions/command-quotas/provider-commands.ts +12 -0
- package/src/extensions/command-tokens/command.ts +102 -0
- package/src/extensions/command-tokens/index.ts +1 -0
- package/src/extensions/command-tokens/tokens-display.ts +357 -0
- package/src/extensions/quota-warnings/index.ts +36 -14
- package/src/extensions/token-status/index.ts +241 -0
- package/src/extensions/token-status/provider-detection.test.ts +30 -0
- package/src/extensions/usage-status/index.test.ts +182 -0
- package/src/extensions/usage-status/index.ts +104 -40
- package/src/lib/quotas.ts +12 -1
- package/src/lib/session-tokens.test.ts +137 -0
- package/src/lib/session-tokens.ts +399 -0
- package/src/providers/fetch.test.ts +31 -0
- package/src/providers/fetch.ts +167 -22
- package/src/providers/opencode-go-config.ts +127 -0
- package/src/providers/opencode-go.ts +183 -0
- package/src/providers/parse.test.ts +185 -7
- package/src/providers/providers.ts +229 -21
- package/src/types/quotas.ts +12 -3
- package/src/utils/quotas-severity.ts +14 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @latentminds/pi-quotas
|
|
2
2
|
|
|
3
|
-
Quota monitoring for Pi. Shows remaining usage and rate limits for Anthropic, OpenAI Codex, GitHub Copilot, OpenRouter, and
|
|
3
|
+
Quota monitoring for Pi. Shows remaining usage and rate limits for Anthropic, OpenAI Codex, GitHub Copilot, OpenRouter, Synthetic, Z.ai, and OpenCode Go — directly in your Pi session.
|
|
4
4
|
|
|
5
5
|
## Screenshots
|
|
6
6
|
|
|
@@ -42,6 +42,9 @@ pi -e npm:@latentminds/pi-quotas
|
|
|
42
42
|
| `/github:quotas` | GitHub Copilot quotas only |
|
|
43
43
|
| `/openrouter:quotas` | OpenRouter quotas only |
|
|
44
44
|
| `/synthetic:quotas` | Synthetic quotas only |
|
|
45
|
+
| `/zai:quotas` | Z.ai quotas only |
|
|
46
|
+
| `/opencode-go:quotas`| OpenCode Go quotas only |
|
|
47
|
+
| `/tokens` | Cross-session token/cost usage |
|
|
45
48
|
| `/quotas:settings` | Toggle individual features on or off |
|
|
46
49
|
|
|
47
50
|
|
|
@@ -64,7 +67,7 @@ Automatic notifications when projected usage is on track to exceed limits before
|
|
|
64
67
|
Use `/quotas:settings` to enable or disable:
|
|
65
68
|
|
|
66
69
|
- Combined `/quotas` command
|
|
67
|
-
- Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, `/synthetic:quotas`)
|
|
70
|
+
- Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, `/synthetic:quotas`, `/zai:quotas`, `/opencode-go:quotas`)
|
|
68
71
|
- Footer status widget
|
|
69
72
|
- Quota warning notifications
|
|
70
73
|
- **Defer to Synthetic** — when both pi-quotas and [pi-synthetic](https://www.npmjs.com/package/@aliou/pi-synthetic) are loaded, pi-quotas hides its own Synthetic footer to avoid showing duplicate quota information. Enabled by default; disable if you prefer to see both footers.
|
|
@@ -81,6 +84,8 @@ Settings can be saved globally (`~/.pi/agent/extensions/quotas.json`) or per-pro
|
|
|
81
84
|
| GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
|
|
82
85
|
| OpenRouter | Monthly budget, daily/weekly/monthly usage | USD spending tracking with cents precision; optional per-key budget limits; UTC-based period resets |
|
|
83
86
|
| Synthetic | Subscription, search/hour, free tools, weekly tokens, 5h limit | Request counts and token budgets; rolling five-hour rate limit; weekly token regen |
|
|
87
|
+
| Z.ai | 5h, 7d, monthly web searches | Token utilisation percentages (rolling 5h/7d windows); monthly web-search count limit |
|
|
88
|
+
| OpenCode Go | Rolling 5h, weekly, monthly USD | USD spend tracking against tier limits; cross-session token/cost aggregation via the `/tokens` command |
|
|
84
89
|
|
|
85
90
|
|
|
86
91
|
## Credentials
|
|
@@ -92,6 +97,8 @@ pi-quotas reads existing Pi auth entries from `~/.pi/agent/auth.json`:
|
|
|
92
97
|
- `github-copilot` — GitHub Copilot OAuth token (falls back to `gh auth token` if needed)
|
|
93
98
|
- `openrouter` — OpenRouter API key (Bearer token)
|
|
94
99
|
- `synthetic` — Synthetic API key (set the `SYNTHETIC_API_KEY` environment variable)
|
|
100
|
+
- `zai` — Z.ai (Zhipu AI / GLM Coding Plan) API key
|
|
101
|
+
- `opencode-go` — OpenCode Go workspace ID and auth cookie (set the `OPENCODE_GO_WORKSPACE_ID` and `OPENCODE_GO_AUTH_COOKIE` environment variables, or configure them in the OpenCode Go config file)
|
|
95
102
|
|
|
96
103
|
No additional setup is required - if Pi can use the provider, pi-quotas can check its quotas. For Synthetic, export `SYNTHETIC_API_KEY` in your shell or Pi environment.
|
|
97
104
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latentminds/pi-quotas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"author": "Markus Strazds <markus@latentminds.ai>",
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi"
|
|
12
|
+
],
|
|
9
13
|
"repository": {
|
|
10
14
|
"type": "git",
|
|
11
15
|
"url": "https://github.com/latentminds-ai/pi-quotas"
|
|
@@ -14,30 +18,46 @@
|
|
|
14
18
|
"extensions": [
|
|
15
19
|
"./src/extensions/core/index.ts",
|
|
16
20
|
"./src/extensions/command-quotas/index.ts",
|
|
21
|
+
"./src/extensions/command-tokens/index.ts",
|
|
17
22
|
"./src/extensions/usage-status/index.ts",
|
|
23
|
+
"./src/extensions/token-status/index.ts",
|
|
18
24
|
"./src/extensions/quota-warnings/index.ts"
|
|
19
25
|
],
|
|
20
26
|
"image": "https://raw.githubusercontent.com/latentminds-ai/pi-quotas/main/docs/quotas-dashboard.png"
|
|
21
27
|
},
|
|
22
|
-
"files": [
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
23
33
|
"peerDependencies": {
|
|
24
34
|
"@mariozechner/pi-coding-agent": "*",
|
|
25
35
|
"@mariozechner/pi-tui": "*"
|
|
26
36
|
},
|
|
27
37
|
"peerDependenciesMeta": {
|
|
28
|
-
"@mariozechner/pi-coding-agent": {
|
|
29
|
-
|
|
38
|
+
"@mariozechner/pi-coding-agent": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"@mariozechner/pi-tui": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
30
44
|
},
|
|
31
45
|
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^10.0.1",
|
|
32
47
|
"@mariozechner/pi-coding-agent": "0.61.0",
|
|
33
48
|
"@mariozechner/pi-tui": "0.61.0",
|
|
34
49
|
"@sinclair/typebox": "^0.34.48",
|
|
35
50
|
"@types/node": "^25.0.10",
|
|
51
|
+
"eslint": "^10.6.0",
|
|
52
|
+
"globals": "^17.7.0",
|
|
36
53
|
"typescript": "^5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.63.0",
|
|
37
55
|
"vitest": "^4.0.18"
|
|
38
56
|
},
|
|
39
57
|
"scripts": {
|
|
40
58
|
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "eslint src",
|
|
60
|
+
"lint:fix": "eslint src --fix",
|
|
41
61
|
"test": "vitest run",
|
|
42
62
|
"test:watch": "vitest"
|
|
43
63
|
}
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type QuotasFeatureId =
|
|
|
9
9
|
| "quotasCommand"
|
|
10
10
|
| "providerCommands"
|
|
11
11
|
| "usageStatus"
|
|
12
|
+
| "tokenStatus"
|
|
12
13
|
| "quotaWarnings"
|
|
13
14
|
| "deferToSynthetic";
|
|
14
15
|
|
|
@@ -16,8 +17,7 @@ export const QUOTAS_EXTENSIONS_REQUEST_EVENT =
|
|
|
16
17
|
"quotas:extensions:request" as const;
|
|
17
18
|
export const QUOTAS_EXTENSIONS_REGISTER_EVENT =
|
|
18
19
|
"quotas:extensions:register" as const;
|
|
19
|
-
export const QUOTAS_CONFIG_UPDATED_EVENT =
|
|
20
|
-
"quotas:config:updated" as const;
|
|
20
|
+
export const QUOTAS_CONFIG_UPDATED_EVENT = "quotas:config:updated" as const;
|
|
21
21
|
|
|
22
22
|
export interface QuotasExtensionsRegisterPayload {
|
|
23
23
|
feature: QuotasFeatureId;
|
|
@@ -28,6 +28,7 @@ export interface QuotasConfig {
|
|
|
28
28
|
quotasCommand?: boolean;
|
|
29
29
|
providerCommands?: boolean;
|
|
30
30
|
usageStatus?: boolean;
|
|
31
|
+
tokenStatus?: boolean;
|
|
31
32
|
quotaWarnings?: boolean;
|
|
32
33
|
/** When true and pi-synthetic's usage footer is active, hide pi-quotas' Synthetic footer. */
|
|
33
34
|
deferToSynthetic?: boolean;
|
|
@@ -38,6 +39,7 @@ export interface ResolvedQuotasConfig {
|
|
|
38
39
|
quotasCommand: boolean;
|
|
39
40
|
providerCommands: boolean;
|
|
40
41
|
usageStatus: boolean;
|
|
42
|
+
tokenStatus: boolean;
|
|
41
43
|
quotaWarnings: boolean;
|
|
42
44
|
deferToSynthetic: boolean;
|
|
43
45
|
}
|
|
@@ -47,6 +49,7 @@ const DEFAULT_CONFIG: ResolvedQuotasConfig = {
|
|
|
47
49
|
quotasCommand: true,
|
|
48
50
|
providerCommands: true,
|
|
49
51
|
usageStatus: true,
|
|
52
|
+
tokenStatus: true,
|
|
50
53
|
quotaWarnings: true,
|
|
51
54
|
deferToSynthetic: true,
|
|
52
55
|
};
|
|
@@ -81,10 +84,13 @@ class QuotasConfigStore {
|
|
|
81
84
|
return {
|
|
82
85
|
configVersion: input?.configVersion ?? DEFAULT_CONFIG.configVersion,
|
|
83
86
|
quotasCommand: input?.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
|
|
84
|
-
providerCommands:
|
|
87
|
+
providerCommands:
|
|
88
|
+
input?.providerCommands ?? DEFAULT_CONFIG.providerCommands,
|
|
85
89
|
usageStatus: input?.usageStatus ?? DEFAULT_CONFIG.usageStatus,
|
|
90
|
+
tokenStatus: input?.tokenStatus ?? DEFAULT_CONFIG.tokenStatus,
|
|
86
91
|
quotaWarnings: input?.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
|
|
87
|
-
deferToSynthetic:
|
|
92
|
+
deferToSynthetic:
|
|
93
|
+
input?.deferToSynthetic ?? DEFAULT_CONFIG.deferToSynthetic,
|
|
88
94
|
};
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -118,7 +124,11 @@ class QuotasConfigStore {
|
|
|
118
124
|
async save(scope: "global" | "local", config: QuotasConfig): Promise<void> {
|
|
119
125
|
const path = scope === "global" ? this.globalPath() : this.localPath();
|
|
120
126
|
await mkdir(dirname(path), { recursive: true });
|
|
121
|
-
await writeFile(
|
|
127
|
+
await writeFile(
|
|
128
|
+
path,
|
|
129
|
+
JSON.stringify(this.resolve(config), null, 2) + "\n",
|
|
130
|
+
"utf8",
|
|
131
|
+
);
|
|
122
132
|
await this.load(this.cwd);
|
|
123
133
|
}
|
|
124
134
|
}
|
|
@@ -126,7 +136,8 @@ class QuotasConfigStore {
|
|
|
126
136
|
export const configLoader = new QuotasConfigStore();
|
|
127
137
|
|
|
128
138
|
export async function seedQuotasConfigIfMissing(): Promise<void> {
|
|
129
|
-
if (configLoader.hasConfig("global") || configLoader.hasConfig("local"))
|
|
139
|
+
if (configLoader.hasConfig("global") || configLoader.hasConfig("local"))
|
|
140
|
+
return;
|
|
130
141
|
markMigrationNoticePending();
|
|
131
142
|
try {
|
|
132
143
|
await configLoader.save("global", DEFAULT_CONFIG);
|
|
@@ -158,13 +169,19 @@ const FEATURE_META: Array<{
|
|
|
158
169
|
{
|
|
159
170
|
id: "providerCommands",
|
|
160
171
|
label: "Provider quota commands",
|
|
161
|
-
description:
|
|
172
|
+
description:
|
|
173
|
+
"Toggle `/anthropic:quotas`, `/codex:quotas`, `/github:quotas`, `/openrouter:quotas`, and `/synthetic:quotas`",
|
|
162
174
|
},
|
|
163
175
|
{
|
|
164
176
|
id: "usageStatus",
|
|
165
177
|
label: "Usage status",
|
|
166
178
|
description: "Toggle footer quota status for the active provider",
|
|
167
179
|
},
|
|
180
|
+
{
|
|
181
|
+
id: "tokenStatus",
|
|
182
|
+
label: "Token usage status",
|
|
183
|
+
description: "Toggle the footer token-usage status and /tokens command",
|
|
184
|
+
},
|
|
168
185
|
{
|
|
169
186
|
id: "quotaWarnings",
|
|
170
187
|
label: "Quota warnings",
|
|
@@ -173,7 +190,8 @@ const FEATURE_META: Array<{
|
|
|
173
190
|
{
|
|
174
191
|
id: "deferToSynthetic",
|
|
175
192
|
label: "Defer to Synthetic",
|
|
176
|
-
description:
|
|
193
|
+
description:
|
|
194
|
+
"When pi-synthetic is loaded, hide pi-quotas' Synthetic footer to avoid duplicates",
|
|
177
195
|
},
|
|
178
196
|
];
|
|
179
197
|
|
|
@@ -196,7 +214,9 @@ export function registerQuotasSettings(
|
|
|
196
214
|
|
|
197
215
|
while (true) {
|
|
198
216
|
const choices = FEATURE_META.map((feature) => {
|
|
199
|
-
const loaded = getLoadedFeatures().has(feature.id)
|
|
217
|
+
const loaded = getLoadedFeatures().has(feature.id)
|
|
218
|
+
? ""
|
|
219
|
+
: " (not loaded)";
|
|
200
220
|
const enabled = draft[feature.id] ? "enabled" : "disabled";
|
|
201
221
|
return `${feature.label}: ${enabled}${loaded}`;
|
|
202
222
|
});
|
|
@@ -214,13 +234,23 @@ export function registerQuotasSettings(
|
|
|
214
234
|
return;
|
|
215
235
|
}
|
|
216
236
|
|
|
217
|
-
const feature = FEATURE_META.find((item) =>
|
|
237
|
+
const feature = FEATURE_META.find((item) =>
|
|
238
|
+
selected.startsWith(item.label),
|
|
239
|
+
);
|
|
218
240
|
if (!feature) continue;
|
|
219
|
-
if (
|
|
220
|
-
|
|
241
|
+
if (
|
|
242
|
+
feature.id !== "deferToSynthetic" &&
|
|
243
|
+
!getLoadedFeatures().has(feature.id)
|
|
244
|
+
) {
|
|
245
|
+
ctx.ui.notify(
|
|
246
|
+
`${feature.label} is not loaded by Pi in this session.`,
|
|
247
|
+
"warning",
|
|
248
|
+
);
|
|
221
249
|
continue;
|
|
222
250
|
}
|
|
223
|
-
(draft as unknown as Record<string, boolean>)[feature.id] = !(
|
|
251
|
+
(draft as unknown as Record<string, boolean>)[feature.id] = !(
|
|
252
|
+
draft as unknown as Record<string, boolean>
|
|
253
|
+
)[feature.id];
|
|
224
254
|
}
|
|
225
255
|
},
|
|
226
256
|
});
|
|
@@ -59,10 +59,32 @@ async function openQuotaView(
|
|
|
59
59
|
|
|
60
60
|
if (result === undefined) {
|
|
61
61
|
const snapshots = await loadSnapshots(true);
|
|
62
|
-
ctx.ui.notify(
|
|
62
|
+
ctx.ui.notify(formatSnapshotsForNotify(snapshots), "info");
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Render quota snapshots as a readable multi-line summary for the
|
|
68
|
+
* non-interactive fallback (when `ctx.ui.custom` returns undefined). Avoids
|
|
69
|
+
* dumping raw JSON — which previously leaked raw HTTP error bodies — and
|
|
70
|
+
* skips "not_applicable" providers since they have nothing to report.
|
|
71
|
+
*/
|
|
72
|
+
function formatSnapshotsForNotify(snapshots: Snapshot[]): string {
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
for (const { provider, result } of snapshots) {
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
if (result.error.kind === "not_applicable") continue;
|
|
77
|
+
lines.push(`${provider}: ${result.error.message}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const summary = result.data.windows
|
|
81
|
+
.map((w) => `${w.label} ${w.usedPercent}%`)
|
|
82
|
+
.join(", ");
|
|
83
|
+
lines.push(`${provider}: ${summary || "no windows"}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n") || "No quota data available";
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
export function registerQuotasCommands(pi: ExtensionAPI): void {
|
|
67
89
|
pi.registerCommand("quotas", {
|
|
68
90
|
description: "Display remaining quotas for Anthropic, Codex, GitHub Copilot, and OpenRouter",
|
|
@@ -122,4 +122,33 @@ describe("QuotasComponent", () => {
|
|
|
122
122
|
|
|
123
123
|
vi.useRealTimers();
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it("renders a not_applicable provider silently with a dim note, not a warning", () => {
|
|
127
|
+
const component = makeComponent();
|
|
128
|
+
component.setState({
|
|
129
|
+
type: "loaded",
|
|
130
|
+
snapshots: [
|
|
131
|
+
{
|
|
132
|
+
provider: "anthropic",
|
|
133
|
+
result: {
|
|
134
|
+
success: false,
|
|
135
|
+
error: {
|
|
136
|
+
kind: "not_applicable",
|
|
137
|
+
message: "Direct API key — no subscription usage to report",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const output = component.render(70).join("\n");
|
|
145
|
+
|
|
146
|
+
expect(output).toContain("Anthropic");
|
|
147
|
+
expect(output).toContain("Direct API key");
|
|
148
|
+
// Rendered as a dim informational note, not a warning.
|
|
149
|
+
expect(output).toContain("\x1b[2m");
|
|
150
|
+
expect(output).not.toContain("\x1b[33m");
|
|
151
|
+
expect(output).not.toContain("usage unavailable");
|
|
152
|
+
expect(output).not.toContain("{");
|
|
153
|
+
});
|
|
125
154
|
});
|
|
@@ -38,7 +38,6 @@ function renderProgressBar(
|
|
|
38
38
|
const paceIndex = showPace
|
|
39
39
|
? Math.min(width - 1, Math.round((Math.max(0, Math.min(100, pacePercent ?? 0)) / 100) * width))
|
|
40
40
|
: null;
|
|
41
|
-
const reset = "\x1b[0m";
|
|
42
41
|
const parts: string[] = [];
|
|
43
42
|
for (let idx = 0; idx < width; idx++) {
|
|
44
43
|
if (paceIndex !== null && idx === paceIndex) {
|
|
@@ -141,7 +140,14 @@ export class QuotasComponent implements Component {
|
|
|
141
140
|
lines.push(truncateToWidth(` ${this.theme.fg("accent", title)}`, maxWidth));
|
|
142
141
|
|
|
143
142
|
if (!snapshot.result.success) {
|
|
144
|
-
|
|
143
|
+
// "not applicable" is an expected, non-error state (e.g. a direct
|
|
144
|
+
// Anthropic API key with no subscription usage) — show a dim note,
|
|
145
|
+
// not a warning-coloured error.
|
|
146
|
+
const { error } = snapshot.result;
|
|
147
|
+
const tone = error.kind === "not_applicable" ? "dim" : "warning";
|
|
148
|
+
lines.push(
|
|
149
|
+
truncateToWidth(` ${this.theme.fg(tone, error.message)}`, maxWidth),
|
|
150
|
+
);
|
|
145
151
|
return lines;
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -40,4 +40,13 @@ describe("getProviderCommandInfo", () => {
|
|
|
40
40
|
title: "OpenRouter Quotas",
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
it("maps zai to zai:quotas", () => {
|
|
45
|
+
const info = getProviderCommandInfo("zai");
|
|
46
|
+
expect(info).toMatchObject<Partial<ProviderCommandInfo>>({
|
|
47
|
+
provider: "zai",
|
|
48
|
+
commandName: "zai:quotas",
|
|
49
|
+
title: "Z.ai Quotas",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
43
52
|
});
|
|
@@ -40,5 +40,17 @@ export function getProviderCommandInfo(
|
|
|
40
40
|
commandName: "synthetic:quotas",
|
|
41
41
|
title: "Synthetic Quotas",
|
|
42
42
|
};
|
|
43
|
+
case "zai":
|
|
44
|
+
return {
|
|
45
|
+
provider,
|
|
46
|
+
commandName: "zai:quotas",
|
|
47
|
+
title: "Z.ai Quotas",
|
|
48
|
+
};
|
|
49
|
+
case "opencode-go":
|
|
50
|
+
return {
|
|
51
|
+
provider,
|
|
52
|
+
commandName: "opencode-go:quotas",
|
|
53
|
+
title: "OpenCode Go Quotas",
|
|
54
|
+
};
|
|
43
55
|
}
|
|
44
56
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { aggregateAllSessions } from "../../lib/session-tokens.js";
|
|
6
|
+
import { TokensComponent } from "./tokens-display.js";
|
|
7
|
+
|
|
8
|
+
async function openTokensView(
|
|
9
|
+
ctx: ExtensionCommandContext,
|
|
10
|
+
cwd?: string,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const component = new TokensComponent(
|
|
15
|
+
theme,
|
|
16
|
+
tui,
|
|
17
|
+
() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
done(null);
|
|
20
|
+
},
|
|
21
|
+
() => {
|
|
22
|
+
component.setState({ type: "loading" });
|
|
23
|
+
tui.requestRender();
|
|
24
|
+
void load();
|
|
25
|
+
},
|
|
26
|
+
cwd,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
async function load(): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
const aggregateResult = await aggregateAllSessions({ cwd });
|
|
32
|
+
if (controller.signal.aborted) return;
|
|
33
|
+
component.setState({ type: "loaded", result: aggregateResult });
|
|
34
|
+
tui.requestRender();
|
|
35
|
+
} catch {
|
|
36
|
+
if (controller.signal.aborted) return;
|
|
37
|
+
component.setState({
|
|
38
|
+
type: "loaded",
|
|
39
|
+
result: {
|
|
40
|
+
totals: {
|
|
41
|
+
input: 0,
|
|
42
|
+
output: 0,
|
|
43
|
+
cacheRead: 0,
|
|
44
|
+
cacheWrite: 0,
|
|
45
|
+
totalTokens: 0,
|
|
46
|
+
costTotal: 0,
|
|
47
|
+
},
|
|
48
|
+
byModel: [],
|
|
49
|
+
byProvider: [],
|
|
50
|
+
bySession: [],
|
|
51
|
+
sessionCount: 0,
|
|
52
|
+
messageCount: 0,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
tui.requestRender();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void load();
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
render: (width: number) => component.render(width),
|
|
63
|
+
invalidate: () => component.invalidate(),
|
|
64
|
+
handleInput: (data: string) => component.handleInput(data),
|
|
65
|
+
dispose: () => {
|
|
66
|
+
controller.abort();
|
|
67
|
+
component.destroy();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Fallback for non-interactive mode
|
|
73
|
+
if (result === undefined) {
|
|
74
|
+
const aggregateResult = await aggregateAllSessions({ cwd });
|
|
75
|
+
const { totals } = aggregateResult;
|
|
76
|
+
const lines = [
|
|
77
|
+
`Token Usage: ${aggregateResult.sessionCount} sessions, ${aggregateResult.messageCount} messages`,
|
|
78
|
+
` Input: ${totals.input.toLocaleString()} · Output: ${totals.output.toLocaleString()}`,
|
|
79
|
+
` Cache Read: ${totals.cacheRead.toLocaleString()} · Cache Write: ${totals.cacheWrite.toLocaleString()}`,
|
|
80
|
+
` Total: ${totals.totalTokens.toLocaleString()} tokens · $${totals.costTotal.toFixed(2)}`,
|
|
81
|
+
];
|
|
82
|
+
for (const model of aggregateResult.byModel) {
|
|
83
|
+
lines.push(
|
|
84
|
+
` ${model.provider}/${model.model}: $${model.tokens.costTotal.toFixed(2)} (${model.messageCount} msgs)`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function registerTokensCommand(pi: ExtensionAPI): void {
|
|
92
|
+
pi.registerCommand("tokens", {
|
|
93
|
+
description: "Display token usage and cost across all sessions",
|
|
94
|
+
handler: async (_args, ctx) => {
|
|
95
|
+
await openTokensView(ctx, ctx.cwd);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default async function (pi: ExtensionAPI) {
|
|
101
|
+
registerTokensCommand(pi);
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./command.js";
|