@narumitw/pi-usage 0.57.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 +36 -2
- package/dist/index.ts +492 -101
- package/dist/index.ts.map +4 -4
- package/package.json +2 -1
- package/src/codex-fast-runtime.ts +34 -26
- package/src/format.ts +33 -1
- package/src/index.ts +7 -0
- package/src/providers/fireworks.ts +198 -0
- package/src/query.ts +141 -3
- package/src/settings.ts +16 -1
- package/src/types.ts +14 -0
- package/src/usage-settings-ui.ts +125 -33
- package/src/usage.ts +37 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"quota",
|
|
14
14
|
"balance",
|
|
15
15
|
"deepseek",
|
|
16
|
+
"fireworks",
|
|
16
17
|
"codex",
|
|
17
18
|
"kimi",
|
|
18
19
|
"kimi-coding",
|
|
@@ -24,6 +24,7 @@ export function registerCodexFastMode(
|
|
|
24
24
|
pi: ExtensionAPI,
|
|
25
25
|
settingsRuntime: UsageSettingsRuntime,
|
|
26
26
|
refreshStatus: (ctx: ExtensionContext) => void,
|
|
27
|
+
options: { registerSessionStart?: boolean } = {},
|
|
27
28
|
) {
|
|
28
29
|
let sessionController = new AbortController();
|
|
29
30
|
let generation = 0;
|
|
@@ -95,41 +96,47 @@ export function registerCodexFastMode(
|
|
|
95
96
|
},
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
const prepareSession = (ctx: ExtensionContext): Promise<void> => {
|
|
100
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
99
101
|
generation += 1;
|
|
100
102
|
sessionController.abort();
|
|
101
103
|
pendingFastRequests.clear();
|
|
102
104
|
sessionController = new AbortController();
|
|
103
105
|
const ownerGeneration = generation;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
return (async () => {
|
|
107
|
+
let state: Readonly<UsageSettingsState>;
|
|
108
|
+
try {
|
|
109
|
+
state = await settingsRuntime.reload(sessionController.signal);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (sessionController.signal.aborted || ownerGeneration !== generation) return;
|
|
112
|
+
if (ctx.hasUI) {
|
|
113
|
+
ctx.ui.notify(
|
|
114
|
+
`Could not load pi-usage.json; using defaults. ${errorMessage(error)}`,
|
|
115
|
+
"warning",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
sessionController.signal.aborted ||
|
|
122
|
+
ownerGeneration !== generation ||
|
|
123
|
+
ctx.sessionManager.getSessionId() !== sessionId
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (ctx.hasUI && state.kind === "invalid") {
|
|
111
128
|
ctx.ui.notify(
|
|
112
|
-
`
|
|
129
|
+
`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
|
|
113
130
|
"warning",
|
|
114
131
|
);
|
|
115
132
|
}
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (ctx.hasUI && state.kind === "invalid") {
|
|
126
|
-
ctx.ui.notify(
|
|
127
|
-
`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
|
|
128
|
-
"warning",
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
refreshStatus(ctx);
|
|
132
|
-
});
|
|
133
|
+
refreshStatus(ctx);
|
|
134
|
+
})();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (options.registerSessionStart !== false) {
|
|
138
|
+
pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
|
|
139
|
+
}
|
|
133
140
|
|
|
134
141
|
pi.on("before_provider_request", (event, ctx) => {
|
|
135
142
|
const rewritten = rewriteCodexFastPayload(
|
|
@@ -164,6 +171,7 @@ export function registerCodexFastMode(
|
|
|
164
171
|
});
|
|
165
172
|
|
|
166
173
|
return {
|
|
174
|
+
prepareSession,
|
|
167
175
|
availability(model: PiModel | undefined) {
|
|
168
176
|
return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
|
|
169
177
|
},
|
package/src/format.ts
CHANGED
|
@@ -12,13 +12,18 @@ const VALUE_COLUMN = 29;
|
|
|
12
12
|
export function formatUsageReport(report: UsageReport, displayState: UsageDisplayState): string {
|
|
13
13
|
const stateLabel = displayState === "current" ? "Current" : "Configured";
|
|
14
14
|
const title =
|
|
15
|
-
report.providerId === "deepseek"
|
|
15
|
+
report.providerId === "deepseek"
|
|
16
|
+
? "DeepSeek API Balance"
|
|
17
|
+
: report.providerId === "fireworks"
|
|
18
|
+
? "Fireworks API Spend"
|
|
19
|
+
: `${report.providerName} Usage`;
|
|
16
20
|
const lines = [`${title} · ${stateLabel}`];
|
|
17
21
|
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
18
22
|
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
19
23
|
|
|
20
24
|
if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
21
25
|
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
26
|
+
else if (report.providerId === "fireworks") formatFireworksReport(lines, report);
|
|
22
27
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
23
28
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
24
29
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
@@ -44,6 +49,7 @@ export function formatUsageStatusline(
|
|
|
44
49
|
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
45
50
|
}
|
|
46
51
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
52
|
+
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
47
53
|
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
48
54
|
if (report.providerId === "openrouter") {
|
|
49
55
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -125,6 +131,32 @@ function formatDeepSeekStatusline(report: UsageReport): string {
|
|
|
125
131
|
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
function formatFireworksReport(lines: string[], report: UsageReport): void {
|
|
135
|
+
lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days (rated)`);
|
|
136
|
+
for (const currency of fireworksCurrencies(report)) {
|
|
137
|
+
lines.push("", `${currency} rated spend:`);
|
|
138
|
+
for (const metric of report.metrics) {
|
|
139
|
+
if (metric.currency !== currency) continue;
|
|
140
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function formatFireworksStatusline(report: UsageReport): string {
|
|
146
|
+
const totals = report.metrics.filter((metric) => metric.id.endsWith("-total"));
|
|
147
|
+
if (totals.length === 0) return "fireworks no rated usage";
|
|
148
|
+
return `fireworks ${totals.map((metric) => `${metric.currency} ${metric.value}`).join(" · ")}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fireworksCurrencies(report: UsageReport): string[] {
|
|
152
|
+
const currencies: string[] = [];
|
|
153
|
+
for (const metric of report.metrics) {
|
|
154
|
+
if (!metric.currency || currencies.includes(metric.currency)) continue;
|
|
155
|
+
currencies.push(metric.currency);
|
|
156
|
+
}
|
|
157
|
+
return currencies;
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
129
161
|
const quota = findGitHubCopilotQuota(report);
|
|
130
162
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,10 @@ export {
|
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
35
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
36
36
|
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
37
|
+
export {
|
|
38
|
+
normalizeFireworksAccountsPayload,
|
|
39
|
+
normalizeFireworksBillingSummaryPayload,
|
|
40
|
+
} from "./providers/fireworks.js";
|
|
37
41
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
38
42
|
export { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
39
43
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -64,6 +68,8 @@ export {
|
|
|
64
68
|
} from "./settings.js";
|
|
65
69
|
export type {
|
|
66
70
|
DeepSeekBalancePayload,
|
|
71
|
+
FireworksAccountsPayload,
|
|
72
|
+
FireworksBillingSummaryPayload,
|
|
67
73
|
KimiCodingUsagePayload,
|
|
68
74
|
ProviderUsageState,
|
|
69
75
|
ResolvedUsageAuth,
|
|
@@ -72,6 +78,7 @@ export type {
|
|
|
72
78
|
UsageMetric,
|
|
73
79
|
UsageModel,
|
|
74
80
|
UsageProviderAdapter,
|
|
81
|
+
UsageQuerySettings,
|
|
75
82
|
UsageReport,
|
|
76
83
|
UsageSemantics,
|
|
77
84
|
UsageSemanticsKind,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type {
|
|
3
|
+
FireworksAccountsPayload,
|
|
4
|
+
FireworksBillingSummaryPayload,
|
|
5
|
+
UsageMetric,
|
|
6
|
+
UsageReport,
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
|
|
9
|
+
const NANOS_PER_UNIT = 1_000_000_000n;
|
|
10
|
+
const CURRENCY_PATTERN = /^[A-Z]{3}$/u;
|
|
11
|
+
const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u;
|
|
12
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
13
|
+
const INT64_MIN = -(2n ** 63n);
|
|
14
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
15
|
+
const MAX_UNITS_CHARS = 20;
|
|
16
|
+
const MAX_NANOS_CHARS = 11;
|
|
17
|
+
|
|
18
|
+
const SERIES_KEYS = ["serverless", "dedicated", "training", "other"] as const;
|
|
19
|
+
type SeriesKey = (typeof SERIES_KEYS)[number];
|
|
20
|
+
|
|
21
|
+
const SERIES_LABELS: Readonly<Record<SeriesKey, string>> = {
|
|
22
|
+
serverless: "Serverless",
|
|
23
|
+
dedicated: "Dedicated deployments",
|
|
24
|
+
training: "Training",
|
|
25
|
+
other: "Other",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function isFireworksAccountId(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && ACCOUNT_ID_PATTERN.test(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayload): string[] {
|
|
33
|
+
if (!Array.isArray(payload.accounts)) {
|
|
34
|
+
throw new Error("Fireworks accounts response did not contain an accounts array.");
|
|
35
|
+
}
|
|
36
|
+
const accounts: string[] = [];
|
|
37
|
+
for (const raw of payload.accounts) {
|
|
38
|
+
const account = asObject(raw);
|
|
39
|
+
if (!account) throw new Error("Fireworks accounts response row was not an object.");
|
|
40
|
+
if (typeof account.name !== "string") {
|
|
41
|
+
throw new Error("Fireworks accounts response omitted the account resource name.");
|
|
42
|
+
}
|
|
43
|
+
const match = /^accounts\/([^/]+)$/u.exec(account.name);
|
|
44
|
+
if (!match || !isFireworksAccountId(match[1])) {
|
|
45
|
+
throw new Error("Fireworks accounts response returned an unsafe account resource name.");
|
|
46
|
+
}
|
|
47
|
+
const accountId = match[1];
|
|
48
|
+
if (accounts.includes(accountId)) {
|
|
49
|
+
throw new Error(`Fireworks accounts response repeated ${accountId}.`);
|
|
50
|
+
}
|
|
51
|
+
accounts.push(accountId);
|
|
52
|
+
}
|
|
53
|
+
return accounts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeFireworksBillingSummaryPayload(
|
|
57
|
+
payload: FireworksBillingSummaryPayload,
|
|
58
|
+
accountId: string,
|
|
59
|
+
capturedAt: number,
|
|
60
|
+
): UsageReport {
|
|
61
|
+
if (!isFireworksAccountId(accountId)) {
|
|
62
|
+
throw new Error("Fireworks billing summary received an unsafe account identifier.");
|
|
63
|
+
}
|
|
64
|
+
if (payload.lineItems !== undefined && !Array.isArray(payload.lineItems)) {
|
|
65
|
+
throw new Error("Fireworks billing summary lineItems was not an array.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const totals = new Map<string, Map<SeriesKey, bigint>>();
|
|
69
|
+
for (const raw of payload.lineItems ?? []) {
|
|
70
|
+
const lineItem = asObject(raw);
|
|
71
|
+
if (!lineItem) throw new Error("Fireworks billing line item was not an object.");
|
|
72
|
+
const cost = moneyAmount(lineItem.totalCost, "line item total cost");
|
|
73
|
+
const series = seriesKey(lineItem.series);
|
|
74
|
+
let amounts = totals.get(cost.currency);
|
|
75
|
+
if (!amounts) {
|
|
76
|
+
amounts = new Map<SeriesKey, bigint>();
|
|
77
|
+
totals.set(cost.currency, amounts);
|
|
78
|
+
}
|
|
79
|
+
amounts.set(series, (amounts.get(series) ?? 0n) + cost.amount);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const metrics: UsageMetric[] = [];
|
|
83
|
+
for (const [currency, amounts] of totals) {
|
|
84
|
+
metrics.push({
|
|
85
|
+
id: `${currency.toLowerCase()}-total`,
|
|
86
|
+
label: "Total spend",
|
|
87
|
+
value: formatMoneyAmount(sumSeries(amounts)),
|
|
88
|
+
unit: "currency",
|
|
89
|
+
currency,
|
|
90
|
+
});
|
|
91
|
+
for (const series of SERIES_KEYS) {
|
|
92
|
+
const amount = amounts.get(series);
|
|
93
|
+
if (amount === undefined) continue;
|
|
94
|
+
metrics.push({
|
|
95
|
+
id: `${currency.toLowerCase()}-${series}`,
|
|
96
|
+
label: SERIES_LABELS[series],
|
|
97
|
+
value: formatMoneyAmount(amount),
|
|
98
|
+
unit: "currency",
|
|
99
|
+
currency,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const notes = [
|
|
105
|
+
"Rated line items may differ from the final invoice once credits or adjustments are applied.",
|
|
106
|
+
];
|
|
107
|
+
if (metrics.length === 0) {
|
|
108
|
+
notes.push("Fireworks returned no rated line items for the last 30 days.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
providerId: "fireworks",
|
|
113
|
+
providerName: "Fireworks",
|
|
114
|
+
capturedAt,
|
|
115
|
+
source: "fireworks-billing-summary",
|
|
116
|
+
semantics: { kind: "api-key", label: "Fireworks API spend" },
|
|
117
|
+
accountLabel: sanitizeDisplayText(accountId, 80),
|
|
118
|
+
buckets: [],
|
|
119
|
+
metrics,
|
|
120
|
+
notes,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type RatedMoney = { currency: string; amount: bigint };
|
|
125
|
+
|
|
126
|
+
function moneyAmount(value: unknown, description: string): RatedMoney {
|
|
127
|
+
const money = asObject(value);
|
|
128
|
+
if (!money) throw new Error(`Fireworks billing ${description} was not a money object.`);
|
|
129
|
+
const currency = typeof money.currencyCode === "string" ? money.currencyCode : undefined;
|
|
130
|
+
if (!currency || !CURRENCY_PATTERN.test(currency)) {
|
|
131
|
+
throw new Error(`Fireworks billing ${description} currency was not an ISO 4217 code.`);
|
|
132
|
+
}
|
|
133
|
+
// proto3 JSON omits zero-valued money fields, so absent components default to zero.
|
|
134
|
+
const units =
|
|
135
|
+
money.units === undefined
|
|
136
|
+
? 0n
|
|
137
|
+
: integerComponent(money.units, description, "whole units", MAX_UNITS_CHARS);
|
|
138
|
+
if (units < INT64_MIN || units > INT64_MAX) {
|
|
139
|
+
throw new Error(`Fireworks billing ${description} whole units exceeded the int64 range.`);
|
|
140
|
+
}
|
|
141
|
+
const nanos =
|
|
142
|
+
money.nanos === undefined
|
|
143
|
+
? 0n
|
|
144
|
+
: integerComponent(money.nanos, description, "nano units", MAX_NANOS_CHARS);
|
|
145
|
+
if (nanos <= -NANOS_PER_UNIT || nanos >= NANOS_PER_UNIT) {
|
|
146
|
+
throw new Error(`Fireworks billing ${description} nano units exceeded the Money range.`);
|
|
147
|
+
}
|
|
148
|
+
if ((units > 0n && nanos < 0n) || (units < 0n && nanos > 0n)) {
|
|
149
|
+
throw new Error(`Fireworks billing ${description} mixed unit and nano signs.`);
|
|
150
|
+
}
|
|
151
|
+
return { currency, amount: units * NANOS_PER_UNIT + nanos };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function integerComponent(
|
|
155
|
+
value: unknown,
|
|
156
|
+
description: string,
|
|
157
|
+
component: string,
|
|
158
|
+
maxChars: number,
|
|
159
|
+
): bigint {
|
|
160
|
+
const text =
|
|
161
|
+
typeof value === "number" && Number.isSafeInteger(value)
|
|
162
|
+
? String(value)
|
|
163
|
+
: typeof value === "string" && INTEGER_PATTERN.test(value)
|
|
164
|
+
? value
|
|
165
|
+
: undefined;
|
|
166
|
+
if (text === undefined || text.length > maxChars) {
|
|
167
|
+
throw new Error(`Fireworks billing ${description} ${component} was not a bounded integer.`);
|
|
168
|
+
}
|
|
169
|
+
return BigInt(text);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function seriesKey(value: unknown): SeriesKey {
|
|
173
|
+
if (value === undefined || value === null) return "other";
|
|
174
|
+
if (typeof value !== "string") throw new Error("Fireworks billing line item series was invalid.");
|
|
175
|
+
if (value === "SERVERLESS") return "serverless";
|
|
176
|
+
if (value === "DEDICATED_DEPLOYMENT") return "dedicated";
|
|
177
|
+
if (value === "TRAINING") return "training";
|
|
178
|
+
return "other";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sumSeries(amounts: ReadonlyMap<SeriesKey, bigint>): bigint {
|
|
182
|
+
let total = 0n;
|
|
183
|
+
for (const amount of amounts.values()) total += amount;
|
|
184
|
+
return total;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatMoneyAmount(amount: bigint): string {
|
|
188
|
+
const negative = amount < 0n;
|
|
189
|
+
const magnitude = negative ? -amount : amount;
|
|
190
|
+
const units = magnitude / NANOS_PER_UNIT;
|
|
191
|
+
const nanos = (magnitude % NANOS_PER_UNIT).toString().padStart(9, "0").replace(/0+$/u, "");
|
|
192
|
+
return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
197
|
+
return value as Record<string, unknown>;
|
|
198
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
} from "./oauth-credential-source.js";
|
|
8
8
|
import { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
9
9
|
import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
10
|
+
import {
|
|
11
|
+
isFireworksAccountId,
|
|
12
|
+
normalizeFireworksAccountsPayload,
|
|
13
|
+
normalizeFireworksBillingSummaryPayload,
|
|
14
|
+
} from "./providers/fireworks.js";
|
|
10
15
|
import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
11
16
|
import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
12
17
|
import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
@@ -16,6 +21,8 @@ import { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
|
16
21
|
import type {
|
|
17
22
|
CodexBackendPayload,
|
|
18
23
|
DeepSeekBalancePayload,
|
|
24
|
+
FireworksAccountsPayload,
|
|
25
|
+
FireworksBillingSummaryPayload,
|
|
19
26
|
GitHubCopilotUsagePayload,
|
|
20
27
|
KimiCodingUsagePayload,
|
|
21
28
|
OpenCodeZenPayload,
|
|
@@ -23,6 +30,7 @@ import type {
|
|
|
23
30
|
PiModel,
|
|
24
31
|
ResolvedUsageAuth,
|
|
25
32
|
UsageProviderAdapter,
|
|
33
|
+
UsageQuerySettings,
|
|
26
34
|
UsageReport,
|
|
27
35
|
XaiBillingPayload,
|
|
28
36
|
XaiUserPayload,
|
|
@@ -31,6 +39,9 @@ import type {
|
|
|
31
39
|
|
|
32
40
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
33
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;
|
|
34
45
|
const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
|
|
35
46
|
const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
|
|
36
47
|
const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
@@ -122,6 +133,35 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
122
133
|
return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
|
|
123
134
|
},
|
|
124
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
|
+
},
|
|
125
165
|
{
|
|
126
166
|
id: "opencode-go",
|
|
127
167
|
displayName: "OpenCode Go",
|
|
@@ -370,9 +410,10 @@ export async function queryProviderUsage(
|
|
|
370
410
|
signal: AbortSignal,
|
|
371
411
|
timeoutMs: number,
|
|
372
412
|
guard?: UsageRequestGuard,
|
|
413
|
+
settings?: Readonly<UsageQuerySettings>,
|
|
373
414
|
): Promise<UsageReport> {
|
|
374
415
|
try {
|
|
375
|
-
return await adapter.query(auth, signal, timeoutMs, guard);
|
|
416
|
+
return await adapter.query(auth, signal, timeoutMs, guard, settings);
|
|
376
417
|
} catch (error) {
|
|
377
418
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
378
419
|
throw new Error(redactUsageError(errorMessage(error), auth.secrets));
|
|
@@ -733,6 +774,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
|
|
|
733
774
|
const url = new URL(value);
|
|
734
775
|
if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
|
|
735
776
|
if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
|
|
777
|
+
if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
|
|
736
778
|
if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
|
|
737
779
|
if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
|
|
738
780
|
if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
|
|
@@ -771,12 +813,108 @@ function validatedXaiUserId(value: unknown): string {
|
|
|
771
813
|
return value;
|
|
772
814
|
}
|
|
773
815
|
|
|
774
|
-
function remainingTimeout(
|
|
816
|
+
function remainingTimeout(
|
|
817
|
+
timeoutMs: number,
|
|
818
|
+
startedAt: number,
|
|
819
|
+
description = "fetching xAI consumer usage",
|
|
820
|
+
): number {
|
|
775
821
|
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
776
|
-
if (remaining <= 0) throw new Error(
|
|
822
|
+
if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
|
|
777
823
|
return remaining;
|
|
778
824
|
}
|
|
779
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
|
+
|
|
780
918
|
function zaiMonitorUrl(baseUrl: string | undefined): string {
|
|
781
919
|
const base = baseUrl?.trim();
|
|
782
920
|
if (!base) throw new Error("Z.AI model base URL is unavailable.");
|
package/src/settings.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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;
|
|
@@ -10,6 +11,7 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
|
|
|
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({
|
|
@@ -60,6 +62,12 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
60
62
|
) {
|
|
61
63
|
return undefined;
|
|
62
64
|
}
|
|
65
|
+
if (
|
|
66
|
+
Object.hasOwn(value, "fireworksAccountId") &&
|
|
67
|
+
!isFireworksAccountId(value.fireworksAccountId)
|
|
68
|
+
) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
63
71
|
return {
|
|
64
72
|
codexFastMode:
|
|
65
73
|
typeof value.codexFastMode === "boolean"
|
|
@@ -69,6 +77,9 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
|
|
|
69
77
|
typeof value.codexStatusResetCountdown === "boolean"
|
|
70
78
|
? value.codexStatusResetCountdown
|
|
71
79
|
: DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
|
|
80
|
+
...(isFireworksAccountId(value.fireworksAccountId)
|
|
81
|
+
? { fireworksAccountId: value.fireworksAccountId }
|
|
82
|
+
: {}),
|
|
72
83
|
};
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -172,7 +183,11 @@ async function saveUsageSettingsPatch(
|
|
|
172
183
|
if (latest.kind === "invalid") {
|
|
173
184
|
throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
|
|
174
185
|
}
|
|
175
|
-
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
|
+
}
|
|
176
191
|
const settings = normalizeUsageSettings(document);
|
|
177
192
|
if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
|
|
178
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
|
|
|
@@ -89,6 +94,15 @@ export type DeepSeekBalancePayload = {
|
|
|
89
94
|
balance_infos?: unknown;
|
|
90
95
|
};
|
|
91
96
|
|
|
97
|
+
export type FireworksAccountsPayload = {
|
|
98
|
+
accounts?: unknown;
|
|
99
|
+
nextPageToken?: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type FireworksBillingSummaryPayload = {
|
|
103
|
+
lineItems?: unknown;
|
|
104
|
+
};
|
|
105
|
+
|
|
92
106
|
export type GitHubCopilotUsagePayload = {
|
|
93
107
|
login?: unknown;
|
|
94
108
|
copilot_plan?: unknown;
|