@narumitw/pi-usage 0.53.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 +171 -45
- package/dist/index.ts +1082 -67
- package/dist/index.ts.map +4 -4
- package/package.json +16 -11
- package/src/format.ts +184 -7
- package/src/index.ts +9 -0
- package/src/providers/deepseek.ts +85 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/query.ts +250 -9
- package/src/settings.ts +12 -0
- package/src/types.ts +28 -2
- package/src/usage-helpers.ts +2 -2
- package/src/usage-settings-ui.ts +127 -0
- package/src/usage.ts +156 -14
package/src/query.ts
CHANGED
|
@@ -6,31 +6,49 @@ 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";
|
|
11
|
+
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
10
12
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
11
13
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
14
|
+
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
12
15
|
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
13
16
|
import type {
|
|
14
17
|
CodexBackendPayload,
|
|
18
|
+
DeepSeekBalancePayload,
|
|
15
19
|
GitHubCopilotUsagePayload,
|
|
20
|
+
KimiCodingUsagePayload,
|
|
16
21
|
OpenCodeZenPayload,
|
|
17
22
|
OpenRouterKeyPayload,
|
|
18
23
|
PiModel,
|
|
19
24
|
ResolvedUsageAuth,
|
|
20
25
|
UsageProviderAdapter,
|
|
21
26
|
UsageReport,
|
|
27
|
+
XaiBillingPayload,
|
|
28
|
+
XaiUserPayload,
|
|
22
29
|
ZaiQuotaPayload,
|
|
23
30
|
} from "./types.js";
|
|
24
31
|
|
|
25
32
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
33
|
+
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
26
34
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
27
35
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
28
36
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
37
|
+
const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
|
38
|
+
const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
39
|
+
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
40
|
+
const XAI_CLIENT_HEADERS = Object.freeze({
|
|
41
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
42
|
+
"x-grok-client-version": "1.0.10",
|
|
43
|
+
"x-grok-client-mode": "interactive",
|
|
44
|
+
});
|
|
29
45
|
const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
30
46
|
const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
31
47
|
|
|
32
48
|
export const AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
33
49
|
|
|
50
|
+
export type UsageRequestGuard = () => Promise<void>;
|
|
51
|
+
|
|
34
52
|
export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
35
53
|
{
|
|
36
54
|
id: "openai-codex",
|
|
@@ -50,6 +68,27 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
50
68
|
return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
|
|
51
69
|
},
|
|
52
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
|
+
},
|
|
53
92
|
{
|
|
54
93
|
id: "github-copilot",
|
|
55
94
|
displayName: "GitHub Copilot",
|
|
@@ -98,11 +137,26 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
98
137
|
return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
|
|
99
138
|
},
|
|
100
139
|
},
|
|
140
|
+
{
|
|
141
|
+
id: "kimi-coding",
|
|
142
|
+
displayName: "Kimi For Coding",
|
|
143
|
+
semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
|
|
144
|
+
async query(auth, signal, timeoutMs) {
|
|
145
|
+
const payload = await fetchProviderJson(
|
|
146
|
+
KIMI_CODING_USAGE_URL,
|
|
147
|
+
auth,
|
|
148
|
+
signal,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
"Kimi Coding usage endpoint",
|
|
151
|
+
{ redirect: "error" },
|
|
152
|
+
);
|
|
153
|
+
return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
|
|
154
|
+
},
|
|
155
|
+
},
|
|
101
156
|
{
|
|
102
157
|
id: "zai",
|
|
103
158
|
displayName: "Z.AI",
|
|
104
159
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
105
|
-
publishesStatusline: false,
|
|
106
160
|
async query(auth, signal, timeoutMs) {
|
|
107
161
|
const payload = await fetchProviderJson(
|
|
108
162
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -118,7 +172,6 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
118
172
|
id: "zai-coding-cn",
|
|
119
173
|
displayName: "Z.AI Coding CN",
|
|
120
174
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
121
|
-
publishesStatusline: false,
|
|
122
175
|
async query(auth, signal, timeoutMs) {
|
|
123
176
|
const payload = await fetchProviderJson(
|
|
124
177
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -137,10 +190,69 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
137
190
|
},
|
|
138
191
|
];
|
|
139
192
|
|
|
193
|
+
// Reviewed contract pins:
|
|
194
|
+
// - Pi xAI provider at https://api.x.ai and OAuth scope
|
|
195
|
+
// "openid profile email offline_access grok-cli:access api:access" at
|
|
196
|
+
// e86823096c5bad39e1ca282ec24bc5eb9bec745b, unchanged at
|
|
197
|
+
// ccfe79ed238674f760c986e3a61493aab794000a.
|
|
198
|
+
// - Grok Build identity, credits routes/structs, required token-auth and version headers, and
|
|
199
|
+
// client-mode telemetry at 9684fa3cdbf2995e30ea8b9b637f1db008f144fc (client version 1.0.10).
|
|
200
|
+
// - xAI Management API's separate team billing boundary at
|
|
201
|
+
// 723dd2aa22d17be35617463837dc47cda008d90e.
|
|
202
|
+
// x-userid remains attached only to billing to bind the proxy-canonical identity as Grok Build does.
|
|
203
|
+
export const XAI_ADAPTER: UsageProviderAdapter = {
|
|
204
|
+
id: "xai",
|
|
205
|
+
displayName: "xAI",
|
|
206
|
+
semantics: {
|
|
207
|
+
kind: "consumer-subscription",
|
|
208
|
+
label: "xAI consumer subscription usage",
|
|
209
|
+
},
|
|
210
|
+
publishesStatusline: false,
|
|
211
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
212
|
+
if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
|
|
213
|
+
const startedAt = Date.now();
|
|
214
|
+
const clientAuth = {
|
|
215
|
+
...auth,
|
|
216
|
+
headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
|
|
217
|
+
};
|
|
218
|
+
await guard();
|
|
219
|
+
const userPayload = (await fetchProviderJson(
|
|
220
|
+
XAI_USER_URL,
|
|
221
|
+
clientAuth,
|
|
222
|
+
signal,
|
|
223
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
224
|
+
"xAI consumer identity endpoint",
|
|
225
|
+
{ redirect: "error", userAgent: false },
|
|
226
|
+
)) as XaiUserPayload;
|
|
227
|
+
await guard();
|
|
228
|
+
const userId = validatedXaiUserId(userPayload.userId);
|
|
229
|
+
const billingAuth = {
|
|
230
|
+
...clientAuth,
|
|
231
|
+
headers: { ...clientAuth.headers, "x-userid": userId },
|
|
232
|
+
secrets: [...clientAuth.secrets, userId],
|
|
233
|
+
};
|
|
234
|
+
await guard();
|
|
235
|
+
const billingPayload = (await fetchProviderJson(
|
|
236
|
+
XAI_BILLING_URL,
|
|
237
|
+
billingAuth,
|
|
238
|
+
signal,
|
|
239
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
240
|
+
"xAI consumer billing endpoint",
|
|
241
|
+
{ redirect: "error", userAgent: false },
|
|
242
|
+
)) as XaiBillingPayload;
|
|
243
|
+
await guard();
|
|
244
|
+
return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export function usageAdapters(): readonly UsageProviderAdapter[] {
|
|
249
|
+
return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
|
|
250
|
+
}
|
|
251
|
+
|
|
140
252
|
export function adapterForProvider(
|
|
141
253
|
providerId: string | undefined,
|
|
142
254
|
): UsageProviderAdapter | undefined {
|
|
143
|
-
return
|
|
255
|
+
return usageAdapters().find((adapter) => adapter.id === providerId);
|
|
144
256
|
}
|
|
145
257
|
|
|
146
258
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
@@ -170,11 +282,14 @@ export async function resolveUsageAuth(
|
|
|
170
282
|
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
171
283
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
172
284
|
let modelAuth: RequestAuth | undefined;
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
289
|
if (!result.ok) throw new Error(redactUsageError(result.error));
|
|
176
|
-
|
|
177
|
-
}
|
|
290
|
+
return authorizationFrom(result) ? result : undefined;
|
|
291
|
+
};
|
|
292
|
+
if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
|
|
178
293
|
if (typeof registry.getProviderAuth !== "function") {
|
|
179
294
|
throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
|
|
180
295
|
}
|
|
@@ -187,6 +302,9 @@ export async function resolveUsageAuth(
|
|
|
187
302
|
`${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
|
|
188
303
|
);
|
|
189
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();
|
|
190
308
|
const auth = modelAuth ?? providerResult?.auth;
|
|
191
309
|
if (!auth) return undefined;
|
|
192
310
|
if (adapter.id === "github-copilot") {
|
|
@@ -204,6 +322,33 @@ export async function resolveUsageAuth(
|
|
|
204
322
|
offered.offeredCount === 0,
|
|
205
323
|
);
|
|
206
324
|
}
|
|
325
|
+
if (adapter.id === "xai") {
|
|
326
|
+
const offered = candidateReader
|
|
327
|
+
? candidateReader(ctx, adapter.id)
|
|
328
|
+
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
329
|
+
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
330
|
+
return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
|
|
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
|
+
}
|
|
207
352
|
const authorization = authorizationFrom(auth);
|
|
208
353
|
if (!authorization) return undefined;
|
|
209
354
|
const headers = { Authorization: authorization };
|
|
@@ -224,9 +369,10 @@ export async function queryProviderUsage(
|
|
|
224
369
|
auth: ResolvedUsageAuth,
|
|
225
370
|
signal: AbortSignal,
|
|
226
371
|
timeoutMs: number,
|
|
372
|
+
guard?: UsageRequestGuard,
|
|
227
373
|
): Promise<UsageReport> {
|
|
228
374
|
try {
|
|
229
|
-
return await adapter.query(auth, signal, timeoutMs);
|
|
375
|
+
return await adapter.query(auth, signal, timeoutMs, guard);
|
|
230
376
|
} catch (error) {
|
|
231
377
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
232
378
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -266,6 +412,8 @@ export async function fetchProviderJson(
|
|
|
266
412
|
request: {
|
|
267
413
|
method?: "GET" | "POST";
|
|
268
414
|
body?: Record<string, unknown>;
|
|
415
|
+
redirect?: RequestRedirect;
|
|
416
|
+
userAgent?: boolean;
|
|
269
417
|
} = {},
|
|
270
418
|
): Promise<Record<string, unknown>> {
|
|
271
419
|
const controller = new AbortController();
|
|
@@ -279,7 +427,9 @@ export async function fetchProviderJson(
|
|
|
279
427
|
}, timeoutMs);
|
|
280
428
|
try {
|
|
281
429
|
const headers = { ...auth.headers };
|
|
282
|
-
if (!hasHeader(headers, "User-Agent"))
|
|
430
|
+
if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
|
|
431
|
+
headers["User-Agent"] = "pi-usage";
|
|
432
|
+
}
|
|
283
433
|
if (request.body && !hasHeader(headers, "Content-Type")) {
|
|
284
434
|
headers["Content-Type"] = "application/json";
|
|
285
435
|
}
|
|
@@ -287,8 +437,10 @@ export async function fetchProviderJson(
|
|
|
287
437
|
method: request.method ?? "GET",
|
|
288
438
|
headers,
|
|
289
439
|
...(request.body ? { body: JSON.stringify(request.body) } : {}),
|
|
440
|
+
...(request.redirect ? { redirect: request.redirect } : {}),
|
|
290
441
|
signal: controller.signal,
|
|
291
442
|
});
|
|
443
|
+
if (response.redirected) throw new Error(`${description} refused a redirected response.`);
|
|
292
444
|
if (controller.signal.aborted)
|
|
293
445
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
294
446
|
const text = await readBoundedResponse(
|
|
@@ -296,6 +448,7 @@ export async function fetchProviderJson(
|
|
|
296
448
|
response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
|
|
297
449
|
!response.ok,
|
|
298
450
|
description,
|
|
451
|
+
controller.signal,
|
|
299
452
|
);
|
|
300
453
|
if (controller.signal.aborted)
|
|
301
454
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
@@ -332,12 +485,16 @@ async function readBoundedResponse(
|
|
|
332
485
|
maxBytes: number,
|
|
333
486
|
truncateOverflow: boolean,
|
|
334
487
|
description: string,
|
|
488
|
+
signal: AbortSignal,
|
|
335
489
|
): Promise<string> {
|
|
336
490
|
if (!response.body) return "";
|
|
337
491
|
const reader = response.body.getReader();
|
|
338
492
|
const chunks: Uint8Array[] = [];
|
|
339
493
|
let total = 0;
|
|
340
494
|
let truncated = false;
|
|
495
|
+
const abort = () => void reader.cancel().catch(() => undefined);
|
|
496
|
+
if (signal.aborted) abort();
|
|
497
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
341
498
|
try {
|
|
342
499
|
while (true) {
|
|
343
500
|
const { done, value } = await reader.read();
|
|
@@ -354,6 +511,7 @@ async function readBoundedResponse(
|
|
|
354
511
|
total += value.byteLength;
|
|
355
512
|
}
|
|
356
513
|
} finally {
|
|
514
|
+
signal.removeEventListener("abort", abort);
|
|
357
515
|
reader.releaseLock();
|
|
358
516
|
}
|
|
359
517
|
if (truncated && !truncateOverflow) {
|
|
@@ -388,6 +546,73 @@ type UsageAuthRegistry = {
|
|
|
388
546
|
>;
|
|
389
547
|
};
|
|
390
548
|
|
|
549
|
+
function resolveXaiUsageAuth(
|
|
550
|
+
auth: RequestAuth,
|
|
551
|
+
model: PiModel,
|
|
552
|
+
salt: Uint8Array,
|
|
553
|
+
candidates: readonly unknown[],
|
|
554
|
+
): ResolvedUsageAuth {
|
|
555
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
556
|
+
if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
|
|
557
|
+
let sawOAuth = false;
|
|
558
|
+
let sawMatchingAccess = false;
|
|
559
|
+
let sawIncompleteMatch = false;
|
|
560
|
+
const matches: Array<{ access: string; refresh: string }> = [];
|
|
561
|
+
for (const candidate of candidates) {
|
|
562
|
+
try {
|
|
563
|
+
const credential = asObject(candidate);
|
|
564
|
+
if (credential?.type !== "oauth") continue;
|
|
565
|
+
sawOAuth = true;
|
|
566
|
+
if (credential.access !== resolvedAccess) continue;
|
|
567
|
+
sawMatchingAccess = true;
|
|
568
|
+
if (
|
|
569
|
+
typeof credential.access !== "string" ||
|
|
570
|
+
!credential.access ||
|
|
571
|
+
typeof credential.refresh !== "string" ||
|
|
572
|
+
!credential.refresh ||
|
|
573
|
+
typeof credential.expires !== "number" ||
|
|
574
|
+
!Number.isFinite(credential.expires)
|
|
575
|
+
) {
|
|
576
|
+
sawIncompleteMatch = true;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
matches.push({ access: credential.access, refresh: credential.refresh });
|
|
580
|
+
} catch {
|
|
581
|
+
// Malformed candidates never authorize a consumer-proxy request.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
585
|
+
if (matches.length > 1) {
|
|
586
|
+
throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
|
|
587
|
+
}
|
|
588
|
+
const match = matches[0];
|
|
589
|
+
if (!match) {
|
|
590
|
+
if (!sawOAuth) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
"xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
596
|
+
throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
|
|
597
|
+
}
|
|
598
|
+
const authorization = `Bearer ${match.access}`;
|
|
599
|
+
const headers = { Authorization: authorization };
|
|
600
|
+
return {
|
|
601
|
+
apiKey: match.access,
|
|
602
|
+
headers,
|
|
603
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
604
|
+
secrets: [
|
|
605
|
+
match.access,
|
|
606
|
+
match.refresh,
|
|
607
|
+
resolvedAccess,
|
|
608
|
+
auth.apiKey,
|
|
609
|
+
headerValue(auth.headers, "Authorization"),
|
|
610
|
+
authorization,
|
|
611
|
+
].filter((value): value is string => Boolean(value)),
|
|
612
|
+
model,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
391
616
|
function resolveGitHubCopilotUsageAuth(
|
|
392
617
|
auth: RequestAuth,
|
|
393
618
|
model: PiModel,
|
|
@@ -507,8 +732,11 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
507
732
|
try {
|
|
508
733
|
const url = new URL(value);
|
|
509
734
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
735
|
+
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
510
736
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
511
737
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
738
|
+
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
739
|
+
if (providerId === "xai") return url.origin === "https://api.x.ai";
|
|
512
740
|
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
513
741
|
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
514
742
|
if (providerId === "github-copilot") {
|
|
@@ -536,6 +764,19 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
536
764
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
537
765
|
}
|
|
538
766
|
|
|
767
|
+
function validatedXaiUserId(value: unknown): string {
|
|
768
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
|
|
769
|
+
throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
|
|
770
|
+
}
|
|
771
|
+
return value;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function remainingTimeout(timeoutMs: number, startedAt: number): number {
|
|
775
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
776
|
+
if (remaining <= 0) throw new Error("Timed out while fetching xAI consumer usage.");
|
|
777
|
+
return remaining;
|
|
778
|
+
}
|
|
779
|
+
|
|
539
780
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
540
781
|
const base = baseUrl?.trim();
|
|
541
782
|
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
package/src/settings.ts
CHANGED
|
@@ -9,10 +9,12 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
|
9
9
|
|
|
10
10
|
export interface UsageSettings {
|
|
11
11
|
codexFastMode: boolean;
|
|
12
|
+
codexStatusResetCountdown: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
15
16
|
codexFastMode: false,
|
|
17
|
+
codexStatusResetCountdown: true,
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
export interface UsageSettingsState {
|
|
@@ -52,11 +54,21 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
52
54
|
if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
|
|
53
55
|
return undefined;
|
|
54
56
|
}
|
|
57
|
+
if (
|
|
58
|
+
Object.hasOwn(value, "codexStatusResetCountdown") &&
|
|
59
|
+
typeof value.codexStatusResetCountdown !== "boolean"
|
|
60
|
+
) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
55
63
|
return {
|
|
56
64
|
codexFastMode:
|
|
57
65
|
typeof value.codexFastMode === "boolean"
|
|
58
66
|
? value.codexFastMode
|
|
59
67
|
: DEFAULT_USAGE_SETTINGS.codexFastMode,
|
|
68
|
+
codexStatusResetCountdown:
|
|
69
|
+
typeof value.codexStatusResetCountdown === "boolean"
|
|
70
|
+
? value.codexStatusResetCountdown
|
|
71
|
+
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
60
72
|
};
|
|
61
73
|
}
|
|
62
74
|
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
|
4
4
|
export type UsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
5
5
|
|
|
6
6
|
export type UsageSemanticsKind = "consumer-subscription" | "api-key" | "project";
|
|
7
|
-
export type UsageUnit = "percent" | "usd" | "count";
|
|
7
|
+
export type UsageUnit = "percent" | "usd" | "currency" | "count";
|
|
8
8
|
export type UsageDisplayState = "current" | "configured";
|
|
9
9
|
|
|
10
10
|
export interface UsageSemantics {
|
|
@@ -32,6 +32,7 @@ export interface UsageMetric {
|
|
|
32
32
|
label: string;
|
|
33
33
|
value: number | string;
|
|
34
34
|
unit?: UsageUnit;
|
|
35
|
+
currency?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface UsageReport {
|
|
@@ -59,7 +60,12 @@ export interface UsageProviderAdapter {
|
|
|
59
60
|
displayName: string;
|
|
60
61
|
semantics: UsageSemantics;
|
|
61
62
|
publishesStatusline?: boolean;
|
|
62
|
-
query(
|
|
63
|
+
query(
|
|
64
|
+
auth: ResolvedUsageAuth,
|
|
65
|
+
signal: AbortSignal,
|
|
66
|
+
timeoutMs: number,
|
|
67
|
+
guard?: () => Promise<void>,
|
|
68
|
+
): Promise<UsageReport>;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export type ProviderUsageState =
|
|
@@ -78,6 +84,11 @@ export type ProviderUsageState =
|
|
|
78
84
|
message: string;
|
|
79
85
|
};
|
|
80
86
|
|
|
87
|
+
export type DeepSeekBalancePayload = {
|
|
88
|
+
is_available?: unknown;
|
|
89
|
+
balance_infos?: unknown;
|
|
90
|
+
};
|
|
91
|
+
|
|
81
92
|
export type GitHubCopilotUsagePayload = {
|
|
82
93
|
login?: unknown;
|
|
83
94
|
copilot_plan?: unknown;
|
|
@@ -102,6 +113,21 @@ export type ZaiQuotaPayload = {
|
|
|
102
113
|
data?: unknown;
|
|
103
114
|
};
|
|
104
115
|
|
|
116
|
+
export type KimiCodingUsagePayload = {
|
|
117
|
+
usage?: unknown;
|
|
118
|
+
limits?: unknown;
|
|
119
|
+
boosterWallet?: unknown;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type XaiUserPayload = {
|
|
123
|
+
userId?: unknown;
|
|
124
|
+
subscriptionTier?: unknown;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type XaiBillingPayload = {
|
|
128
|
+
config?: unknown;
|
|
129
|
+
};
|
|
130
|
+
|
|
105
131
|
export type CodexBackendPayload = {
|
|
106
132
|
plan_type?: unknown;
|
|
107
133
|
rate_limit?: unknown;
|
package/src/usage-helpers.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { sanitizeDisplayText } from "./core.js";
|
|
3
|
-
import { providerIsConfigured,
|
|
3
|
+
import { providerIsConfigured, usageAdapters } from "./query.js";
|
|
4
4
|
import type { PiModel, UsageProviderAdapter } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export function configuredAdapters(ctx: ExtensionContext): UsageProviderAdapter[] {
|
|
7
|
-
return
|
|
7
|
+
return usageAdapters().filter(
|
|
8
8
|
(adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id),
|
|
9
9
|
);
|
|
10
10
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionCommandContext,
|
|
3
|
+
getSettingsListTheme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
Container,
|
|
7
|
+
Key,
|
|
8
|
+
matchesKey,
|
|
9
|
+
type SettingItem,
|
|
10
|
+
SettingsList,
|
|
11
|
+
Text,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
13
|
+
import { errorMessage } from "./core.js";
|
|
14
|
+
import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
|
|
15
|
+
|
|
16
|
+
const OFF = "Off";
|
|
17
|
+
const ON = "On";
|
|
18
|
+
|
|
19
|
+
type UsageSettingId = keyof UsageSettings;
|
|
20
|
+
|
|
21
|
+
export async function showUsageSettings(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
settingsRuntime: UsageSettingsRuntime,
|
|
24
|
+
parentSignal: AbortSignal,
|
|
25
|
+
isCurrent: () => boolean,
|
|
26
|
+
onApplied: (id: UsageSettingId) => void,
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
if (ctx.mode !== "tui") {
|
|
29
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (parentSignal.aborted || !isCurrent()) return false;
|
|
33
|
+
|
|
34
|
+
return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
|
|
35
|
+
const localController = new AbortController();
|
|
36
|
+
const signal = AbortSignal.any([parentSignal, localController.signal]);
|
|
37
|
+
let changed = false;
|
|
38
|
+
let closing = false;
|
|
39
|
+
let saveQueue = Promise.resolve();
|
|
40
|
+
const state = settingsRuntime.get();
|
|
41
|
+
const items: SettingItem[] = [
|
|
42
|
+
{
|
|
43
|
+
id: "codexFastMode",
|
|
44
|
+
label: "Codex Fast mode",
|
|
45
|
+
description: "Use faster Codex routing at increased plan allowance consumption.",
|
|
46
|
+
currentValue: state.settings.codexFastMode ? ON : OFF,
|
|
47
|
+
values: [OFF, ON],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
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
|
+
values: [OFF, ON],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const container = new Container();
|
|
58
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
59
|
+
|
|
60
|
+
let settingsList: SettingsList;
|
|
61
|
+
const cancel = () => {
|
|
62
|
+
if (closing) return;
|
|
63
|
+
closing = true;
|
|
64
|
+
localController.abort();
|
|
65
|
+
done(changed);
|
|
66
|
+
};
|
|
67
|
+
settingsList = new SettingsList(
|
|
68
|
+
items,
|
|
69
|
+
items.length + 2,
|
|
70
|
+
getSettingsListTheme(),
|
|
71
|
+
(id, value) => {
|
|
72
|
+
if (closing || signal.aborted || !isCurrent()) return;
|
|
73
|
+
const settingId = id as UsageSettingId;
|
|
74
|
+
const requested = value !== OFF;
|
|
75
|
+
saveQueue = saveQueue.then(async () => {
|
|
76
|
+
const previous = settingsRuntime.get().settings[settingId];
|
|
77
|
+
if (settingsRuntime.get().kind === "invalid") {
|
|
78
|
+
settingsList.updateValue(id, displayValue(previous));
|
|
79
|
+
if (!signal.aborted && isCurrent()) {
|
|
80
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
81
|
+
tui.requestRender();
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await settingsRuntime.update({ [settingId]: requested }, signal);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (signal.aborted || !isCurrent()) return;
|
|
89
|
+
settingsList.updateValue(id, displayValue(previous));
|
|
90
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
91
|
+
tui.requestRender();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (previous !== requested) {
|
|
95
|
+
changed = true;
|
|
96
|
+
onApplied(settingId);
|
|
97
|
+
}
|
|
98
|
+
if (signal.aborted || !isCurrent()) return;
|
|
99
|
+
settingsList.updateValue(id, displayValue(requested));
|
|
100
|
+
tui.requestRender();
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
cancel,
|
|
104
|
+
);
|
|
105
|
+
container.addChild(settingsList);
|
|
106
|
+
|
|
107
|
+
parentSignal.addEventListener("abort", cancel, { once: true });
|
|
108
|
+
return {
|
|
109
|
+
render: (width: number) => container.render(width),
|
|
110
|
+
invalidate: () => container.invalidate(),
|
|
111
|
+
handleInput(data: string) {
|
|
112
|
+
if (closing) return;
|
|
113
|
+
if (matchesKey(data, Key.ctrl("c"))) cancel();
|
|
114
|
+
else settingsList.handleInput(data);
|
|
115
|
+
tui.requestRender();
|
|
116
|
+
},
|
|
117
|
+
dispose() {
|
|
118
|
+
localController.abort();
|
|
119
|
+
parentSignal.removeEventListener("abort", cancel);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function displayValue(enabled: boolean): string {
|
|
126
|
+
return enabled ? ON : OFF;
|
|
127
|
+
}
|