@narumitw/pi-usage 0.54.0 → 0.58.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 +137 -64
- package/dist/index.ts +783 -173
- package/dist/index.ts.map +4 -4
- package/package.json +10 -10
- package/src/codex-fast-runtime.ts +34 -26
- package/src/format.ts +126 -7
- package/src/index.ts +9 -0
- package/src/providers/deepseek.ts +85 -0
- package/src/providers/fireworks.ts +198 -0
- package/src/query.ts +199 -13
- package/src/settings.ts +26 -6
- package/src/types.ts +19 -0
- package/src/usage-helpers.ts +2 -2
- package/src/usage-settings-ui.ts +130 -39
- package/src/usage.ts +156 -94
package/src/query.ts
CHANGED
|
@@ -6,6 +6,12 @@ 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";
|
|
10
|
+
import {
|
|
11
|
+
isFireworksAccountId,
|
|
12
|
+
normalizeFireworksAccountsPayload,
|
|
13
|
+
normalizeFireworksBillingSummaryPayload,
|
|
14
|
+
} from "./providers/fireworks.js";
|
|
9
15
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
10
16
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
11
17
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -14,6 +20,9 @@ import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
|
14
20
|
import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
15
21
|
import type {
|
|
16
22
|
CodexBackendPayload,
|
|
23
|
+
DeepSeekBalancePayload,
|
|
24
|
+
FireworksAccountsPayload,
|
|
25
|
+
FireworksBillingSummaryPayload,
|
|
17
26
|
GitHubCopilotUsagePayload,
|
|
18
27
|
KimiCodingUsagePayload,
|
|
19
28
|
OpenCodeZenPayload,
|
|
@@ -21,6 +30,7 @@ import type {
|
|
|
21
30
|
PiModel,
|
|
22
31
|
ResolvedUsageAuth,
|
|
23
32
|
UsageProviderAdapter,
|
|
33
|
+
UsageQuerySettings,
|
|
24
34
|
UsageReport,
|
|
25
35
|
XaiBillingPayload,
|
|
26
36
|
XaiUserPayload,
|
|
@@ -28,6 +38,10 @@ import type {
|
|
|
28
38
|
} from "./types.js";
|
|
29
39
|
|
|
30
40
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
41
|
+
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
42
|
+
const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
|
|
43
|
+
const FIREWORKS_SPEND_WINDOW_DAYS = 30;
|
|
44
|
+
const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
|
|
31
45
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
32
46
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
33
47
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
@@ -65,6 +79,27 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
65
79
|
return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
|
|
66
80
|
},
|
|
67
81
|
},
|
|
82
|
+
{
|
|
83
|
+
id: "deepseek",
|
|
84
|
+
displayName: "DeepSeek",
|
|
85
|
+
semantics: { kind: "api-key", label: "DeepSeek API balance" },
|
|
86
|
+
async query(auth, signal, timeoutMs, guard) {
|
|
87
|
+
if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
await guard();
|
|
90
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
91
|
+
if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
|
|
92
|
+
const payload = await fetchProviderJson(
|
|
93
|
+
DEEPSEEK_BALANCE_URL,
|
|
94
|
+
auth,
|
|
95
|
+
signal,
|
|
96
|
+
remainingMs,
|
|
97
|
+
"DeepSeek API balance endpoint",
|
|
98
|
+
{ redirect: "error" },
|
|
99
|
+
);
|
|
100
|
+
return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
|
|
101
|
+
},
|
|
102
|
+
},
|
|
68
103
|
{
|
|
69
104
|
id: "github-copilot",
|
|
70
105
|
displayName: "GitHub Copilot",
|
|
@@ -98,6 +133,35 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
98
133
|
return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
|
|
99
134
|
},
|
|
100
135
|
},
|
|
136
|
+
{
|
|
137
|
+
id: "fireworks",
|
|
138
|
+
displayName: "Fireworks",
|
|
139
|
+
semantics: { kind: "api-key", label: "Fireworks API spend" },
|
|
140
|
+
async query(auth, signal, timeoutMs, guard, settings) {
|
|
141
|
+
if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
await guard();
|
|
144
|
+
const accountId = await resolveFireworksAccountId(
|
|
145
|
+
auth,
|
|
146
|
+
signal,
|
|
147
|
+
remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
|
|
148
|
+
guard,
|
|
149
|
+
settings?.fireworksAccountId,
|
|
150
|
+
);
|
|
151
|
+
await guard();
|
|
152
|
+
const billingWindowAt = Date.now();
|
|
153
|
+
const payload = (await fetchProviderJson(
|
|
154
|
+
fireworksBillingSummaryUrl(accountId, billingWindowAt),
|
|
155
|
+
auth,
|
|
156
|
+
signal,
|
|
157
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
|
|
158
|
+
"Fireworks billing summary endpoint",
|
|
159
|
+
{ redirect: "error" },
|
|
160
|
+
)) as FireworksBillingSummaryPayload;
|
|
161
|
+
await guard();
|
|
162
|
+
return normalizeFireworksBillingSummaryPayload(payload, accountId, Date.now());
|
|
163
|
+
},
|
|
164
|
+
},
|
|
101
165
|
{
|
|
102
166
|
id: "opencode-go",
|
|
103
167
|
displayName: "OpenCode Go",
|
|
@@ -133,7 +197,6 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
133
197
|
id: "zai",
|
|
134
198
|
displayName: "Z.AI",
|
|
135
199
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
136
|
-
publishesStatusline: false,
|
|
137
200
|
async query(auth, signal, timeoutMs) {
|
|
138
201
|
const payload = await fetchProviderJson(
|
|
139
202
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -149,7 +212,6 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
149
212
|
id: "zai-coding-cn",
|
|
150
213
|
displayName: "Z.AI Coding CN",
|
|
151
214
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
152
|
-
publishesStatusline: false,
|
|
153
215
|
async query(auth, signal, timeoutMs) {
|
|
154
216
|
const payload = await fetchProviderJson(
|
|
155
217
|
zaiMonitorUrl(auth.model.baseUrl),
|
|
@@ -223,15 +285,14 @@ export const XAI_ADAPTER: UsageProviderAdapter = {
|
|
|
223
285
|
},
|
|
224
286
|
};
|
|
225
287
|
|
|
226
|
-
export function usageAdapters(
|
|
227
|
-
return
|
|
288
|
+
export function usageAdapters(): readonly UsageProviderAdapter[] {
|
|
289
|
+
return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
|
|
228
290
|
}
|
|
229
291
|
|
|
230
292
|
export function adapterForProvider(
|
|
231
293
|
providerId: string | undefined,
|
|
232
|
-
xaiUsage = true,
|
|
233
294
|
): UsageProviderAdapter | undefined {
|
|
234
|
-
return usageAdapters(
|
|
295
|
+
return usageAdapters().find((adapter) => adapter.id === providerId);
|
|
235
296
|
}
|
|
236
297
|
|
|
237
298
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
@@ -261,11 +322,14 @@ export async function resolveUsageAuth(
|
|
|
261
322
|
// SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
|
|
262
323
|
const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
|
|
263
324
|
let modelAuth: RequestAuth | undefined;
|
|
264
|
-
|
|
265
|
-
|
|
325
|
+
const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
|
|
326
|
+
const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
|
|
327
|
+
if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
|
|
328
|
+
const result = await registry.getApiKeyAndHeaders(currentModel);
|
|
266
329
|
if (!result.ok) throw new Error(redactUsageError(result.error));
|
|
267
|
-
|
|
268
|
-
}
|
|
330
|
+
return authorizationFrom(result) ? result : undefined;
|
|
331
|
+
};
|
|
332
|
+
if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
|
|
269
333
|
if (typeof registry.getProviderAuth !== "function") {
|
|
270
334
|
throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
|
|
271
335
|
}
|
|
@@ -278,6 +342,9 @@ export async function resolveUsageAuth(
|
|
|
278
342
|
`${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
|
|
279
343
|
);
|
|
280
344
|
}
|
|
345
|
+
// DeepSeek reads selected-model auth last so a rotation during provider-origin validation
|
|
346
|
+
// cannot leave the earlier credential queued for the balance request.
|
|
347
|
+
if (adapter.id === "deepseek") modelAuth = await resolveCurrentModelAuth();
|
|
281
348
|
const auth = modelAuth ?? providerResult?.auth;
|
|
282
349
|
if (!auth) return undefined;
|
|
283
350
|
if (adapter.id === "github-copilot") {
|
|
@@ -302,6 +369,26 @@ export async function resolveUsageAuth(
|
|
|
302
369
|
if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
|
|
303
370
|
return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
|
|
304
371
|
}
|
|
372
|
+
if (adapter.id === "deepseek") {
|
|
373
|
+
const resolvedAuthorization = authorizationFrom(auth);
|
|
374
|
+
const access = bearerToken(resolvedAuthorization);
|
|
375
|
+
if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
|
|
376
|
+
const authorization = `Bearer ${access}`;
|
|
377
|
+
const headers = { Authorization: authorization };
|
|
378
|
+
return {
|
|
379
|
+
apiKey: access,
|
|
380
|
+
headers,
|
|
381
|
+
fingerprint: fingerprintResolvedAuth({ headers }, salt),
|
|
382
|
+
secrets: [
|
|
383
|
+
access,
|
|
384
|
+
auth.apiKey,
|
|
385
|
+
headerValue(auth.headers, "Authorization"),
|
|
386
|
+
resolvedAuthorization,
|
|
387
|
+
authorization,
|
|
388
|
+
].filter((value): value is string => Boolean(value)),
|
|
389
|
+
model,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
305
392
|
const authorization = authorizationFrom(auth);
|
|
306
393
|
if (!authorization) return undefined;
|
|
307
394
|
const headers = { Authorization: authorization };
|
|
@@ -323,9 +410,10 @@ export async function queryProviderUsage(
|
|
|
323
410
|
signal: AbortSignal,
|
|
324
411
|
timeoutMs: number,
|
|
325
412
|
guard?: UsageRequestGuard,
|
|
413
|
+
settings?: Readonly<UsageQuerySettings>,
|
|
326
414
|
): Promise<UsageReport> {
|
|
327
415
|
try {
|
|
328
|
-
return await adapter.query(auth, signal, timeoutMs, guard);
|
|
416
|
+
return await adapter.query(auth, signal, timeoutMs, guard, settings);
|
|
329
417
|
} catch (error) {
|
|
330
418
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
331
419
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -685,6 +773,8 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
685
773
|
try {
|
|
686
774
|
const url = new URL(value);
|
|
687
775
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
776
|
+
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
777
|
+
if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
|
|
688
778
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
689
779
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
690
780
|
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
@@ -723,12 +813,108 @@ function validatedXaiUserId(value: unknown): string {
|
|
|
723
813
|
return value;
|
|
724
814
|
}
|
|
725
815
|
|
|
726
|
-
function remainingTimeout(
|
|
816
|
+
function remainingTimeout(
|
|
817
|
+
timeoutMs: number,
|
|
818
|
+
startedAt: number,
|
|
819
|
+
description = "fetching xAI consumer usage",
|
|
820
|
+
): number {
|
|
727
821
|
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
728
|
-
if (remaining <= 0) throw new Error(
|
|
822
|
+
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
729
823
|
return remaining;
|
|
730
824
|
}
|
|
731
825
|
|
|
826
|
+
// Fireworks requires an account slug for its billing endpoints; discover it through the
|
|
827
|
+
// documented account listing, requiring an explicit slug when a key can see several accounts.
|
|
828
|
+
async function resolveFireworksAccountId(
|
|
829
|
+
auth: ResolvedUsageAuth,
|
|
830
|
+
signal: AbortSignal,
|
|
831
|
+
timeoutMs: number,
|
|
832
|
+
guard: () => Promise<void>,
|
|
833
|
+
configuredAccountId: string | undefined,
|
|
834
|
+
): Promise<string> {
|
|
835
|
+
if (configuredAccountId !== undefined && !isFireworksAccountId(configuredAccountId)) {
|
|
836
|
+
throw new Error("The Fireworks account setting was not a safe account slug.");
|
|
837
|
+
}
|
|
838
|
+
const startedAt = Date.now();
|
|
839
|
+
const accounts: string[] = [];
|
|
840
|
+
let pageToken: string | undefined;
|
|
841
|
+
for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
|
|
842
|
+
await guard();
|
|
843
|
+
const payload = (await fetchProviderJson(
|
|
844
|
+
fireworksAccountsUrl(pageToken),
|
|
845
|
+
auth,
|
|
846
|
+
signal,
|
|
847
|
+
remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
|
|
848
|
+
"Fireworks accounts endpoint",
|
|
849
|
+
{ redirect: "error" },
|
|
850
|
+
)) as FireworksAccountsPayload;
|
|
851
|
+
for (const accountId of normalizeFireworksAccountsPayload(
|
|
852
|
+
payload as FireworksAccountsPayload,
|
|
853
|
+
)) {
|
|
854
|
+
if (accounts.includes(accountId)) {
|
|
855
|
+
throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
|
|
856
|
+
}
|
|
857
|
+
accounts.push(accountId);
|
|
858
|
+
if (configuredAccountId === accountId) return accountId;
|
|
859
|
+
}
|
|
860
|
+
pageToken = fireworksNextPageToken(payload.nextPageToken);
|
|
861
|
+
if (!pageToken) break;
|
|
862
|
+
}
|
|
863
|
+
if (pageToken) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
configuredAccountId
|
|
866
|
+
? `The configured Fireworks account was not found within the first ${FIREWORKS_MAX_ACCOUNT_PAGES} listing pages.`
|
|
867
|
+
: `Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages; set fireworksAccountId in pi-usage.json to an account returned in those pages.`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (accounts.length === 0) {
|
|
871
|
+
throw new Error("Fireworks account discovery returned no accounts for this API key.");
|
|
872
|
+
}
|
|
873
|
+
if (configuredAccountId) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
"The configured Fireworks account does not match an account visible to this API key.",
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (accounts.length === 1) return accounts[0] as string;
|
|
879
|
+
const preview = accounts.slice(0, 8).join(", ");
|
|
880
|
+
const suffix = accounts.length > 8 ? ` …and ${accounts.length - 8} more` : "";
|
|
881
|
+
throw new Error(
|
|
882
|
+
`The Fireworks key can see ${accounts.length} accounts (${preview}${suffix}); set fireworksAccountId in pi-usage.json to one of them.`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function fireworksAccountsUrl(pageToken: string | undefined): string {
|
|
887
|
+
const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
|
|
888
|
+
url.searchParams.set("pageSize", "200");
|
|
889
|
+
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
|
|
890
|
+
return url.toString();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function fireworksNextPageToken(value: unknown): string | undefined {
|
|
894
|
+
if (value === undefined || value === null) return undefined;
|
|
895
|
+
if (typeof value !== "string" || !value || value.length > 512) {
|
|
896
|
+
throw new Error("Fireworks accounts listing returned an invalid page token.");
|
|
897
|
+
}
|
|
898
|
+
return value;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
|
|
902
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
903
|
+
const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
|
|
904
|
+
const url = new URL(
|
|
905
|
+
`/v1/accounts/${accountId}/billing/summary`,
|
|
906
|
+
FIREWORKS_BILLING_SUMMARY_ORIGIN,
|
|
907
|
+
);
|
|
908
|
+
// The endpoint aggregates by UTC date; endTime is exclusive, so the window includes today
|
|
909
|
+
// plus the preceding 29 dates.
|
|
910
|
+
url.searchParams.set(
|
|
911
|
+
"startTime",
|
|
912
|
+
dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
|
|
913
|
+
);
|
|
914
|
+
url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
|
|
915
|
+
return url.toString();
|
|
916
|
+
}
|
|
917
|
+
|
|
732
918
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
733
919
|
const base = baseUrl?.trim();
|
|
734
920
|
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
package/src/settings.ts
CHANGED
|
@@ -3,18 +3,20 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { isFireworksAccountId } from "./providers/fireworks.js";
|
|
6
7
|
|
|
7
8
|
export const USAGE_SETTINGS_FILE = "pi-usage.json";
|
|
8
9
|
export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
9
10
|
|
|
10
11
|
export interface UsageSettings {
|
|
11
12
|
codexFastMode: boolean;
|
|
12
|
-
|
|
13
|
+
codexStatusResetCountdown: boolean;
|
|
14
|
+
fireworksAccountId?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
|
|
16
18
|
codexFastMode: false,
|
|
17
|
-
|
|
19
|
+
codexStatusResetCountdown: true,
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
export interface UsageSettingsState {
|
|
@@ -54,7 +56,16 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
54
56
|
if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
|
|
55
57
|
return undefined;
|
|
56
58
|
}
|
|
57
|
-
if (
|
|
59
|
+
if (
|
|
60
|
+
Object.hasOwn(value, "codexStatusResetCountdown") &&
|
|
61
|
+
typeof value.codexStatusResetCountdown !== "boolean"
|
|
62
|
+
) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
Object.hasOwn(value, "fireworksAccountId") &&
|
|
67
|
+
!isFireworksAccountId(value.fireworksAccountId)
|
|
68
|
+
) {
|
|
58
69
|
return undefined;
|
|
59
70
|
}
|
|
60
71
|
return {
|
|
@@ -62,8 +73,13 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
62
73
|
typeof value.codexFastMode === "boolean"
|
|
63
74
|
? value.codexFastMode
|
|
64
75
|
: DEFAULT_USAGE_SETTINGS.codexFastMode,
|
|
65
|
-
|
|
66
|
-
typeof value.
|
|
76
|
+
codexStatusResetCountdown:
|
|
77
|
+
typeof value.codexStatusResetCountdown === "boolean"
|
|
78
|
+
? value.codexStatusResetCountdown
|
|
79
|
+
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
80
|
+
...(isFireworksAccountId(value.fireworksAccountId)
|
|
81
|
+
? { fireworksAccountId: value.fireworksAccountId }
|
|
82
|
+
: {}),
|
|
67
83
|
};
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -167,7 +183,11 @@ async function saveUsageSettingsPatch(
|
|
|
167
183
|
if (latest.kind === "invalid") {
|
|
168
184
|
throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
|
|
169
185
|
}
|
|
170
|
-
const document = { ...latest.document
|
|
186
|
+
const document = { ...latest.document };
|
|
187
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
188
|
+
if (value === undefined) delete document[key];
|
|
189
|
+
else document[key] = value;
|
|
190
|
+
}
|
|
171
191
|
const settings = normalizeUsageSettings(document);
|
|
172
192
|
if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
|
|
173
193
|
const directory = dirname(path);
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface ResolvedUsageAuth {
|
|
|
55
55
|
model: PiModel;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface UsageQuerySettings {
|
|
59
|
+
fireworksAccountId?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
export interface UsageProviderAdapter {
|
|
59
63
|
id: string;
|
|
60
64
|
displayName: string;
|
|
@@ -65,6 +69,7 @@ export interface UsageProviderAdapter {
|
|
|
65
69
|
signal: AbortSignal,
|
|
66
70
|
timeoutMs: number,
|
|
67
71
|
guard?: () => Promise<void>,
|
|
72
|
+
settings?: Readonly<UsageQuerySettings>,
|
|
68
73
|
): Promise<UsageReport>;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -84,6 +89,20 @@ export type ProviderUsageState =
|
|
|
84
89
|
message: string;
|
|
85
90
|
};
|
|
86
91
|
|
|
92
|
+
export type DeepSeekBalancePayload = {
|
|
93
|
+
is_available?: unknown;
|
|
94
|
+
balance_infos?: unknown;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type FireworksAccountsPayload = {
|
|
98
|
+
accounts?: unknown;
|
|
99
|
+
nextPageToken?: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type FireworksBillingSummaryPayload = {
|
|
103
|
+
lineItems?: unknown;
|
|
104
|
+
};
|
|
105
|
+
|
|
87
106
|
export type GitHubCopilotUsagePayload = {
|
|
88
107
|
login?: unknown;
|
|
89
108
|
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
|
@@ -11,33 +11,60 @@ import {
|
|
|
11
11
|
Text,
|
|
12
12
|
} from "@earendil-works/pi-tui";
|
|
13
13
|
import { errorMessage } from "./core.js";
|
|
14
|
+
import { isFireworksAccountId } from "./providers/fireworks.js";
|
|
14
15
|
import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
|
|
15
16
|
|
|
17
|
+
const AUTO = "Auto";
|
|
18
|
+
const EDIT = "Edit…";
|
|
16
19
|
const OFF = "Off";
|
|
17
20
|
const ON = "On";
|
|
18
21
|
|
|
19
22
|
type UsageSettingId = keyof UsageSettings;
|
|
23
|
+
type SettingsScreenResult = { changed: boolean; editFireworksAccount: boolean };
|
|
20
24
|
|
|
21
25
|
export async function showUsageSettings(
|
|
22
26
|
ctx: ExtensionCommandContext,
|
|
23
27
|
settingsRuntime: UsageSettingsRuntime,
|
|
24
28
|
parentSignal: AbortSignal,
|
|
25
29
|
isCurrent: () => boolean,
|
|
26
|
-
onApplied: (id: UsageSettingId
|
|
30
|
+
onApplied: (id: UsageSettingId) => void,
|
|
27
31
|
): Promise<boolean> {
|
|
28
32
|
if (ctx.mode !== "tui") {
|
|
29
33
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
30
34
|
return false;
|
|
31
35
|
}
|
|
32
|
-
|
|
36
|
+
let changed = false;
|
|
37
|
+
while (!parentSignal.aborted && isCurrent()) {
|
|
38
|
+
const result = await showSettingsList(ctx, settingsRuntime, parentSignal, isCurrent, onApplied);
|
|
39
|
+
if (!result) return changed;
|
|
40
|
+
changed ||= result.changed;
|
|
41
|
+
if (!result.editFireworksAccount) return changed;
|
|
42
|
+
changed ||= await editFireworksAccount(
|
|
43
|
+
ctx,
|
|
44
|
+
settingsRuntime,
|
|
45
|
+
parentSignal,
|
|
46
|
+
isCurrent,
|
|
47
|
+
onApplied,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return changed;
|
|
51
|
+
}
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
async function showSettingsList(
|
|
54
|
+
ctx: ExtensionCommandContext,
|
|
55
|
+
settingsRuntime: UsageSettingsRuntime,
|
|
56
|
+
parentSignal: AbortSignal,
|
|
57
|
+
isCurrent: () => boolean,
|
|
58
|
+
onApplied: (id: UsageSettingId) => void,
|
|
59
|
+
): Promise<SettingsScreenResult | undefined> {
|
|
60
|
+
return ctx.ui.custom<SettingsScreenResult>((tui, theme, _keybindings, done) => {
|
|
35
61
|
const localController = new AbortController();
|
|
36
62
|
const signal = AbortSignal.any([parentSignal, localController.signal]);
|
|
37
63
|
let changed = false;
|
|
38
64
|
let closing = false;
|
|
39
65
|
let saveQueue = Promise.resolve();
|
|
40
66
|
const state = settingsRuntime.get();
|
|
67
|
+
const fireworksValue = state.settings.fireworksAccountId ?? AUTO;
|
|
41
68
|
const items: SettingItem[] = [
|
|
42
69
|
{
|
|
43
70
|
id: "codexFastMode",
|
|
@@ -47,12 +74,21 @@ export async function showUsageSettings(
|
|
|
47
74
|
values: [OFF, ON],
|
|
48
75
|
},
|
|
49
76
|
{
|
|
50
|
-
id: "
|
|
51
|
-
label: "
|
|
52
|
-
description: "
|
|
53
|
-
currentValue: state.
|
|
77
|
+
id: "codexStatusResetCountdown",
|
|
78
|
+
label: "Codex reset countdown",
|
|
79
|
+
description: "Show time remaining until each Codex usage limit resets.",
|
|
80
|
+
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
54
81
|
values: [OFF, ON],
|
|
55
82
|
},
|
|
83
|
+
{
|
|
84
|
+
id: "fireworksAccountId",
|
|
85
|
+
label: "Fireworks account",
|
|
86
|
+
description: "Select Edit to enter a visible account slug, or submit blank to clear it.",
|
|
87
|
+
currentValue: fireworksValue,
|
|
88
|
+
values: state.settings.fireworksAccountId
|
|
89
|
+
? [state.settings.fireworksAccountId, EDIT]
|
|
90
|
+
: [AUTO, EDIT],
|
|
91
|
+
},
|
|
56
92
|
];
|
|
57
93
|
const container = new Container();
|
|
58
94
|
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
@@ -62,7 +98,40 @@ export async function showUsageSettings(
|
|
|
62
98
|
if (closing) return;
|
|
63
99
|
closing = true;
|
|
64
100
|
localController.abort();
|
|
65
|
-
done(changed);
|
|
101
|
+
done({ changed, editFireworksAccount: false });
|
|
102
|
+
};
|
|
103
|
+
const queueUpdate = (
|
|
104
|
+
id: UsageSettingId,
|
|
105
|
+
requested: UsageSettings[UsageSettingId],
|
|
106
|
+
display: string,
|
|
107
|
+
) => {
|
|
108
|
+
saveQueue = saveQueue.then(async () => {
|
|
109
|
+
const previous = settingsRuntime.get().settings[id];
|
|
110
|
+
if (settingsRuntime.get().kind === "invalid") {
|
|
111
|
+
settingsList.updateValue(id, displaySetting(id, previous));
|
|
112
|
+
if (!signal.aborted && isCurrent()) {
|
|
113
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
114
|
+
tui.requestRender();
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await settingsRuntime.update({ [id]: requested }, signal);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (signal.aborted || !isCurrent()) return;
|
|
122
|
+
settingsList.updateValue(id, displaySetting(id, previous));
|
|
123
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
124
|
+
tui.requestRender();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (previous !== requested) {
|
|
128
|
+
changed = true;
|
|
129
|
+
onApplied(id);
|
|
130
|
+
}
|
|
131
|
+
if (signal.aborted || !isCurrent()) return;
|
|
132
|
+
settingsList.updateValue(id, display);
|
|
133
|
+
tui.requestRender();
|
|
134
|
+
});
|
|
66
135
|
};
|
|
67
136
|
settingsList = new SettingsList(
|
|
68
137
|
items,
|
|
@@ -70,36 +139,18 @@ export async function showUsageSettings(
|
|
|
70
139
|
getSettingsListTheme(),
|
|
71
140
|
(id, value) => {
|
|
72
141
|
if (closing || signal.aborted || !isCurrent()) return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
142
|
+
if (id === "fireworksAccountId") {
|
|
143
|
+
if (value === EDIT) {
|
|
144
|
+
saveQueue = saveQueue.then(() => {
|
|
145
|
+
if (closing || signal.aborted || !isCurrent()) return;
|
|
146
|
+
closing = true;
|
|
147
|
+
done({ changed, editFireworksAccount: true });
|
|
148
|
+
});
|
|
85
149
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const settingId = id as "codexFastMode" | "codexStatusResetCountdown";
|
|
153
|
+
queueUpdate(settingId, value !== OFF, value);
|
|
103
154
|
},
|
|
104
155
|
cancel,
|
|
105
156
|
);
|
|
@@ -123,6 +174,46 @@ export async function showUsageSettings(
|
|
|
123
174
|
});
|
|
124
175
|
}
|
|
125
176
|
|
|
126
|
-
function
|
|
127
|
-
|
|
177
|
+
async function editFireworksAccount(
|
|
178
|
+
ctx: ExtensionCommandContext,
|
|
179
|
+
settingsRuntime: UsageSettingsRuntime,
|
|
180
|
+
signal: AbortSignal,
|
|
181
|
+
isCurrent: () => boolean,
|
|
182
|
+
onApplied: (id: UsageSettingId) => void,
|
|
183
|
+
): Promise<boolean> {
|
|
184
|
+
while (!signal.aborted && isCurrent()) {
|
|
185
|
+
const state = settingsRuntime.get();
|
|
186
|
+
if (state.kind === "invalid") {
|
|
187
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const entered = await ctx.ui.input(
|
|
191
|
+
"Fireworks account slug · submit blank for Auto",
|
|
192
|
+
state.settings.fireworksAccountId ?? "Example: acme",
|
|
193
|
+
{ signal },
|
|
194
|
+
);
|
|
195
|
+
if (signal.aborted || !isCurrent() || entered === undefined) return false;
|
|
196
|
+
const normalized = entered.trim();
|
|
197
|
+
const requested = normalized || undefined;
|
|
198
|
+
if (requested !== undefined && !isFireworksAccountId(requested)) {
|
|
199
|
+
ctx.ui.notify("Enter a URL-safe Fireworks account slug.", "warning");
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (requested === state.settings.fireworksAccountId) return false;
|
|
203
|
+
try {
|
|
204
|
+
await settingsRuntime.update({ fireworksAccountId: requested }, signal);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (signal.aborted || !isCurrent()) return false;
|
|
207
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
onApplied("fireworksAccountId");
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function displaySetting(id: UsageSettingId, value: UsageSettings[UsageSettingId]): string {
|
|
217
|
+
if (id === "fireworksAccountId") return typeof value === "string" ? value : AUTO;
|
|
218
|
+
return value ? ON : OFF;
|
|
128
219
|
}
|