@narumitw/pi-usage 0.54.0 → 0.57.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 +103 -64
- package/dist/index.ts +329 -110
- package/dist/index.ts.map +4 -4
- package/package.json +9 -10
- package/src/format.ts +94 -7
- package/src/index.ts +2 -0
- package/src/providers/deepseek.ts +85 -0
- package/src/query.ts +58 -10
- package/src/settings.ts +10 -5
- package/src/types.ts +5 -0
- package/src/usage-helpers.ts +2 -2
- package/src/usage-settings-ui.ts +10 -11
- package/src/usage.ts +121 -85
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
|
|
3
|
+
"version": "0.57.0",
|
|
4
|
+
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"pi",
|
|
12
12
|
"usage",
|
|
13
13
|
"quota",
|
|
14
|
+
"balance",
|
|
15
|
+
"deepseek",
|
|
14
16
|
"codex",
|
|
15
17
|
"kimi",
|
|
16
18
|
"kimi-coding",
|
|
@@ -34,9 +36,6 @@
|
|
|
34
36
|
"./dist/index.ts"
|
|
35
37
|
]
|
|
36
38
|
},
|
|
37
|
-
"piExtension": {
|
|
38
|
-
"lifecycle": "stable"
|
|
39
|
-
},
|
|
40
39
|
"scripts": {
|
|
41
40
|
"build": "node scripts/build-runtime.mjs",
|
|
42
41
|
"check": "npm run build && biome check --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md && npm run typecheck",
|
|
@@ -50,11 +49,11 @@
|
|
|
50
49
|
"@earendil-works/pi-tui": "*"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "2.5.
|
|
54
|
-
"@earendil-works/pi-ai": "0.84.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
56
|
-
"@earendil-works/pi-tui": "0.84.
|
|
57
|
-
"@types/node": "26.
|
|
52
|
+
"@biomejs/biome": "2.5.11",
|
|
53
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
55
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
56
|
+
"@types/node": "26.4.0",
|
|
58
57
|
"esbuild": "0.28.2",
|
|
59
58
|
"typescript": "7.0.2"
|
|
60
59
|
},
|
package/src/format.ts
CHANGED
|
@@ -11,11 +11,14 @@ const VALUE_COLUMN = 29;
|
|
|
11
11
|
|
|
12
12
|
export function formatUsageReport(report: UsageReport, displayState: UsageDisplayState): string {
|
|
13
13
|
const stateLabel = displayState === "current" ? "Current" : "Configured";
|
|
14
|
-
const
|
|
14
|
+
const title =
|
|
15
|
+
report.providerId === "deepseek" ? "DeepSeek API Balance" : `${report.providerName} Usage`;
|
|
16
|
+
const lines = [`${title} · ${stateLabel}`];
|
|
15
17
|
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
16
18
|
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
17
19
|
|
|
18
20
|
if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
21
|
+
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
19
22
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
20
23
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
21
24
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
@@ -31,8 +34,16 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
|
|
|
31
34
|
return lines.join("\n").trimEnd();
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export function formatUsageStatusline(
|
|
35
|
-
|
|
37
|
+
export function formatUsageStatusline(
|
|
38
|
+
report: UsageReport,
|
|
39
|
+
model?: UsageModel,
|
|
40
|
+
now = Date.now(),
|
|
41
|
+
showCodexResetCountdown = true,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
if (report.providerId === "openai-codex") {
|
|
44
|
+
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
45
|
+
}
|
|
46
|
+
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
36
47
|
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
37
48
|
if (report.providerId === "openrouter") {
|
|
38
49
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -42,6 +53,9 @@ export function formatUsageStatusline(report: UsageReport, model?: UsageModel):
|
|
|
42
53
|
}
|
|
43
54
|
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
44
55
|
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
56
|
+
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
57
|
+
return formatZaiStatusline(report);
|
|
58
|
+
}
|
|
45
59
|
return undefined;
|
|
46
60
|
}
|
|
47
61
|
|
|
@@ -84,6 +98,33 @@ function formatCodexReport(lines: string[], report: UsageReport): void {
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
function formatDeepSeekReport(lines: string[], report: UsageReport): void {
|
|
102
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
103
|
+
lines.push(
|
|
104
|
+
`${"API calls:".padEnd(VALUE_COLUMN)}${availability?.value === "available" ? "Available" : "Unavailable"}`,
|
|
105
|
+
);
|
|
106
|
+
for (const currency of ["CNY", "USD"]) {
|
|
107
|
+
const metrics = report.metrics.filter((metric) => metric.currency === currency);
|
|
108
|
+
if (metrics.length === 0) continue;
|
|
109
|
+
lines.push("", `${currency} balance:`);
|
|
110
|
+
for (const metric of metrics) {
|
|
111
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatDeepSeekStatusline(report: UsageReport): string {
|
|
117
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
118
|
+
if (availability?.value !== "available") return "deepseek API unavailable";
|
|
119
|
+
const totals = ["CNY", "USD"].flatMap((currency) => {
|
|
120
|
+
const metric = report.metrics.find(
|
|
121
|
+
(candidate) => candidate.id === `${currency.toLowerCase()}-total`,
|
|
122
|
+
);
|
|
123
|
+
return metric ? [`${currency} ${metric.value}`] : [];
|
|
124
|
+
});
|
|
125
|
+
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
126
|
+
}
|
|
127
|
+
|
|
87
128
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
88
129
|
const quota = findGitHubCopilotQuota(report);
|
|
89
130
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
@@ -213,6 +254,22 @@ function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
|
213
254
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
214
255
|
}
|
|
215
256
|
|
|
257
|
+
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
258
|
+
const selected = [
|
|
259
|
+
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
260
|
+
report.buckets.find((bucket) => bucket.id === "weekly"),
|
|
261
|
+
];
|
|
262
|
+
const parts = ["zai"];
|
|
263
|
+
for (const bucket of selected) {
|
|
264
|
+
if (!bucket?.limit || bucket.remaining === undefined) continue;
|
|
265
|
+
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
266
|
+
parts.push(
|
|
267
|
+
`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
216
273
|
function formatCurrencyMetric(metric: UsageReport["metrics"][number]): string {
|
|
217
274
|
if (typeof metric.value !== "number") return String(metric.value);
|
|
218
275
|
if (!metric.currency) return "unavailable";
|
|
@@ -289,7 +346,12 @@ function formatGenericReport(lines: string[], report: UsageReport): void {
|
|
|
289
346
|
}
|
|
290
347
|
}
|
|
291
348
|
|
|
292
|
-
function formatCodexStatusline(
|
|
349
|
+
function formatCodexStatusline(
|
|
350
|
+
report: UsageReport,
|
|
351
|
+
model?: UsageModel,
|
|
352
|
+
now = Date.now(),
|
|
353
|
+
showResetCountdown = true,
|
|
354
|
+
): string | undefined {
|
|
293
355
|
const group = selectCodexGroup(report, model);
|
|
294
356
|
if (!group) return formatCodexCreditsStatus(report);
|
|
295
357
|
const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
@@ -299,10 +361,15 @@ function formatCodexStatusline(report: UsageReport, model?: UsageModel): string
|
|
|
299
361
|
];
|
|
300
362
|
for (const bucket of buckets) {
|
|
301
363
|
if (bucket.remaining === undefined) continue;
|
|
364
|
+
const percent = `${clampPercent(bucket.remaining).toFixed(0)}%`;
|
|
302
365
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
366
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
367
|
+
if (!showResetCountdown) {
|
|
368
|
+
parts.push(`${percent} ${window}`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const reset = formatResetCountdown(bucket.resetsAt, now);
|
|
372
|
+
parts.push(`${percent} ${reset ? `↻ ${reset}` : window}`);
|
|
306
373
|
}
|
|
307
374
|
return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
|
|
308
375
|
}
|
|
@@ -405,6 +472,26 @@ function formatWindowLabel(
|
|
|
405
472
|
return `${minutes}m`;
|
|
406
473
|
}
|
|
407
474
|
|
|
475
|
+
function formatResetCountdown(resetsAt: number | undefined, now: number): string | undefined {
|
|
476
|
+
if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now))
|
|
477
|
+
return undefined;
|
|
478
|
+
const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
|
|
479
|
+
const days = Math.floor(totalMinutes / 1_440);
|
|
480
|
+
const hours = Math.floor((totalMinutes % 1_440) / 60);
|
|
481
|
+
const minutes = totalMinutes % 60;
|
|
482
|
+
if (days > 0) {
|
|
483
|
+
return [
|
|
484
|
+
`${String(days)}d`,
|
|
485
|
+
hours > 0 ? `${String(hours)}h` : minutes > 0 ? `${String(minutes)}m` : "",
|
|
486
|
+
]
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.join("");
|
|
489
|
+
}
|
|
490
|
+
if (hours > 0)
|
|
491
|
+
return [`${String(hours)}h`, minutes > 0 ? `${String(minutes)}m` : ""].filter(Boolean).join("");
|
|
492
|
+
return `${String(minutes)}m`;
|
|
493
|
+
}
|
|
494
|
+
|
|
408
495
|
function formatMetricValue(value: number | string, unit: UsageBucket["unit"] | undefined): string {
|
|
409
496
|
if (unit === "usd" && typeof value === "number") return formatUsd(value);
|
|
410
497
|
return String(value);
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
} from "./core.js";
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
35
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
36
|
+
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
36
37
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
37
38
|
export { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
38
39
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -62,6 +63,7 @@ export {
|
|
|
62
63
|
usageSettingsPath,
|
|
63
64
|
} from "./settings.js";
|
|
64
65
|
export type {
|
|
66
|
+
DeepSeekBalancePayload,
|
|
65
67
|
KimiCodingUsagePayload,
|
|
66
68
|
ProviderUsageState,
|
|
67
69
|
ResolvedUsageAuth,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DeepSeekBalancePayload, UsageMetric, UsageReport } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const CURRENCIES = ["CNY", "USD"] as const;
|
|
4
|
+
type DeepSeekCurrency = (typeof CURRENCIES)[number];
|
|
5
|
+
|
|
6
|
+
const BALANCE_FIELDS = [
|
|
7
|
+
["total", "Total balance", "total_balance"],
|
|
8
|
+
["granted", "Granted balance", "granted_balance"],
|
|
9
|
+
["topped-up", "Topped-up balance", "topped_up_balance"],
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export function normalizeDeepSeekBalancePayload(
|
|
13
|
+
payload: DeepSeekBalancePayload,
|
|
14
|
+
capturedAt: number,
|
|
15
|
+
): UsageReport {
|
|
16
|
+
if (typeof payload.is_available !== "boolean") {
|
|
17
|
+
throw new Error("DeepSeek API balance response availability was not a boolean.");
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
|
|
20
|
+
throw new Error("DeepSeek API balance response returned no balance information.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const balances = new Map<DeepSeekCurrency, Record<string, unknown>>();
|
|
24
|
+
for (const raw of payload.balance_infos) {
|
|
25
|
+
const balance = asObject(raw);
|
|
26
|
+
if (!balance) throw new Error("DeepSeek API balance row was not an object.");
|
|
27
|
+
const currency = deepSeekCurrency(balance.currency);
|
|
28
|
+
if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
|
|
29
|
+
if (balances.has(currency)) {
|
|
30
|
+
throw new Error(`DeepSeek API balance response repeated ${currency}.`);
|
|
31
|
+
}
|
|
32
|
+
for (const [, label, field] of BALANCE_FIELDS) {
|
|
33
|
+
if (!decimalAmount(balance[field])) {
|
|
34
|
+
throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
balances.set(currency, balance);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const metrics: UsageMetric[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "api-availability",
|
|
43
|
+
label: "API calls",
|
|
44
|
+
value: payload.is_available ? "available" : "unavailable",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
for (const currency of CURRENCIES) {
|
|
48
|
+
const balance = balances.get(currency);
|
|
49
|
+
if (!balance) continue;
|
|
50
|
+
for (const [id, label, field] of BALANCE_FIELDS) {
|
|
51
|
+
metrics.push({
|
|
52
|
+
id: `${currency.toLowerCase()}-${id}`,
|
|
53
|
+
label,
|
|
54
|
+
value: balance[field] as string,
|
|
55
|
+
unit: "currency",
|
|
56
|
+
currency,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
providerId: "deepseek",
|
|
63
|
+
providerName: "DeepSeek",
|
|
64
|
+
capturedAt,
|
|
65
|
+
source: "deepseek-balance",
|
|
66
|
+
semantics: { kind: "api-key", label: "DeepSeek API balance" },
|
|
67
|
+
buckets: [],
|
|
68
|
+
metrics,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
73
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
74
|
+
return value as Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deepSeekCurrency(value: unknown): DeepSeekCurrency | undefined {
|
|
78
|
+
return CURRENCIES.find((currency) => currency === value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function decimalAmount(value: unknown): value is string {
|
|
82
|
+
return (
|
|
83
|
+
typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value)
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type OAuthCredentialCandidateReader,
|
|
7
7
|
} from "./oauth-credential-source.js";
|
|
8
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
9
|
+
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
9
10
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
10
11
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
11
12
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -14,6 +15,7 @@ import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
|
14
15
|
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
15
16
|
import type {
|
|
16
17
|
CodexBackendPayload,
|
|
18
|
+
DeepSeekBalancePayload,
|
|
17
19
|
GitHubCopilotUsagePayload,
|
|
18
20
|
KimiCodingUsagePayload,
|
|
19
21
|
OpenCodeZenPayload,
|
|
@@ -28,6 +30,7 @@ import type {
|
|
|
28
30
|
} from "./types.js";
|
|
29
31
|
|
|
30
32
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
33
|
+
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
31
34
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
32
35
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
33
36
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
@@ -65,6 +68,27 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
65
68
|
return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
|
|
66
69
|
},
|
|
67
70
|
},
|
|
71
|
+
{
|
|
72
|
+
id: "deepseek",
|
|
73
|
+
displayName: "DeepSeek",
|
|
74
|
+
semantics: { kind: "api-key", label: "DeepSeek API balance" },
|
|
75
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
76
|
+
if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
await guard();
|
|
79
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
80
|
+
if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
|
|
81
|
+
const payload = await fetchProviderJson(
|
|
82
|
+
DEEPSEEK_BALANCE_URL,
|
|
83
|
+
auth,
|
|
84
|
+
signal,
|
|
85
|
+
remainingMs,
|
|
86
|
+
"DeepSeek API balance endpoint",
|
|
87
|
+
{ redirect: "error" },
|
|
88
|
+
);
|
|
89
|
+
return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
|
|
90
|
+
},
|
|
91
|
+
},
|
|
68
92
|
{
|
|
69
93
|
id: "github-copilot",
|
|
70
94
|
displayName: "GitHub Copilot",
|
|
@@ -133,7 +157,6 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
133
157
|
id: "zai",
|
|
134
158
|
displayName: "Z.AI",
|
|
135
159
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
136
|
-
publishesStatusline: false,
|
|
137
160
|
async query(auth, signal, timeoutMs) {
|
|
138
161
|
const payload = await fetchProviderJson(
|
|
139
162
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -149,7 +172,6 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
149
172
|
id: "zai-coding-cn",
|
|
150
173
|
displayName: "Z.AI Coding CN",
|
|
151
174
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
152
|
-
publishesStatusline: false,
|
|
153
175
|
async query(auth, signal, timeoutMs) {
|
|
154
176
|
const payload = await fetchProviderJson(
|
|
155
177
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -223,15 +245,14 @@ export const XAI_ADAPTER: UsageProviderAdapter = {
|
|
|
223
245
|
},
|
|
224
246
|
};
|
|
225
247
|
|
|
226
|
-
export function usageAdapters(
|
|
227
|
-
return
|
|
248
|
+
export function usageAdapters(): readonly UsageProviderAdapter[] {
|
|
249
|
+
return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
|
|
228
250
|
}
|
|
229
251
|
|
|
230
252
|
export function adapterForProvider(
|
|
231
253
|
providerId: string | undefined,
|
|
232
|
-
xaiUsage = true,
|
|
233
254
|
): UsageProviderAdapter | undefined {
|
|
234
|
-
return usageAdapters(
|
|
255
|
+
return usageAdapters().find((adapter) => adapter.id === providerId);
|
|
235
256
|
}
|
|
236
257
|
|
|
237
258
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
@@ -261,11 +282,14 @@ export async function resolveUsageAuth(
|
|
|
261
282
|
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
262
283
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
263
284
|
let modelAuth: RequestAuth | undefined;
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
|
|
286
|
+
const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
|
|
287
|
+
if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
|
|
288
|
+
const result = await registry.getApiKeyAndHeaders(currentModel);
|
|
266
289
|
if (!result.ok) throw new Error(redactUsageError(result.error));
|
|
267
|
-
|
|
268
|
-
}
|
|
290
|
+
return authorizationFrom(result) ? result : undefined;
|
|
291
|
+
};
|
|
292
|
+
if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
|
|
269
293
|
if (typeof registry.getProviderAuth !== "function") {
|
|
270
294
|
throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
|
|
271
295
|
}
|
|
@@ -278,6 +302,9 @@ export async function resolveUsageAuth(
|
|
|
278
302
|
`${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
|
|
279
303
|
);
|
|
280
304
|
}
|
|
305
|
+
// DeepSeek reads selected-model auth last so a rotation during provider-origin validation
|
|
306
|
+
// cannot leave the earlier credential queued for the balance request.
|
|
307
|
+
if (adapter.id === "deepseek") modelAuth = await resolveCurrentModelAuth();
|
|
281
308
|
const auth = modelAuth ?? providerResult?.auth;
|
|
282
309
|
if (!auth) return undefined;
|
|
283
310
|
if (adapter.id === "github-copilot") {
|
|
@@ -302,6 +329,26 @@ export async function resolveUsageAuth(
|
|
|
302
329
|
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
303
330
|
return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
|
|
304
331
|
}
|
|
332
|
+
if (adapter.id === "deepseek") {
|
|
333
|
+
const resolvedAuthorization = authorizationFrom(auth);
|
|
334
|
+
const access = bearerToken(resolvedAuthorization);
|
|
335
|
+
if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
|
|
336
|
+
const authorization = `Bearer ${access}`;
|
|
337
|
+
const headers = { Authorization: authorization };
|
|
338
|
+
return {
|
|
339
|
+
apiKey: access,
|
|
340
|
+
headers,
|
|
341
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
342
|
+
secrets: [
|
|
343
|
+
access,
|
|
344
|
+
auth.apiKey,
|
|
345
|
+
headerValue(auth.headers, "Authorization"),
|
|
346
|
+
resolvedAuthorization,
|
|
347
|
+
authorization,
|
|
348
|
+
].filter((value): value is string => Boolean(value)),
|
|
349
|
+
model,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
305
352
|
const authorization = authorizationFrom(auth);
|
|
306
353
|
if (!authorization) return undefined;
|
|
307
354
|
const headers = { Authorization: authorization };
|
|
@@ -685,6 +732,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
685
732
|
try {
|
|
686
733
|
const url = new URL(value);
|
|
687
734
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
735
|
+
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
688
736
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
689
737
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
690
738
|
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
package/src/settings.ts
CHANGED
|
@@ -9,12 +9,12 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
|
9
9
|
|
|
10
10
|
export interface UsageSettings {
|
|
11
11
|
codexFastMode: boolean;
|
|
12
|
-
|
|
12
|
+
codexStatusResetCountdown: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
16
16
|
codexFastMode: false,
|
|
17
|
-
|
|
17
|
+
codexStatusResetCountdown: true,
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
export interface UsageSettingsState {
|
|
@@ -54,7 +54,10 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
54
54
|
if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
Object.hasOwn(value, "codexStatusResetCountdown") &&
|
|
59
|
+
typeof value.codexStatusResetCountdown !== "boolean"
|
|
60
|
+
) {
|
|
58
61
|
return undefined;
|
|
59
62
|
}
|
|
60
63
|
return {
|
|
@@ -62,8 +65,10 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
62
65
|
typeof value.codexFastMode === "boolean"
|
|
63
66
|
? value.codexFastMode
|
|
64
67
|
: DEFAULT_USAGE_SETTINGS.codexFastMode,
|
|
65
|
-
|
|
66
|
-
typeof value.
|
|
68
|
+
codexStatusResetCountdown:
|
|
69
|
+
typeof value.codexStatusResetCountdown === "boolean"
|
|
70
|
+
? value.codexStatusResetCountdown
|
|
71
|
+
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
74
|
|
package/src/types.ts
CHANGED
|
@@ -84,6 +84,11 @@ export type ProviderUsageState =
|
|
|
84
84
|
message: string;
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
export type DeepSeekBalancePayload = {
|
|
88
|
+
is_available?: unknown;
|
|
89
|
+
balance_infos?: unknown;
|
|
90
|
+
};
|
|
91
|
+
|
|
87
92
|
export type GitHubCopilotUsagePayload = {
|
|
88
93
|
login?: unknown;
|
|
89
94
|
copilot_plan?: unknown;
|
package/src/usage-helpers.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { sanitizeDisplayText } from "./core.js";
|
|
|
3
3
|
import { providerIsConfigured, usageAdapters } from "./query.js";
|
|
4
4
|
import type { PiModel, UsageProviderAdapter } from "./types.js";
|
|
5
5
|
|
|
6
|
-
export function configuredAdapters(ctx: ExtensionContext
|
|
7
|
-
return usageAdapters(
|
|
6
|
+
export function configuredAdapters(ctx: ExtensionContext): UsageProviderAdapter[] {
|
|
7
|
+
return usageAdapters().filter(
|
|
8
8
|
(adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id),
|
|
9
9
|
);
|
|
10
10
|
}
|
package/src/usage-settings-ui.ts
CHANGED
|
@@ -23,7 +23,7 @@ export async function showUsageSettings(
|
|
|
23
23
|
settingsRuntime: UsageSettingsRuntime,
|
|
24
24
|
parentSignal: AbortSignal,
|
|
25
25
|
isCurrent: () => boolean,
|
|
26
|
-
onApplied: (id: UsageSettingId
|
|
26
|
+
onApplied: (id: UsageSettingId) => void,
|
|
27
27
|
): Promise<boolean> {
|
|
28
28
|
if (ctx.mode !== "tui") {
|
|
29
29
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
@@ -47,10 +47,10 @@ export async function showUsageSettings(
|
|
|
47
47
|
values: [OFF, ON],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
-
id: "
|
|
51
|
-
label: "
|
|
52
|
-
description: "
|
|
53
|
-
currentValue: state.
|
|
50
|
+
id: "codexStatusResetCountdown",
|
|
51
|
+
label: "Codex reset countdown",
|
|
52
|
+
description: "Show time remaining until each Codex usage limit resets.",
|
|
53
|
+
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
54
54
|
values: [OFF, ON],
|
|
55
55
|
},
|
|
56
56
|
];
|
|
@@ -75,8 +75,7 @@ export async function showUsageSettings(
|
|
|
75
75
|
saveQueue = saveQueue.then(async () => {
|
|
76
76
|
const previous = settingsRuntime.get().settings[settingId];
|
|
77
77
|
if (settingsRuntime.get().kind === "invalid") {
|
|
78
|
-
|
|
79
|
-
settingsList.updateValue(id, displayValue(settingId, effectivePrevious));
|
|
78
|
+
settingsList.updateValue(id, displayValue(previous));
|
|
80
79
|
if (!signal.aborted && isCurrent()) {
|
|
81
80
|
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
82
81
|
tui.requestRender();
|
|
@@ -87,17 +86,17 @@ export async function showUsageSettings(
|
|
|
87
86
|
await settingsRuntime.update({ [settingId]: requested }, signal);
|
|
88
87
|
} catch (error) {
|
|
89
88
|
if (signal.aborted || !isCurrent()) return;
|
|
90
|
-
settingsList.updateValue(id, displayValue(
|
|
89
|
+
settingsList.updateValue(id, displayValue(previous));
|
|
91
90
|
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
92
91
|
tui.requestRender();
|
|
93
92
|
return;
|
|
94
93
|
}
|
|
95
94
|
if (previous !== requested) {
|
|
96
95
|
changed = true;
|
|
97
|
-
onApplied(settingId
|
|
96
|
+
onApplied(settingId);
|
|
98
97
|
}
|
|
99
98
|
if (signal.aborted || !isCurrent()) return;
|
|
100
|
-
settingsList.updateValue(id, displayValue(
|
|
99
|
+
settingsList.updateValue(id, displayValue(requested));
|
|
101
100
|
tui.requestRender();
|
|
102
101
|
});
|
|
103
102
|
},
|
|
@@ -123,6 +122,6 @@ export async function showUsageSettings(
|
|
|
123
122
|
});
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function displayValue(
|
|
125
|
+
function displayValue(enabled: boolean): string {
|
|
127
126
|
return enabled ? ON : OFF;
|
|
128
127
|
}
|