@narumitw/pi-usage 0.51.0 → 0.52.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 +10 -0
- package/package.json +9 -7
- package/src/format.ts +20 -0
- package/src/index.ts +1 -0
- package/src/providers/opencode-zen.ts +83 -0
- package/src/query.ts +24 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -134,6 +134,16 @@ GitHub's quota endpoint requires the original GitHub OAuth token rather than the
|
|
|
134
134
|
|
|
135
135
|
The extension does not call OpenRouter's account-level `/credits` endpoint because that operation requires a separate management key. OpenRouter documents the distinction between credit and rate limits in its [API limits guide](https://openrouter.ai/docs/api_reference/limits).
|
|
136
136
|
|
|
137
|
+
### OpenCode Go (Zen)
|
|
138
|
+
|
|
139
|
+
- Provider ID: `opencode-go`
|
|
140
|
+
- Semantics: OpenCode Zen plan usage windows—rolling, weekly, and monthly
|
|
141
|
+
- Source: `GET {model base URL}/usage` on the configured `opencode.ai` gateway using Pi's resolved inference API key
|
|
142
|
+
- Displayed data: used percentage and reset time for each window; `rate-limited` windows remain visible at their reported usage, while unknown statuses are reported as unavailable notes
|
|
143
|
+
- Statusline examples: `zen 0% r 4% w 2% m`
|
|
144
|
+
|
|
145
|
+
The usage endpoint is derived from the model's base URL (`…/zen/go/v1/usage`) and is only queried when the resolved origin is `https://opencode.ai`; other origins fail before sending the credential.
|
|
146
|
+
|
|
137
147
|
## 🧭 Current and configured accounts
|
|
138
148
|
|
|
139
149
|
`Current` means the provider and credential used by Pi's selected model. `Configured` means Pi reports runtime auth for another supported provider; it does not mean that provider is active.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, and
|
|
3
|
+
"version": "0.52.0",
|
|
4
|
+
"description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, OpenRouter, and OpenCode Zen.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"quota",
|
|
14
14
|
"codex",
|
|
15
15
|
"copilot",
|
|
16
|
-
"openrouter"
|
|
16
|
+
"openrouter",
|
|
17
|
+
"opencode",
|
|
18
|
+
"zen"
|
|
17
19
|
],
|
|
18
20
|
"files": [
|
|
19
21
|
"src",
|
|
@@ -38,10 +40,10 @@
|
|
|
38
40
|
"@earendil-works/pi-coding-agent": "*"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "2.5.
|
|
42
|
-
"@earendil-works/pi-ai": "0.84.
|
|
43
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
44
|
-
"@types/node": "26.
|
|
43
|
+
"@biomejs/biome": "2.5.8",
|
|
44
|
+
"@earendil-works/pi-ai": "0.84.2",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
46
|
+
"@types/node": "26.2.0",
|
|
45
47
|
"typescript": "7.0.2"
|
|
46
48
|
},
|
|
47
49
|
"repository": {
|
package/src/format.ts
CHANGED
|
@@ -18,6 +18,7 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
|
|
|
18
18
|
if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
19
19
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
20
20
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
21
|
+
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
21
22
|
else formatGenericReport(lines, report);
|
|
22
23
|
|
|
23
24
|
if (report.notes) {
|
|
@@ -35,6 +36,7 @@ export function formatUsageStatusline(report: UsageReport, model?: UsageModel):
|
|
|
35
36
|
const total = report.metrics.find((metric) => metric.id === "usage-total");
|
|
36
37
|
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
37
38
|
}
|
|
39
|
+
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
38
40
|
return undefined;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -140,6 +142,24 @@ function formatOpenRouterReport(lines: string[], report: UsageReport): void {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
|
|
146
|
+
for (const bucket of report.buckets) {
|
|
147
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
148
|
+
const used = bucket.used ?? "unavailable";
|
|
149
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
154
|
+
const parts = ["zen"];
|
|
155
|
+
for (const bucket of report.buckets) {
|
|
156
|
+
if (bucket.used === undefined) continue;
|
|
157
|
+
const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
|
|
158
|
+
parts.push(`${clampPercent(bucket.used).toFixed(0)}% ${compact}`);
|
|
159
|
+
}
|
|
160
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
143
163
|
function formatGenericReport(lines: string[], report: UsageReport): void {
|
|
144
164
|
for (const bucket of report.buckets) {
|
|
145
165
|
lines.push(
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
35
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
36
36
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
37
|
+
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
37
38
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
38
39
|
export {
|
|
39
40
|
adapterForProvider,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { OpenCodeZenPayload, UsageBucket, UsageReport } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const ZEN_WINDOWS = [
|
|
5
|
+
{ key: "rolling", label: "Rolling" },
|
|
6
|
+
{ key: "weekly", label: "Weekly" },
|
|
7
|
+
{ key: "monthly", label: "Monthly" },
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export function normalizeOpenCodeZenPayload(
|
|
11
|
+
payload: OpenCodeZenPayload,
|
|
12
|
+
capturedAt: number,
|
|
13
|
+
): UsageReport {
|
|
14
|
+
const usage = asObject(payload.usage);
|
|
15
|
+
if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
|
|
16
|
+
|
|
17
|
+
const buckets: UsageBucket[] = [];
|
|
18
|
+
const notes: string[] = [];
|
|
19
|
+
for (const window of ZEN_WINDOWS) {
|
|
20
|
+
const raw = asObject(usage[window.key]);
|
|
21
|
+
if (!raw) continue;
|
|
22
|
+
const status = asString(raw.status);
|
|
23
|
+
if (status !== "ok" && status !== "rate-limited") {
|
|
24
|
+
notes.push(`${window.label} window unavailable (${status ?? "unknown status"}).`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const used = asNonnegativeNumber(raw.percent);
|
|
28
|
+
if (used === undefined) continue;
|
|
29
|
+
const resetsAt = asEpochSeconds(raw.resetsAt);
|
|
30
|
+
buckets.push({
|
|
31
|
+
id: window.key,
|
|
32
|
+
label: `${window.label} window`,
|
|
33
|
+
used,
|
|
34
|
+
remaining: 100 - clampPercent(used),
|
|
35
|
+
limit: 100,
|
|
36
|
+
unit: "percent",
|
|
37
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (buckets.length === 0) {
|
|
41
|
+
throw new Error("OpenCode Zen usage endpoint returned no displayable usage data.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
providerId: "opencode-go",
|
|
46
|
+
providerName: "OpenCode Go",
|
|
47
|
+
capturedAt,
|
|
48
|
+
source: "opencode-zen-usage",
|
|
49
|
+
semantics: {
|
|
50
|
+
kind: "consumer-subscription",
|
|
51
|
+
label: "OpenCode Zen plan usage",
|
|
52
|
+
},
|
|
53
|
+
buckets,
|
|
54
|
+
metrics: [],
|
|
55
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
61
|
+
return value as Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function asString(value: unknown): string | undefined {
|
|
65
|
+
if (typeof value !== "string") return undefined;
|
|
66
|
+
return sanitizeDisplayText(value, 80) || undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function asNonnegativeNumber(value: unknown): number | undefined {
|
|
70
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function asEpochSeconds(value: unknown): number | undefined {
|
|
75
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
76
|
+
const parsed = Date.parse(value);
|
|
77
|
+
if (Number.isNaN(parsed)) return undefined;
|
|
78
|
+
return Math.floor(parsed / 1000);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clampPercent(value: number): number {
|
|
82
|
+
return Math.min(100, Math.max(0, value));
|
|
83
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-
|
|
|
3
3
|
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
4
4
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
5
5
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
6
|
+
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
6
7
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
7
8
|
import type {
|
|
8
9
|
CodexBackendPayload,
|
|
9
10
|
GitHubCopilotUsagePayload,
|
|
11
|
+
OpenCodeZenPayload,
|
|
10
12
|
OpenRouterKeyPayload,
|
|
11
13
|
PiModel,
|
|
12
14
|
ResolvedUsageAuth,
|
|
@@ -74,6 +76,21 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
74
76
|
return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
|
|
75
77
|
},
|
|
76
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: "opencode-go",
|
|
81
|
+
displayName: "OpenCode Go",
|
|
82
|
+
semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
|
|
83
|
+
async query(auth, signal, timeoutMs) {
|
|
84
|
+
const payload = await fetchProviderJson(
|
|
85
|
+
opencodeUsageUrl(auth.model.baseUrl),
|
|
86
|
+
auth,
|
|
87
|
+
signal,
|
|
88
|
+
timeoutMs,
|
|
89
|
+
"OpenCode Zen usage endpoint",
|
|
90
|
+
);
|
|
91
|
+
return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
|
|
92
|
+
},
|
|
93
|
+
},
|
|
77
94
|
];
|
|
78
95
|
|
|
79
96
|
export function adapterForProvider(
|
|
@@ -393,6 +410,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
393
410
|
const url = new URL(value);
|
|
394
411
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
395
412
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
413
|
+
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
396
414
|
if (providerId === "github-copilot") {
|
|
397
415
|
return (
|
|
398
416
|
url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname)
|
|
@@ -418,6 +436,12 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
418
436
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
419
437
|
}
|
|
420
438
|
|
|
439
|
+
function opencodeUsageUrl(baseUrl: string | undefined): string {
|
|
440
|
+
const base = baseUrl?.trim().replace(/\/+$/u, "");
|
|
441
|
+
if (!base) throw new Error("OpenCode Go model base URL is unavailable.");
|
|
442
|
+
return `${base}/usage`;
|
|
443
|
+
}
|
|
444
|
+
|
|
421
445
|
function isAbortError(error: unknown): boolean {
|
|
422
446
|
return error instanceof Error && error.name === "AbortError";
|
|
423
447
|
}
|