@narumitw/pi-usage 0.53.0 → 0.54.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 +94 -7
- package/dist/index.ts +843 -47
- package/dist/index.ts.map +4 -4
- package/package.json +10 -4
- package/src/format.ts +90 -0
- package/src/index.ts +7 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/query.ts +196 -3
- package/src/settings.ts +7 -0
- package/src/types.ts +23 -2
- package/src/usage-helpers.ts +3 -3
- package/src/usage-settings-ui.ts +128 -0
- package/src/usage.ts +119 -13
package/src/query.ts
CHANGED
|
@@ -7,18 +7,23 @@ import {
|
|
|
7
7
|
} from "./oauth-credential-source.js";
|
|
8
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
9
9
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
10
|
+
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
10
11
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
11
12
|
import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
13
|
+
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
12
14
|
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
13
15
|
import type {
|
|
14
16
|
CodexBackendPayload,
|
|
15
17
|
GitHubCopilotUsagePayload,
|
|
18
|
+
KimiCodingUsagePayload,
|
|
16
19
|
OpenCodeZenPayload,
|
|
17
20
|
OpenRouterKeyPayload,
|
|
18
21
|
PiModel,
|
|
19
22
|
ResolvedUsageAuth,
|
|
20
23
|
UsageProviderAdapter,
|
|
21
24
|
UsageReport,
|
|
25
|
+
XaiBillingPayload,
|
|
26
|
+
XaiUserPayload,
|
|
22
27
|
ZaiQuotaPayload,
|
|
23
28
|
} from "./types.js";
|
|
24
29
|
|
|
@@ -26,11 +31,21 @@ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
|
26
31
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
27
32
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
28
33
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
34
|
+
const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
|
35
|
+
const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
|
|
36
|
+
const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
37
|
+
const XAI_CLIENT_HEADERS = Object.freeze({
|
|
38
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
39
|
+
"x-grok-client-version": "1.0.10",
|
|
40
|
+
"x-grok-client-mode": "interactive",
|
|
41
|
+
});
|
|
29
42
|
const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
|
|
30
43
|
const MAX_ERROR_BODY_BYTES = 4 * 1024;
|
|
31
44
|
|
|
32
45
|
export const AUTH_FINGERPRINT_SALT = randomBytes(32);
|
|
33
46
|
|
|
47
|
+
export type UsageRequestGuard = () => Promise<void>;
|
|
48
|
+
|
|
34
49
|
export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
35
50
|
{
|
|
36
51
|
id: "openai-codex",
|
|
@@ -98,6 +113,22 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
98
113
|
return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
|
|
99
114
|
},
|
|
100
115
|
},
|
|
116
|
+
{
|
|
117
|
+
id: "kimi-coding",
|
|
118
|
+
displayName: "Kimi For Coding",
|
|
119
|
+
semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
|
|
120
|
+
async query(auth, signal, timeoutMs) {
|
|
121
|
+
const payload = await fetchProviderJson(
|
|
122
|
+
KIMI_CODING_USAGE_URL,
|
|
123
|
+
auth,
|
|
124
|
+
signal,
|
|
125
|
+
timeoutMs,
|
|
126
|
+
"Kimi Coding usage endpoint",
|
|
127
|
+
{ redirect: "error" },
|
|
128
|
+
);
|
|
129
|
+
return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
|
|
130
|
+
},
|
|
131
|
+
},
|
|
101
132
|
{
|
|
102
133
|
id: "zai",
|
|
103
134
|
displayName: "Z.AI",
|
|
@@ -137,10 +168,70 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
137
168
|
},
|
|
138
169
|
];
|
|
139
170
|
|
|
171
|
+
// Reviewed contract pins:
|
|
172
|
+
// - Pi xAI provider at https://api.x.ai and OAuth scope
|
|
173
|
+
// "openid profile email offline_access grok-cli:access api:access" at
|
|
174
|
+
// e86823096c5bad39e1ca282ec24bc5eb9bec745b, unchanged at
|
|
175
|
+
// ccfe79ed238674f760c986e3a61493aab794000a.
|
|
176
|
+
// - Grok Build identity, credits routes/structs, required token-auth and version headers, and
|
|
177
|
+
// client-mode telemetry at 9684fa3cdbf2995e30ea8b9b637f1db008f144fc (client version 1.0.10).
|
|
178
|
+
// - xAI Management API's separate team billing boundary at
|
|
179
|
+
// 723dd2aa22d17be35617463837dc47cda008d90e.
|
|
180
|
+
// x-userid remains attached only to billing to bind the proxy-canonical identity as Grok Build does.
|
|
181
|
+
export const XAI_ADAPTER: UsageProviderAdapter = {
|
|
182
|
+
id: "xai",
|
|
183
|
+
displayName: "xAI",
|
|
184
|
+
semantics: {
|
|
185
|
+
kind: "consumer-subscription",
|
|
186
|
+
label: "xAI consumer subscription usage",
|
|
187
|
+
},
|
|
188
|
+
publishesStatusline: false,
|
|
189
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
190
|
+
if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
|
|
191
|
+
const startedAt = Date.now();
|
|
192
|
+
const clientAuth = {
|
|
193
|
+
...auth,
|
|
194
|
+
headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
|
|
195
|
+
};
|
|
196
|
+
await guard();
|
|
197
|
+
const userPayload = (await fetchProviderJson(
|
|
198
|
+
XAI_USER_URL,
|
|
199
|
+
clientAuth,
|
|
200
|
+
signal,
|
|
201
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
202
|
+
"xAI consumer identity endpoint",
|
|
203
|
+
{ redirect: "error", userAgent: false },
|
|
204
|
+
)) as XaiUserPayload;
|
|
205
|
+
await guard();
|
|
206
|
+
const userId = validatedXaiUserId(userPayload.userId);
|
|
207
|
+
const billingAuth = {
|
|
208
|
+
...clientAuth,
|
|
209
|
+
headers: { ...clientAuth.headers, "x-userid": userId },
|
|
210
|
+
secrets: [...clientAuth.secrets, userId],
|
|
211
|
+
};
|
|
212
|
+
await guard();
|
|
213
|
+
const billingPayload = (await fetchProviderJson(
|
|
214
|
+
XAI_BILLING_URL,
|
|
215
|
+
billingAuth,
|
|
216
|
+
signal,
|
|
217
|
+
remainingTimeout(timeoutMs, startedAt),
|
|
218
|
+
"xAI consumer billing endpoint",
|
|
219
|
+
{ redirect: "error", userAgent: false },
|
|
220
|
+
)) as XaiBillingPayload;
|
|
221
|
+
await guard();
|
|
222
|
+
return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export function usageAdapters(xaiUsage = true): readonly UsageProviderAdapter[] {
|
|
227
|
+
return xaiUsage ? [...SUPPORTED_ADAPTERS, XAI_ADAPTER] : SUPPORTED_ADAPTERS;
|
|
228
|
+
}
|
|
229
|
+
|
|
140
230
|
export function adapterForProvider(
|
|
141
231
|
providerId: string | undefined,
|
|
232
|
+
xaiUsage = true,
|
|
142
233
|
): UsageProviderAdapter | undefined {
|
|
143
|
-
return
|
|
234
|
+
return usageAdapters(xaiUsage).find((adapter) => adapter.id === providerId);
|
|
144
235
|
}
|
|
145
236
|
|
|
146
237
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
@@ -204,6 +295,13 @@ export async function resolveUsageAuth(
|
|
|
204
295
|
offered.offeredCount === 0,
|
|
205
296
|
);
|
|
206
297
|
}
|
|
298
|
+
if (adapter.id === "xai") {
|
|
299
|
+
const offered = candidateReader
|
|
300
|
+
? candidateReader(ctx, adapter.id)
|
|
301
|
+
: fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
|
|
302
|
+
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
303
|
+
return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
|
|
304
|
+
}
|
|
207
305
|
const authorization = authorizationFrom(auth);
|
|
208
306
|
if (!authorization) return undefined;
|
|
209
307
|
const headers = { Authorization: authorization };
|
|
@@ -224,9 +322,10 @@ export async function queryProviderUsage(
|
|
|
224
322
|
auth: ResolvedUsageAuth,
|
|
225
323
|
signal: AbortSignal,
|
|
226
324
|
timeoutMs: number,
|
|
325
|
+
guard?: UsageRequestGuard,
|
|
227
326
|
): Promise<UsageReport> {
|
|
228
327
|
try {
|
|
229
|
-
return await adapter.query(auth, signal, timeoutMs);
|
|
328
|
+
return await adapter.query(auth, signal, timeoutMs, guard);
|
|
230
329
|
} catch (error) {
|
|
231
330
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
232
331
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -266,6 +365,8 @@ export async function fetchProviderJson(
|
|
|
266
365
|
request: {
|
|
267
366
|
method?: "GET" | "POST";
|
|
268
367
|
body?: Record<string, unknown>;
|
|
368
|
+
redirect?: RequestRedirect;
|
|
369
|
+
userAgent?: boolean;
|
|
269
370
|
} = {},
|
|
270
371
|
): Promise<Record<string, unknown>> {
|
|
271
372
|
const controller = new AbortController();
|
|
@@ -279,7 +380,9 @@ export async function fetchProviderJson(
|
|
|
279
380
|
}, timeoutMs);
|
|
280
381
|
try {
|
|
281
382
|
const headers = { ...auth.headers };
|
|
282
|
-
if (!hasHeader(headers, "User-Agent"))
|
|
383
|
+
if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
|
|
384
|
+
headers["User-Agent"] = "pi-usage";
|
|
385
|
+
}
|
|
283
386
|
if (request.body && !hasHeader(headers, "Content-Type")) {
|
|
284
387
|
headers["Content-Type"] = "application/json";
|
|
285
388
|
}
|
|
@@ -287,8 +390,10 @@ export async function fetchProviderJson(
|
|
|
287
390
|
method: request.method ?? "GET",
|
|
288
391
|
headers,
|
|
289
392
|
...(request.body ? { body: JSON.stringify(request.body) } : {}),
|
|
393
|
+
...(request.redirect ? { redirect: request.redirect } : {}),
|
|
290
394
|
signal: controller.signal,
|
|
291
395
|
});
|
|
396
|
+
if (response.redirected) throw new Error(`${description} refused a redirected response.`);
|
|
292
397
|
if (controller.signal.aborted)
|
|
293
398
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
294
399
|
const text = await readBoundedResponse(
|
|
@@ -296,6 +401,7 @@ export async function fetchProviderJson(
|
|
|
296
401
|
response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
|
|
297
402
|
!response.ok,
|
|
298
403
|
description,
|
|
404
|
+
controller.signal,
|
|
299
405
|
);
|
|
300
406
|
if (controller.signal.aborted)
|
|
301
407
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
@@ -332,12 +438,16 @@ async function readBoundedResponse(
|
|
|
332
438
|
maxBytes: number,
|
|
333
439
|
truncateOverflow: boolean,
|
|
334
440
|
description: string,
|
|
441
|
+
signal: AbortSignal,
|
|
335
442
|
): Promise<string> {
|
|
336
443
|
if (!response.body) return "";
|
|
337
444
|
const reader = response.body.getReader();
|
|
338
445
|
const chunks: Uint8Array[] = [];
|
|
339
446
|
let total = 0;
|
|
340
447
|
let truncated = false;
|
|
448
|
+
const abort = () => void reader.cancel().catch(() => undefined);
|
|
449
|
+
if (signal.aborted) abort();
|
|
450
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
341
451
|
try {
|
|
342
452
|
while (true) {
|
|
343
453
|
const { done, value } = await reader.read();
|
|
@@ -354,6 +464,7 @@ async function readBoundedResponse(
|
|
|
354
464
|
total += value.byteLength;
|
|
355
465
|
}
|
|
356
466
|
} finally {
|
|
467
|
+
signal.removeEventListener("abort", abort);
|
|
357
468
|
reader.releaseLock();
|
|
358
469
|
}
|
|
359
470
|
if (truncated && !truncateOverflow) {
|
|
@@ -388,6 +499,73 @@ type UsageAuthRegistry = {
|
|
|
388
499
|
>;
|
|
389
500
|
};
|
|
390
501
|
|
|
502
|
+
function resolveXaiUsageAuth(
|
|
503
|
+
auth: RequestAuth,
|
|
504
|
+
model: PiModel,
|
|
505
|
+
salt: Uint8Array,
|
|
506
|
+
candidates: readonly unknown[],
|
|
507
|
+
): ResolvedUsageAuth {
|
|
508
|
+
const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
|
|
509
|
+
if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
|
|
510
|
+
let sawOAuth = false;
|
|
511
|
+
let sawMatchingAccess = false;
|
|
512
|
+
let sawIncompleteMatch = false;
|
|
513
|
+
const matches: Array<{ access: string; refresh: string }> = [];
|
|
514
|
+
for (const candidate of candidates) {
|
|
515
|
+
try {
|
|
516
|
+
const credential = asObject(candidate);
|
|
517
|
+
if (credential?.type !== "oauth") continue;
|
|
518
|
+
sawOAuth = true;
|
|
519
|
+
if (credential.access !== resolvedAccess) continue;
|
|
520
|
+
sawMatchingAccess = true;
|
|
521
|
+
if (
|
|
522
|
+
typeof credential.access !== "string" ||
|
|
523
|
+
!credential.access ||
|
|
524
|
+
typeof credential.refresh !== "string" ||
|
|
525
|
+
!credential.refresh ||
|
|
526
|
+
typeof credential.expires !== "number" ||
|
|
527
|
+
!Number.isFinite(credential.expires)
|
|
528
|
+
) {
|
|
529
|
+
sawIncompleteMatch = true;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
matches.push({ access: credential.access, refresh: credential.refresh });
|
|
533
|
+
} catch {
|
|
534
|
+
// Malformed candidates never authorize a consumer-proxy request.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
538
|
+
if (matches.length > 1) {
|
|
539
|
+
throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
|
|
540
|
+
}
|
|
541
|
+
const match = matches[0];
|
|
542
|
+
if (!match) {
|
|
543
|
+
if (!sawOAuth) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
"xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
|
|
549
|
+
throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
|
|
550
|
+
}
|
|
551
|
+
const authorization = `Bearer ${match.access}`;
|
|
552
|
+
const headers = { Authorization: authorization };
|
|
553
|
+
return {
|
|
554
|
+
apiKey: match.access,
|
|
555
|
+
headers,
|
|
556
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
557
|
+
secrets: [
|
|
558
|
+
match.access,
|
|
559
|
+
match.refresh,
|
|
560
|
+
resolvedAccess,
|
|
561
|
+
auth.apiKey,
|
|
562
|
+
headerValue(auth.headers, "Authorization"),
|
|
563
|
+
authorization,
|
|
564
|
+
].filter((value): value is string => Boolean(value)),
|
|
565
|
+
model,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
391
569
|
function resolveGitHubCopilotUsageAuth(
|
|
392
570
|
auth: RequestAuth,
|
|
393
571
|
model: PiModel,
|
|
@@ -509,6 +687,8 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
509
687
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
510
688
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
511
689
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
690
|
+
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
691
|
+
if (providerId === "xai") return url.origin === "https://api.x.ai";
|
|
512
692
|
if (providerId === "zai") return url.origin === "https://api.z.ai";
|
|
513
693
|
if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
|
|
514
694
|
if (providerId === "github-copilot") {
|
|
@@ -536,6 +716,19 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
536
716
|
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
537
717
|
}
|
|
538
718
|
|
|
719
|
+
function validatedXaiUserId(value: unknown): string {
|
|
720
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
|
|
721
|
+
throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
|
|
722
|
+
}
|
|
723
|
+
return value;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function remainingTimeout(timeoutMs: number, startedAt: number): number {
|
|
727
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
728
|
+
if (remaining <= 0) throw new Error("Timed out while fetching xAI consumer usage.");
|
|
729
|
+
return remaining;
|
|
730
|
+
}
|
|
731
|
+
|
|
539
732
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
540
733
|
const base = baseUrl?.trim();
|
|
541
734
|
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
|
+
xaiUsage: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
15
16
|
codexFastMode: false,
|
|
17
|
+
xaiUsage: true,
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
export interface UsageSettingsState {
|
|
@@ -52,11 +54,16 @@ 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 (Object.hasOwn(value, "xaiUsage") && typeof value.xaiUsage !== "boolean") {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
55
60
|
return {
|
|
56
61
|
codexFastMode:
|
|
57
62
|
typeof value.codexFastMode === "boolean"
|
|
58
63
|
? value.codexFastMode
|
|
59
64
|
: DEFAULT_USAGE_SETTINGS.codexFastMode,
|
|
65
|
+
xaiUsage:
|
|
66
|
+
typeof value.xaiUsage === "boolean" ? value.xaiUsage : DEFAULT_USAGE_SETTINGS.xaiUsage,
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
|
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 =
|
|
@@ -102,6 +108,21 @@ export type ZaiQuotaPayload = {
|
|
|
102
108
|
data?: unknown;
|
|
103
109
|
};
|
|
104
110
|
|
|
111
|
+
export type KimiCodingUsagePayload = {
|
|
112
|
+
usage?: unknown;
|
|
113
|
+
limits?: unknown;
|
|
114
|
+
boosterWallet?: unknown;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type XaiUserPayload = {
|
|
118
|
+
userId?: unknown;
|
|
119
|
+
subscriptionTier?: unknown;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type XaiBillingPayload = {
|
|
123
|
+
config?: unknown;
|
|
124
|
+
};
|
|
125
|
+
|
|
105
126
|
export type CodexBackendPayload = {
|
|
106
127
|
plan_type?: unknown;
|
|
107
128
|
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
|
-
export function configuredAdapters(ctx: ExtensionContext): UsageProviderAdapter[] {
|
|
7
|
-
return
|
|
6
|
+
export function configuredAdapters(ctx: ExtensionContext, xaiUsage = true): UsageProviderAdapter[] {
|
|
7
|
+
return usageAdapters(xaiUsage).filter(
|
|
8
8
|
(adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id),
|
|
9
9
|
);
|
|
10
10
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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, previous: boolean, next: boolean) => 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: "xaiUsage",
|
|
51
|
+
label: "xAI usage",
|
|
52
|
+
description: "Report OAuth subscription allowance and credits.",
|
|
53
|
+
currentValue: state.kind !== "invalid" && state.settings.xaiUsage ? 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
|
+
const effectivePrevious = settingId === "xaiUsage" ? false : previous;
|
|
79
|
+
settingsList.updateValue(id, displayValue(settingId, effectivePrevious));
|
|
80
|
+
if (!signal.aborted && isCurrent()) {
|
|
81
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
82
|
+
tui.requestRender();
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await settingsRuntime.update({ [settingId]: requested }, signal);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (signal.aborted || !isCurrent()) return;
|
|
90
|
+
settingsList.updateValue(id, displayValue(settingId, previous));
|
|
91
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
92
|
+
tui.requestRender();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (previous !== requested) {
|
|
96
|
+
changed = true;
|
|
97
|
+
onApplied(settingId, previous, requested);
|
|
98
|
+
}
|
|
99
|
+
if (signal.aborted || !isCurrent()) return;
|
|
100
|
+
settingsList.updateValue(id, displayValue(settingId, requested));
|
|
101
|
+
tui.requestRender();
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
cancel,
|
|
105
|
+
);
|
|
106
|
+
container.addChild(settingsList);
|
|
107
|
+
|
|
108
|
+
parentSignal.addEventListener("abort", cancel, { once: true });
|
|
109
|
+
return {
|
|
110
|
+
render: (width: number) => container.render(width),
|
|
111
|
+
invalidate: () => container.invalidate(),
|
|
112
|
+
handleInput(data: string) {
|
|
113
|
+
if (closing) return;
|
|
114
|
+
if (matchesKey(data, Key.ctrl("c"))) cancel();
|
|
115
|
+
else settingsList.handleInput(data);
|
|
116
|
+
tui.requestRender();
|
|
117
|
+
},
|
|
118
|
+
dispose() {
|
|
119
|
+
localController.abort();
|
|
120
|
+
parentSignal.removeEventListener("abort", cancel);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function displayValue(_id: UsageSettingId, enabled: boolean): string {
|
|
127
|
+
return enabled ? ON : OFF;
|
|
128
|
+
}
|