@narumitw/pi-usage 0.63.1 → 0.64.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 +3 -2
- package/dist/chunks/{chunk-ENCC7IBP.ts → chunk-ACPQJ2CM.ts} +379 -104
- package/dist/chunks/chunk-ACPQJ2CM.ts.map +7 -0
- package/dist/chunks/{openai-companion-auth-6VLPFVHF.ts → openai-companion-auth-FGFC6DBT.ts} +2 -2
- package/dist/chunks/{openai-companion-usage-4NOBHGP7.ts → openai-companion-usage-TGHK2OQR.ts} +2 -2
- package/dist/index.ts +45 -4
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +19 -0
- package/package.json +2 -1
- package/src/format.ts +32 -0
- package/src/index.ts +6 -0
- package/src/providers/command-code.ts +228 -0
- package/src/query.ts +107 -1
- package/src/types.ts +29 -0
- package/src/usage.ts +16 -3
- package/dist/chunks/chunk-ENCC7IBP.ts.map +0 -7
- /package/dist/chunks/{openai-companion-auth-6VLPFVHF.ts.map → openai-companion-auth-FGFC6DBT.ts.map} +0 -0
- /package/dist/chunks/{openai-companion-usage-4NOBHGP7.ts.map → openai-companion-usage-TGHK2OQR.ts.map} +0 -0
package/docs/providers.md
CHANGED
|
@@ -17,6 +17,7 @@ The README contains the capability overview and shared security requirements.
|
|
|
17
17
|
- [Vercel AI Gateway](#vercel-ai-gateway-credits)
|
|
18
18
|
- [Baseten](#baseten-model-apis-spend)
|
|
19
19
|
- [OpenCode Go](#opencode-go-zen)
|
|
20
|
+
- [Command Code](#command-code)
|
|
20
21
|
- [xAI](#xai-consumer-subscriptions)
|
|
21
22
|
- [Z.AI](#zai-glm-coding-plan)
|
|
22
23
|
|
|
@@ -265,6 +266,24 @@ The fixed endpoint is queried only when the OpenCode Go model uses the official
|
|
|
265
266
|
When resolved provider auth includes a base URL, that URL must use the same origin.
|
|
266
267
|
Other origins fail before the credential is sent.
|
|
267
268
|
|
|
269
|
+
### Command Code
|
|
270
|
+
|
|
271
|
+
- Provider ID: `command-code`
|
|
272
|
+
- Semantics: plan credits plus rolling 5-hour and weekly spending caps
|
|
273
|
+
- Account route: `GET https://api.commandcode.ai/alpha/whoami`
|
|
274
|
+
- Credits route: `GET https://api.commandcode.ai/alpha/billing/credits`
|
|
275
|
+
- Plan route: `GET https://api.commandcode.ai/alpha/billing/subscriptions`
|
|
276
|
+
- Period route: `GET https://api.commandcode.ai/alpha/usage/summary`
|
|
277
|
+
- Displayed data: five-hour and weekly spend caps in USD with reset times, the billing-period credit pool and remaining balance, plan name and status, request and token totals, and purchased or free credits when present
|
|
278
|
+
- Statusline examples: `cmd 99% 5h 92% wk 96% mo`
|
|
279
|
+
|
|
280
|
+
The official `@commandcode/pi-commandcode-provider` extension registers this provider through `pi.registerProvider()`, so `command-code` is not a Pi built-in provider.
|
|
281
|
+
Pi still resolves its stored `/login` API key through the normal provider registry, and the extension sends it as Bearer authorization only when the selected model and resolved auth share the official `https://api.commandcode.ai` origin.
|
|
282
|
+
`credits.monthlyCredits`, `purchasedCredits`, and `freeCredits` are remaining amounts, so the billing-period pool is their sum plus `usage/summary.totalCost`, and `windowLimits.*.resetAt` is a millisecond epoch.
|
|
283
|
+
The account response is required; the credits, plan, and period responses degrade to individual notes when unavailable, and a plan without rolling limits reports that instead of inventing windows.
|
|
284
|
+
These are undocumented Command Code endpoints pinned on 2026-09-29 against live responses and the same routes the Command Code CLI `/usage` command reads.
|
|
285
|
+
They may change without notice, and this paragraph is the contract evidence for the reviewed behavior.
|
|
286
|
+
|
|
268
287
|
### xAI consumer subscriptions
|
|
269
288
|
|
|
270
289
|
- Provider ID: `xai`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.1",
|
|
4
4
|
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"baseten",
|
|
18
18
|
"vercel-ai-gateway",
|
|
19
19
|
"codex",
|
|
20
|
+
"command-code",
|
|
20
21
|
"kimi",
|
|
21
22
|
"kimi-coding",
|
|
22
23
|
"moonshot",
|
package/src/format.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
|
|
|
37
37
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
38
38
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
39
39
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
40
|
+
else if (report.providerId === "command-code") formatCommandCodeReport(lines, report);
|
|
40
41
|
else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
|
|
41
42
|
else if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
42
43
|
formatMoonshotReport(lines, report);
|
|
@@ -86,6 +87,7 @@ export function formatUsageStatusline(
|
|
|
86
87
|
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
87
88
|
}
|
|
88
89
|
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
90
|
+
if (report.providerId === "command-code") return formatCommandCodeStatusline(report);
|
|
89
91
|
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
90
92
|
if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
91
93
|
return formatMoonshotStatusline(report);
|
|
@@ -293,6 +295,36 @@ function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
|
293
295
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
function formatCommandCodeReport(lines: string[], report: UsageReport): void {
|
|
299
|
+
for (const bucket of report.buckets) {
|
|
300
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
301
|
+
if (bucket.used === undefined || bucket.limit === undefined) {
|
|
302
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
lines.push(
|
|
306
|
+
`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatUsd(bucket.used)} of ${formatUsd(bucket.limit)} used · ${percentRemaining(bucket)}% left${reset}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
for (const metric of report.metrics) {
|
|
310
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function formatCommandCodeStatusline(report: UsageReport): string | undefined {
|
|
315
|
+
const parts = ["cmd"];
|
|
316
|
+
for (const [id, label] of [
|
|
317
|
+
["five-hour", "5h"],
|
|
318
|
+
["weekly", "wk"],
|
|
319
|
+
["monthly", "mo"],
|
|
320
|
+
] as const) {
|
|
321
|
+
const bucket = report.buckets.find((candidate) => candidate.id === id);
|
|
322
|
+
if (bucket?.unit !== "usd" || bucket.limit === undefined || bucket.remaining === undefined) continue;
|
|
323
|
+
parts.push(`${percentRemaining(bucket)}% ${label}`);
|
|
324
|
+
}
|
|
325
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
296
328
|
function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
297
329
|
for (const bucket of report.buckets) {
|
|
298
330
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
35
|
export { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
36
36
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
37
|
+
export { commandCodeOrgId, normalizeCommandCodeUsagePayload } from "./providers/command-code.js";
|
|
37
38
|
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
38
39
|
export {
|
|
39
40
|
createFireworksAdapter,
|
|
@@ -80,6 +81,11 @@ export {
|
|
|
80
81
|
} from "./settings.js";
|
|
81
82
|
export type {
|
|
82
83
|
BasetenBillingUsagePayload,
|
|
84
|
+
CommandCodeAccountPayload,
|
|
85
|
+
CommandCodeCreditsPayload,
|
|
86
|
+
CommandCodeSubscriptionPayload,
|
|
87
|
+
CommandCodeUsageBundle,
|
|
88
|
+
CommandCodeUsageSummaryPayload,
|
|
83
89
|
DeepSeekBalancePayload,
|
|
84
90
|
FireworksAccountsPayload,
|
|
85
91
|
FireworksBillingSummaryPayload,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type {
|
|
3
|
+
CommandCodeAccountPayload,
|
|
4
|
+
CommandCodeUsageBundle,
|
|
5
|
+
UsageBucket,
|
|
6
|
+
UsageMetric,
|
|
7
|
+
UsageReport,
|
|
8
|
+
} from "../types.js";
|
|
9
|
+
|
|
10
|
+
const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
11
|
+
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
12
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/u;
|
|
13
|
+
const MILLISECOND_EPOCH_FLOOR = 1e12;
|
|
14
|
+
const WINDOWS = [
|
|
15
|
+
{ key: "fiveHour", id: "five-hour", label: "Five-hour window", windowMinutes: FIVE_HOUR_WINDOW_MINUTES },
|
|
16
|
+
{ key: "weekly", id: "weekly", label: "Weekly window", windowMinutes: WEEKLY_WINDOW_MINUTES },
|
|
17
|
+
] as const;
|
|
18
|
+
const PLAN_LABELS: Readonly<Record<string, string>> = {
|
|
19
|
+
"individual-go": "Go",
|
|
20
|
+
"individual-goat": "GOAT",
|
|
21
|
+
"individual-pro": "Pro",
|
|
22
|
+
"individual-provider": "Provider",
|
|
23
|
+
"individual-max": "Max",
|
|
24
|
+
"individual-ultra": "Ultra",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function commandCodeOrgId(account: CommandCodeAccountPayload): string | undefined {
|
|
28
|
+
const id = asObject(account.org)?.id;
|
|
29
|
+
return typeof id === "string" && IDENTIFIER_PATTERN.test(id) ? id : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Source contract observed live on 2026-09-29. Command Code serves the account, credits, plan,
|
|
34
|
+
* and billing-period usage that its CLI `/usage` command displays from the undocumented
|
|
35
|
+
* `https://api.commandcode.ai/alpha/*` endpoints, authenticated with the same Bearer API key Pi
|
|
36
|
+
* stores for the `command-code` provider. `resetAt` is a millisecond epoch, plan credits and
|
|
37
|
+
* rolling-window caps are USD-denominated, and `org` may be `null` for a personal account.
|
|
38
|
+
* `credits.monthlyCredits`, `purchasedCredits`, and `freeCredits` are remaining amounts, so the
|
|
39
|
+
* billing-period pool is their sum plus `usage/summary.totalCost`.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeCommandCodeUsagePayload(bundle: CommandCodeUsageBundle, capturedAt: number): UsageReport {
|
|
42
|
+
const account = asObject(bundle.account);
|
|
43
|
+
const user = asObject(account?.user);
|
|
44
|
+
const accountLabel = asString(user?.userName) ?? asString(user?.name);
|
|
45
|
+
if (!accountLabel) {
|
|
46
|
+
throw new Error("Command Code account response omitted a safe account name.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const creditsPayload = asObject(bundle.credits);
|
|
50
|
+
const credits = asObject(creditsPayload?.credits);
|
|
51
|
+
const windowLimits = asObject(creditsPayload?.windowLimits);
|
|
52
|
+
const subscription = asObject(asObject(bundle.subscription)?.data);
|
|
53
|
+
const usage = asObject(bundle.usage);
|
|
54
|
+
|
|
55
|
+
const buckets: UsageBucket[] = [];
|
|
56
|
+
for (const window of WINDOWS) {
|
|
57
|
+
const bucket = parseWindow(windowLimits?.[window.key], window);
|
|
58
|
+
if (bucket) buckets.push(bucket);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const totalCost = asNonnegativeNumber(usage?.totalCost);
|
|
62
|
+
const remainingCredits = commandCodeRemainingCredits(credits);
|
|
63
|
+
const monthlyReset = asIsoEpochSeconds(subscription?.currentPeriodEnd);
|
|
64
|
+
if (totalCost !== undefined && remainingCredits !== undefined) {
|
|
65
|
+
const pool = totalCost + remainingCredits;
|
|
66
|
+
if (pool > 0) {
|
|
67
|
+
buckets.push({
|
|
68
|
+
id: "monthly",
|
|
69
|
+
label: "Monthly credits",
|
|
70
|
+
used: totalCost,
|
|
71
|
+
remaining: remainingCredits,
|
|
72
|
+
limit: pool,
|
|
73
|
+
unit: "usd",
|
|
74
|
+
period: "billing-period",
|
|
75
|
+
...(monthlyReset !== undefined ? { resetsAt: monthlyReset } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const metrics: UsageMetric[] = [];
|
|
81
|
+
const plan = planLabel(subscription?.planId);
|
|
82
|
+
const planStatus = asString(subscription?.status, 40);
|
|
83
|
+
if (plan || planStatus) {
|
|
84
|
+
metrics.push({
|
|
85
|
+
id: "plan",
|
|
86
|
+
label: "Plan",
|
|
87
|
+
value: [plan, planStatus ? `(${planStatus})` : undefined].filter(Boolean).join(" "),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (buckets.every((bucket) => bucket.id !== "monthly")) {
|
|
91
|
+
if (remainingCredits !== undefined) {
|
|
92
|
+
metrics.push({
|
|
93
|
+
id: "monthly-credits",
|
|
94
|
+
label: "Monthly credits remaining",
|
|
95
|
+
value: remainingCredits,
|
|
96
|
+
unit: "usd",
|
|
97
|
+
});
|
|
98
|
+
} else if (totalCost !== undefined) {
|
|
99
|
+
metrics.push({ id: "period-cost", label: "Cost this billing period", value: totalCost, unit: "usd" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const purchased = asNonnegativeNumber(credits?.purchasedCredits);
|
|
103
|
+
if (purchased) {
|
|
104
|
+
metrics.push({ id: "purchased-credits", label: "Purchased credits remaining", value: purchased, unit: "usd" });
|
|
105
|
+
}
|
|
106
|
+
const free = asNonnegativeNumber(credits?.freeCredits);
|
|
107
|
+
if (free) {
|
|
108
|
+
metrics.push({ id: "free-credits", label: "Free credits remaining", value: free, unit: "usd" });
|
|
109
|
+
}
|
|
110
|
+
const totalCount = asNonnegativeNumber(usage?.totalCount);
|
|
111
|
+
if (totalCount !== undefined) {
|
|
112
|
+
metrics.push({ id: "requests", label: "Requests this period", value: totalCount, unit: "count" });
|
|
113
|
+
}
|
|
114
|
+
const totalTokens = asNonnegativeNumber(usage?.totalTokens);
|
|
115
|
+
if (totalTokens !== undefined) {
|
|
116
|
+
metrics.push({ id: "tokens", label: "Tokens this period", value: totalTokens, unit: "count" });
|
|
117
|
+
} else {
|
|
118
|
+
const tokensIn = asNonnegativeNumber(usage?.totalTokensIn);
|
|
119
|
+
const tokensOut = asNonnegativeNumber(usage?.totalTokensOut);
|
|
120
|
+
if (tokensIn !== undefined) {
|
|
121
|
+
metrics.push({ id: "tokens-in", label: "Input tokens", value: tokensIn, unit: "count" });
|
|
122
|
+
}
|
|
123
|
+
if (tokensOut !== undefined) {
|
|
124
|
+
metrics.push({ id: "tokens-out", label: "Output tokens", value: tokensOut, unit: "count" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const notes: string[] = [];
|
|
129
|
+
if (!creditsPayload) notes.push("Command Code credits were unavailable.");
|
|
130
|
+
else if (windowLimits?.limited === false) {
|
|
131
|
+
notes.push("Rolling 5-hour and weekly limits are not enabled for this plan.");
|
|
132
|
+
}
|
|
133
|
+
if (!subscription) notes.push("Command Code plan details were unavailable.");
|
|
134
|
+
if (!usage) notes.push("Command Code billing-period usage was unavailable.");
|
|
135
|
+
|
|
136
|
+
if (buckets.length === 0 && metrics.length === 0) {
|
|
137
|
+
throw new Error("Command Code usage response returned no displayable usage data.");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
providerId: "command-code",
|
|
142
|
+
providerName: "Command Code",
|
|
143
|
+
capturedAt,
|
|
144
|
+
source: "command-code-alpha-usage",
|
|
145
|
+
semantics: { kind: "consumer-subscription", label: "Command Code plan credits and rolling limits" },
|
|
146
|
+
accountLabel,
|
|
147
|
+
buckets,
|
|
148
|
+
metrics,
|
|
149
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseWindow(value: unknown, window: (typeof WINDOWS)[number]): UsageBucket | undefined {
|
|
154
|
+
const row = asObject(value);
|
|
155
|
+
if (!row) return undefined;
|
|
156
|
+
const used = asNonnegativeNumber(row.used);
|
|
157
|
+
const cap = asNonnegativeNumber(row.cap);
|
|
158
|
+
if (used === undefined || cap === undefined || cap <= 0) return undefined;
|
|
159
|
+
const resetsAt = asMillisecondEpochSeconds(row.resetAt);
|
|
160
|
+
return {
|
|
161
|
+
id: window.id,
|
|
162
|
+
label: window.label,
|
|
163
|
+
used,
|
|
164
|
+
remaining: Math.max(0, cap - used),
|
|
165
|
+
limit: cap,
|
|
166
|
+
unit: "usd",
|
|
167
|
+
windowMinutes: window.windowMinutes,
|
|
168
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function commandCodeRemainingCredits(credits: Record<string, unknown> | undefined): number | undefined {
|
|
173
|
+
if (!credits) return undefined;
|
|
174
|
+
const parts = ["monthlyCredits", "purchasedCredits", "freeCredits"]
|
|
175
|
+
.map((key) => asNonnegativeNumber(credits[key]))
|
|
176
|
+
.filter((value): value is number => value !== undefined);
|
|
177
|
+
if (parts.length === 0) return undefined;
|
|
178
|
+
return parts.reduce((total, value) => total + value, 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function planLabel(value: unknown): string | undefined {
|
|
182
|
+
const raw = asString(value, 40);
|
|
183
|
+
if (!raw) return undefined;
|
|
184
|
+
const known = PLAN_LABELS[raw];
|
|
185
|
+
if (known) return known;
|
|
186
|
+
const words = raw
|
|
187
|
+
.replace(/^individual[-_]/u, "")
|
|
188
|
+
.replace(/[-_]+/gu, " ")
|
|
189
|
+
.trim();
|
|
190
|
+
return words ? capitalize(words) : raw;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
194
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
195
|
+
return value as Record<string, unknown>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function asString(value: unknown, maxLength = 80): string | undefined {
|
|
199
|
+
if (typeof value !== "string") return undefined;
|
|
200
|
+
return sanitizeDisplayText(value, maxLength) || undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function asNonnegativeNumber(value: unknown): number | undefined {
|
|
204
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function asIsoEpochSeconds(value: unknown): number | undefined {
|
|
209
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
210
|
+
const parsed = Date.parse(value);
|
|
211
|
+
if (Number.isNaN(parsed)) return undefined;
|
|
212
|
+
return Math.floor(parsed / 1000);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function asMillisecondEpochSeconds(value: unknown): number | undefined {
|
|
216
|
+
const raw =
|
|
217
|
+
typeof value === "number" && Number.isFinite(value)
|
|
218
|
+
? value
|
|
219
|
+
: typeof value === "string" && /^\d+$/u.test(value.trim())
|
|
220
|
+
? Number(value.trim())
|
|
221
|
+
: undefined;
|
|
222
|
+
if (raw === undefined || raw < 0) return undefined;
|
|
223
|
+
return Math.floor(raw >= MILLISECOND_EPOCH_FLOOR ? raw / 1000 : raw);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function capitalize(value: string): string {
|
|
227
|
+
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
228
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// separating that security boundary would duplicate request and redaction policy across providers.
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
5
|
+
import { abortError, errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
|
|
6
6
|
import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
7
7
|
import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
8
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
9
|
+
import { commandCodeOrgId, normalizeCommandCodeUsagePayload } from "./providers/command-code.js";
|
|
9
10
|
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
10
11
|
import { createFireworksAdapter } from "./providers/fireworks.js";
|
|
11
12
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
@@ -26,6 +27,11 @@ import { zaiResponseError } from "./providers/zai-errors.js";
|
|
|
26
27
|
import type {
|
|
27
28
|
BasetenBillingUsagePayload,
|
|
28
29
|
CodexBackendPayload,
|
|
30
|
+
CommandCodeAccountPayload,
|
|
31
|
+
CommandCodeCreditsPayload,
|
|
32
|
+
CommandCodeSubscriptionPayload,
|
|
33
|
+
CommandCodeUsageBundle,
|
|
34
|
+
CommandCodeUsageSummaryPayload,
|
|
29
35
|
DeepSeekBalancePayload,
|
|
30
36
|
GitHubCopilotUsagePayload,
|
|
31
37
|
KimiCodingUsagePayload,
|
|
@@ -51,6 +57,11 @@ import { resolveUsageTarget } from "./usage-targets.js";
|
|
|
51
57
|
const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
|
|
52
58
|
const BASETEN_USAGE_WINDOW_DAYS = 30;
|
|
53
59
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
60
|
+
const COMMAND_CODE_API_ORIGIN = "https://api.commandcode.ai";
|
|
61
|
+
const COMMAND_CODE_ACCOUNT_URL = `${COMMAND_CODE_API_ORIGIN}/alpha/whoami`;
|
|
62
|
+
const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_API_ORIGIN}/alpha/billing/credits`;
|
|
63
|
+
const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_API_ORIGIN}/alpha/billing/subscriptions`;
|
|
64
|
+
const COMMAND_CODE_USAGE_SUMMARY_URL = `${COMMAND_CODE_API_ORIGIN}/alpha/usage/summary`;
|
|
54
65
|
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
55
66
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
56
67
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
@@ -181,6 +192,63 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
181
192
|
return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
|
|
182
193
|
},
|
|
183
194
|
},
|
|
195
|
+
{
|
|
196
|
+
id: "command-code",
|
|
197
|
+
displayName: "Command Code",
|
|
198
|
+
semantics: { kind: "consumer-subscription", label: "Command Code plan credits and rolling limits" },
|
|
199
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
200
|
+
if (!guard) throw new Error("Command Code usage requires request-boundary revalidation.");
|
|
201
|
+
const startedAt = Date.now();
|
|
202
|
+
await guard();
|
|
203
|
+
const account = (await fetchProviderJson(
|
|
204
|
+
COMMAND_CODE_ACCOUNT_URL,
|
|
205
|
+
auth,
|
|
206
|
+
signal,
|
|
207
|
+
remainingTimeout(timeoutMs, startedAt, "fetching the Command Code account"),
|
|
208
|
+
"Command Code account endpoint",
|
|
209
|
+
{ redirect: "error" },
|
|
210
|
+
)) as CommandCodeAccountPayload;
|
|
211
|
+
await guard();
|
|
212
|
+
const orgId = commandCodeOrgId(account);
|
|
213
|
+
const [credits, subscription] = await Promise.all([
|
|
214
|
+
fetchCommandCodeOptional(
|
|
215
|
+
commandCodeUrl(COMMAND_CODE_CREDITS_URL, { orgId }),
|
|
216
|
+
auth,
|
|
217
|
+
signal,
|
|
218
|
+
timeoutMs - (Date.now() - startedAt),
|
|
219
|
+
"Command Code credits endpoint",
|
|
220
|
+
),
|
|
221
|
+
fetchCommandCodeOptional(
|
|
222
|
+
commandCodeUrl(COMMAND_CODE_SUBSCRIPTIONS_URL, { orgId }),
|
|
223
|
+
auth,
|
|
224
|
+
signal,
|
|
225
|
+
timeoutMs - (Date.now() - startedAt),
|
|
226
|
+
"Command Code plan endpoint",
|
|
227
|
+
),
|
|
228
|
+
]);
|
|
229
|
+
await guard();
|
|
230
|
+
const usage = await fetchCommandCodeOptional(
|
|
231
|
+
commandCodeUrl(COMMAND_CODE_USAGE_SUMMARY_URL, {
|
|
232
|
+
orgId,
|
|
233
|
+
since: commandCodeSubscriptionStart(subscription),
|
|
234
|
+
}),
|
|
235
|
+
auth,
|
|
236
|
+
signal,
|
|
237
|
+
timeoutMs - (Date.now() - startedAt),
|
|
238
|
+
"Command Code usage summary endpoint",
|
|
239
|
+
);
|
|
240
|
+
await guard();
|
|
241
|
+
if (signal.aborted) throw abortError();
|
|
242
|
+
// The account response is mandatory; the credits, plan, and period payloads degrade to notes.
|
|
243
|
+
const bundle: CommandCodeUsageBundle = {
|
|
244
|
+
account,
|
|
245
|
+
...(credits ? { credits: credits as CommandCodeCreditsPayload } : {}),
|
|
246
|
+
...(subscription ? { subscription: subscription as CommandCodeSubscriptionPayload } : {}),
|
|
247
|
+
...(usage ? { usage: usage as CommandCodeUsageSummaryPayload } : {}),
|
|
248
|
+
};
|
|
249
|
+
return normalizeCommandCodeUsagePayload(bundle, Date.now());
|
|
250
|
+
},
|
|
251
|
+
},
|
|
184
252
|
createFireworksAdapter(fetchProviderJson),
|
|
185
253
|
{
|
|
186
254
|
id: "opencode-go",
|
|
@@ -923,6 +991,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
923
991
|
}
|
|
924
992
|
if (providerId === "openai") return url.origin === "https://api.openai.com";
|
|
925
993
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
994
|
+
if (providerId === "command-code") return url.origin === COMMAND_CODE_API_ORIGIN;
|
|
926
995
|
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
927
996
|
if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
|
|
928
997
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
@@ -1096,3 +1165,40 @@ async function fetchZaiPlan(
|
|
|
1096
1165
|
function isAbortError(error: unknown): boolean {
|
|
1097
1166
|
return error instanceof Error && error.name === "AbortError";
|
|
1098
1167
|
}
|
|
1168
|
+
|
|
1169
|
+
async function fetchCommandCodeOptional(
|
|
1170
|
+
url: string,
|
|
1171
|
+
auth: ResolvedUsageAuth,
|
|
1172
|
+
signal: AbortSignal,
|
|
1173
|
+
timeoutMs: number,
|
|
1174
|
+
description: string,
|
|
1175
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
1176
|
+
if (signal.aborted) throw abortError();
|
|
1177
|
+
// Optional sections must not discard collected data when an earlier request used the budget.
|
|
1178
|
+
if (timeoutMs <= 0) return undefined;
|
|
1179
|
+
try {
|
|
1180
|
+
const payload = await fetchProviderJson(url, auth, signal, timeoutMs, description, { redirect: "error" });
|
|
1181
|
+
if (signal.aborted) throw abortError();
|
|
1182
|
+
return payload;
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (signal.aborted) throw abortError();
|
|
1185
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
|
|
1186
|
+
return undefined;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function commandCodeUrl(base: string, params: Record<string, string | undefined>): string {
|
|
1191
|
+
const query = new URLSearchParams();
|
|
1192
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1193
|
+
if (value) query.set(key, value);
|
|
1194
|
+
}
|
|
1195
|
+
const search = query.toString();
|
|
1196
|
+
return search ? `${base}?${search}` : base;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function commandCodeSubscriptionStart(subscription: Record<string, unknown> | undefined): string | undefined {
|
|
1200
|
+
const data = subscription?.data;
|
|
1201
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
|
|
1202
|
+
const value = (data as Record<string, unknown>).currentPeriodStart;
|
|
1203
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1204
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -238,3 +238,32 @@ export type CodexBackendPayload = {
|
|
|
238
238
|
credits?: unknown;
|
|
239
239
|
rate_limit_reset_credits?: unknown;
|
|
240
240
|
};
|
|
241
|
+
|
|
242
|
+
export type CommandCodeAccountPayload = {
|
|
243
|
+
user?: unknown;
|
|
244
|
+
org?: unknown;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export type CommandCodeCreditsPayload = {
|
|
248
|
+
credits?: unknown;
|
|
249
|
+
windowLimits?: unknown;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type CommandCodeSubscriptionPayload = {
|
|
253
|
+
data?: unknown;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export type CommandCodeUsageSummaryPayload = {
|
|
257
|
+
totalCost?: unknown;
|
|
258
|
+
totalCount?: unknown;
|
|
259
|
+
totalTokens?: unknown;
|
|
260
|
+
totalTokensIn?: unknown;
|
|
261
|
+
totalTokensOut?: unknown;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export interface CommandCodeUsageBundle {
|
|
265
|
+
account: CommandCodeAccountPayload;
|
|
266
|
+
credits?: CommandCodeCreditsPayload;
|
|
267
|
+
subscription?: CommandCodeSubscriptionPayload;
|
|
268
|
+
usage?: CommandCodeUsageSummaryPayload;
|
|
269
|
+
}
|
package/src/usage.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { createUsageTargetSelectOptions, listUsageTargets, resolveUsageTarget }
|
|
|
50
50
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
51
51
|
const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
|
|
52
52
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
53
|
+
const COMMAND_CODE_REVALIDATION_RESERVE_MS = 1_000;
|
|
53
54
|
const ALL_PROVIDER_CONCURRENCY = 2;
|
|
54
55
|
const FAILURE_BACKOFF_MS = 30_000;
|
|
55
56
|
const MAX_ACCOUNT_STATES = 32;
|
|
@@ -177,6 +178,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
177
178
|
const settings = settingsRuntime.get().settings;
|
|
178
179
|
const showCodexResetCountdown =
|
|
179
180
|
outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
181
|
+
const isCompanionReport = outcome.state.report.source === "openai-chatgpt-companion";
|
|
180
182
|
const now = Date.now();
|
|
181
183
|
const rawValue = formatUsageStatusline(
|
|
182
184
|
outcome.state.report,
|
|
@@ -190,9 +192,13 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
190
192
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
191
193
|
if (
|
|
192
194
|
sessionActive &&
|
|
193
|
-
(showCodexResetCountdown ||
|
|
195
|
+
(showCodexResetCountdown || isCompanionReport) &&
|
|
194
196
|
outcome.state.report.buckets.some(
|
|
195
|
-
(bucket) =>
|
|
197
|
+
(bucket) =>
|
|
198
|
+
(!isCompanionReport || bucket.groupId === "chatgpt-plan") &&
|
|
199
|
+
bucket.resetsAt !== undefined &&
|
|
200
|
+
Number.isFinite(bucket.resetsAt) &&
|
|
201
|
+
bucket.resetsAt * 1_000 > now,
|
|
196
202
|
)
|
|
197
203
|
) {
|
|
198
204
|
const generation = statusGeneration;
|
|
@@ -277,6 +283,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
277
283
|
adapter.targets !== undefined ||
|
|
278
284
|
[
|
|
279
285
|
"baseten",
|
|
286
|
+
"command-code",
|
|
280
287
|
"deepseek",
|
|
281
288
|
"minimax",
|
|
282
289
|
"minimax-cn",
|
|
@@ -414,11 +421,17 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
414
421
|
querySequence += 1;
|
|
415
422
|
queryId = querySequence;
|
|
416
423
|
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
424
|
+
// Command Code can retain partial data after an optional transport timeout. Keep time
|
|
425
|
+
// inside the overall deadline for its remaining auth guards and publication guard.
|
|
426
|
+
const queryTimeoutMs = Math.max(
|
|
427
|
+
1,
|
|
428
|
+
deadlineAt - Date.now() - (adapter.id === "command-code" ? COMMAND_CODE_REVALIDATION_RESERVE_MS : 0),
|
|
429
|
+
);
|
|
417
430
|
const report = await queryProviderUsage(
|
|
418
431
|
adapter,
|
|
419
432
|
auth,
|
|
420
433
|
signal,
|
|
421
|
-
|
|
434
|
+
queryTimeoutMs,
|
|
422
435
|
requiresRequestBoundaryGuard ? guard : undefined,
|
|
423
436
|
target.targetId,
|
|
424
437
|
);
|