@narumitw/pi-usage 0.60.7 → 0.60.8

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.
@@ -1,11 +1,11 @@
1
1
  import { sanitizeDisplayText } from "../core.js";
2
2
  import type {
3
- FireworksAccountsPayload,
4
- FireworksBillingSummaryPayload,
5
- ResolvedUsageAuth,
6
- UsageMetric,
7
- UsageProviderAdapter,
8
- UsageReport,
3
+ FireworksAccountsPayload,
4
+ FireworksBillingSummaryPayload,
5
+ ResolvedUsageAuth,
6
+ UsageMetric,
7
+ UsageProviderAdapter,
8
+ UsageReport,
9
9
  } from "../types.js";
10
10
 
11
11
  const NANOS_PER_UNIT = 1_000_000_000n;
@@ -24,287 +24,268 @@ const SERIES_KEYS = ["serverless", "dedicated", "training", "other"] as const;
24
24
  type SeriesKey = (typeof SERIES_KEYS)[number];
25
25
 
26
26
  const SERIES_LABELS: Readonly<Record<SeriesKey, string>> = {
27
- serverless: "Serverless",
28
- dedicated: "Dedicated deployments",
29
- training: "Training",
30
- other: "Other",
27
+ serverless: "Serverless",
28
+ dedicated: "Dedicated deployments",
29
+ training: "Training",
30
+ other: "Other",
31
31
  };
32
32
 
33
33
  export function isFireworksAccountId(value: unknown): value is string {
34
- return typeof value === "string" && ACCOUNT_ID_PATTERN.test(value);
34
+ return typeof value === "string" && ACCOUNT_ID_PATTERN.test(value);
35
35
  }
36
36
 
37
37
  export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayload): string[] {
38
- if (!Array.isArray(payload.accounts)) {
39
- throw new Error("Fireworks accounts response did not contain an accounts array.");
40
- }
41
- const accounts: string[] = [];
42
- for (const raw of payload.accounts) {
43
- const account = asObject(raw);
44
- if (!account) throw new Error("Fireworks accounts response row was not an object.");
45
- if (typeof account.name !== "string") {
46
- throw new Error("Fireworks accounts response omitted the account resource name.");
47
- }
48
- const match = /^accounts\/([^/]+)$/u.exec(account.name);
49
- if (!match || !isFireworksAccountId(match[1])) {
50
- throw new Error("Fireworks accounts response returned an unsafe account resource name.");
51
- }
52
- const accountId = match[1];
53
- if (accounts.includes(accountId)) {
54
- throw new Error(`Fireworks accounts response repeated ${accountId}.`);
55
- }
56
- accounts.push(accountId);
57
- }
58
- return accounts;
38
+ if (!Array.isArray(payload.accounts)) {
39
+ throw new Error("Fireworks accounts response did not contain an accounts array.");
40
+ }
41
+ const accounts: string[] = [];
42
+ for (const raw of payload.accounts) {
43
+ const account = asObject(raw);
44
+ if (!account) throw new Error("Fireworks accounts response row was not an object.");
45
+ if (typeof account.name !== "string") {
46
+ throw new Error("Fireworks accounts response omitted the account resource name.");
47
+ }
48
+ const match = /^accounts\/([^/]+)$/u.exec(account.name);
49
+ if (!match || !isFireworksAccountId(match[1])) {
50
+ throw new Error("Fireworks accounts response returned an unsafe account resource name.");
51
+ }
52
+ const accountId = match[1];
53
+ if (accounts.includes(accountId)) {
54
+ throw new Error(`Fireworks accounts response repeated ${accountId}.`);
55
+ }
56
+ accounts.push(accountId);
57
+ }
58
+ return accounts;
59
59
  }
60
60
 
61
61
  type FetchProviderJson = (
62
- url: string,
63
- auth: ResolvedUsageAuth,
64
- signal: AbortSignal,
65
- timeoutMs: number,
66
- description: string,
67
- request?: { redirect?: RequestRedirect },
62
+ url: string,
63
+ auth: ResolvedUsageAuth,
64
+ signal: AbortSignal,
65
+ timeoutMs: number,
66
+ description: string,
67
+ request?: { redirect?: RequestRedirect },
68
68
  ) => Promise<Record<string, unknown>>;
69
69
 
70
70
  export function createFireworksAdapter(fetchProviderJson: FetchProviderJson): UsageProviderAdapter {
71
- return {
72
- id: "fireworks",
73
- displayName: "Fireworks",
74
- semantics: { kind: "api-key", label: "Fireworks API spend" },
75
- targets: {
76
- singularLabel: "account",
77
- pluralLabel: "accounts",
78
- async list(auth, signal, timeoutMs, guard) {
79
- const startedAt = Date.now();
80
- const accounts: string[] = [];
81
- let pageToken: string | undefined;
82
- for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
83
- await guard();
84
- const payload = (await fetchProviderJson(
85
- fireworksAccountsUrl(pageToken),
86
- auth,
87
- signal,
88
- remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
89
- "Fireworks accounts endpoint",
90
- { redirect: "error" },
91
- )) as FireworksAccountsPayload;
92
- await guard();
93
- for (const accountId of normalizeFireworksAccountsPayload(payload)) {
94
- if (accounts.includes(accountId)) {
95
- throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
96
- }
97
- accounts.push(accountId);
98
- }
99
- pageToken = fireworksNextPageToken(payload.nextPageToken);
100
- if (!pageToken) break;
101
- }
102
- if (pageToken) {
103
- throw new Error(
104
- `Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages.`,
105
- );
106
- }
107
- return accounts.map((id) => ({ id, label: id }));
108
- },
109
- },
110
- async query(auth, signal, timeoutMs, guard, targetId) {
111
- if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
112
- if (!isFireworksAccountId(targetId)) {
113
- throw new Error("Fireworks billing requires a safe selected account slug.");
114
- }
115
- const startedAt = Date.now();
116
- await guard();
117
- const billingWindowAt = Date.now();
118
- const payload = (await fetchProviderJson(
119
- fireworksBillingSummaryUrl(targetId, billingWindowAt),
120
- auth,
121
- signal,
122
- remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
123
- "Fireworks billing summary endpoint",
124
- { redirect: "error" },
125
- )) as FireworksBillingSummaryPayload;
126
- await guard();
127
- return normalizeFireworksBillingSummaryPayload(payload, targetId, Date.now());
128
- },
129
- };
71
+ return {
72
+ id: "fireworks",
73
+ displayName: "Fireworks",
74
+ semantics: { kind: "api-key", label: "Fireworks API spend" },
75
+ targets: {
76
+ singularLabel: "account",
77
+ pluralLabel: "accounts",
78
+ async list(auth, signal, timeoutMs, guard) {
79
+ const startedAt = Date.now();
80
+ const accounts: string[] = [];
81
+ let pageToken: string | undefined;
82
+ for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
83
+ await guard();
84
+ const payload = (await fetchProviderJson(
85
+ fireworksAccountsUrl(pageToken),
86
+ auth,
87
+ signal,
88
+ remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
89
+ "Fireworks accounts endpoint",
90
+ { redirect: "error" },
91
+ )) as FireworksAccountsPayload;
92
+ await guard();
93
+ for (const accountId of normalizeFireworksAccountsPayload(payload)) {
94
+ if (accounts.includes(accountId)) {
95
+ throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
96
+ }
97
+ accounts.push(accountId);
98
+ }
99
+ pageToken = fireworksNextPageToken(payload.nextPageToken);
100
+ if (!pageToken) break;
101
+ }
102
+ if (pageToken) {
103
+ throw new Error(`Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages.`);
104
+ }
105
+ return accounts.map((id) => ({ id, label: id }));
106
+ },
107
+ },
108
+ async query(auth, signal, timeoutMs, guard, targetId) {
109
+ if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
110
+ if (!isFireworksAccountId(targetId)) {
111
+ throw new Error("Fireworks billing requires a safe selected account slug.");
112
+ }
113
+ const startedAt = Date.now();
114
+ await guard();
115
+ const billingWindowAt = Date.now();
116
+ const payload = (await fetchProviderJson(
117
+ fireworksBillingSummaryUrl(targetId, billingWindowAt),
118
+ auth,
119
+ signal,
120
+ remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
121
+ "Fireworks billing summary endpoint",
122
+ { redirect: "error" },
123
+ )) as FireworksBillingSummaryPayload;
124
+ await guard();
125
+ return normalizeFireworksBillingSummaryPayload(payload, targetId, Date.now());
126
+ },
127
+ };
130
128
  }
131
129
 
132
130
  export function normalizeFireworksBillingSummaryPayload(
133
- payload: FireworksBillingSummaryPayload,
134
- accountId: string,
135
- capturedAt: number,
131
+ payload: FireworksBillingSummaryPayload,
132
+ accountId: string,
133
+ capturedAt: number,
136
134
  ): UsageReport {
137
- if (!isFireworksAccountId(accountId)) {
138
- throw new Error("Fireworks billing summary received an unsafe account identifier.");
139
- }
140
- if (payload.lineItems !== undefined && !Array.isArray(payload.lineItems)) {
141
- throw new Error("Fireworks billing summary lineItems was not an array.");
142
- }
135
+ if (!isFireworksAccountId(accountId)) {
136
+ throw new Error("Fireworks billing summary received an unsafe account identifier.");
137
+ }
138
+ if (payload.lineItems !== undefined && !Array.isArray(payload.lineItems)) {
139
+ throw new Error("Fireworks billing summary lineItems was not an array.");
140
+ }
143
141
 
144
- const totals = new Map<string, Map<SeriesKey, bigint>>();
145
- for (const raw of payload.lineItems ?? []) {
146
- const lineItem = asObject(raw);
147
- if (!lineItem) throw new Error("Fireworks billing line item was not an object.");
148
- const cost = moneyAmount(lineItem.totalCost, "line item total cost");
149
- const series = seriesKey(lineItem.series);
150
- let amounts = totals.get(cost.currency);
151
- if (!amounts) {
152
- amounts = new Map<SeriesKey, bigint>();
153
- totals.set(cost.currency, amounts);
154
- }
155
- amounts.set(series, (amounts.get(series) ?? 0n) + cost.amount);
156
- }
142
+ const totals = new Map<string, Map<SeriesKey, bigint>>();
143
+ for (const raw of payload.lineItems ?? []) {
144
+ const lineItem = asObject(raw);
145
+ if (!lineItem) throw new Error("Fireworks billing line item was not an object.");
146
+ const cost = moneyAmount(lineItem.totalCost, "line item total cost");
147
+ const series = seriesKey(lineItem.series);
148
+ let amounts = totals.get(cost.currency);
149
+ if (!amounts) {
150
+ amounts = new Map<SeriesKey, bigint>();
151
+ totals.set(cost.currency, amounts);
152
+ }
153
+ amounts.set(series, (amounts.get(series) ?? 0n) + cost.amount);
154
+ }
157
155
 
158
- const metrics: UsageMetric[] = [];
159
- for (const [currency, amounts] of totals) {
160
- metrics.push({
161
- id: `${currency.toLowerCase()}-total`,
162
- label: "Total spend",
163
- value: formatMoneyAmount(sumSeries(amounts)),
164
- unit: "currency",
165
- currency,
166
- });
167
- for (const series of SERIES_KEYS) {
168
- const amount = amounts.get(series);
169
- if (amount === undefined) continue;
170
- metrics.push({
171
- id: `${currency.toLowerCase()}-${series}`,
172
- label: SERIES_LABELS[series],
173
- value: formatMoneyAmount(amount),
174
- unit: "currency",
175
- currency,
176
- });
177
- }
178
- }
156
+ const metrics: UsageMetric[] = [];
157
+ for (const [currency, amounts] of totals) {
158
+ metrics.push({
159
+ id: `${currency.toLowerCase()}-total`,
160
+ label: "Total spend",
161
+ value: formatMoneyAmount(sumSeries(amounts)),
162
+ unit: "currency",
163
+ currency,
164
+ });
165
+ for (const series of SERIES_KEYS) {
166
+ const amount = amounts.get(series);
167
+ if (amount === undefined) continue;
168
+ metrics.push({
169
+ id: `${currency.toLowerCase()}-${series}`,
170
+ label: SERIES_LABELS[series],
171
+ value: formatMoneyAmount(amount),
172
+ unit: "currency",
173
+ currency,
174
+ });
175
+ }
176
+ }
179
177
 
180
- const notes = [
181
- "Rated line items may differ from the final invoice once credits or adjustments are applied.",
182
- ];
183
- if (metrics.length === 0) {
184
- notes.push("Fireworks returned no rated line items for the last 30 days.");
185
- }
178
+ const notes = ["Rated line items may differ from the final invoice once credits or adjustments are applied."];
179
+ if (metrics.length === 0) {
180
+ notes.push("Fireworks returned no rated line items for the last 30 days.");
181
+ }
186
182
 
187
- return {
188
- providerId: "fireworks",
189
- providerName: "Fireworks",
190
- capturedAt,
191
- source: "fireworks-billing-summary",
192
- semantics: { kind: "api-key", label: "Fireworks API spend" },
193
- accountLabel: sanitizeDisplayText(accountId, 80),
194
- buckets: [],
195
- metrics,
196
- notes,
197
- };
183
+ return {
184
+ providerId: "fireworks",
185
+ providerName: "Fireworks",
186
+ capturedAt,
187
+ source: "fireworks-billing-summary",
188
+ semantics: { kind: "api-key", label: "Fireworks API spend" },
189
+ accountLabel: sanitizeDisplayText(accountId, 80),
190
+ buckets: [],
191
+ metrics,
192
+ notes,
193
+ };
198
194
  }
199
195
 
200
196
  type RatedMoney = { currency: string; amount: bigint };
201
197
 
202
198
  function moneyAmount(value: unknown, description: string): RatedMoney {
203
- const money = asObject(value);
204
- if (!money) throw new Error(`Fireworks billing ${description} was not a money object.`);
205
- const currency = typeof money.currencyCode === "string" ? money.currencyCode : undefined;
206
- if (!currency || !CURRENCY_PATTERN.test(currency)) {
207
- throw new Error(`Fireworks billing ${description} currency was not an ISO 4217 code.`);
208
- }
209
- // proto3 JSON omits zero-valued money fields, so absent components default to zero.
210
- const units =
211
- money.units === undefined
212
- ? 0n
213
- : integerComponent(money.units, description, "whole units", MAX_UNITS_CHARS);
214
- if (units < INT64_MIN || units > INT64_MAX) {
215
- throw new Error(`Fireworks billing ${description} whole units exceeded the int64 range.`);
216
- }
217
- const nanos =
218
- money.nanos === undefined
219
- ? 0n
220
- : integerComponent(money.nanos, description, "nano units", MAX_NANOS_CHARS);
221
- if (nanos <= -NANOS_PER_UNIT || nanos >= NANOS_PER_UNIT) {
222
- throw new Error(`Fireworks billing ${description} nano units exceeded the Money range.`);
223
- }
224
- if ((units > 0n && nanos < 0n) || (units < 0n && nanos > 0n)) {
225
- throw new Error(`Fireworks billing ${description} mixed unit and nano signs.`);
226
- }
227
- return { currency, amount: units * NANOS_PER_UNIT + nanos };
199
+ const money = asObject(value);
200
+ if (!money) throw new Error(`Fireworks billing ${description} was not a money object.`);
201
+ const currency = typeof money.currencyCode === "string" ? money.currencyCode : undefined;
202
+ if (!currency || !CURRENCY_PATTERN.test(currency)) {
203
+ throw new Error(`Fireworks billing ${description} currency was not an ISO 4217 code.`);
204
+ }
205
+ // proto3 JSON omits zero-valued money fields, so absent components default to zero.
206
+ const units =
207
+ money.units === undefined ? 0n : integerComponent(money.units, description, "whole units", MAX_UNITS_CHARS);
208
+ if (units < INT64_MIN || units > INT64_MAX) {
209
+ throw new Error(`Fireworks billing ${description} whole units exceeded the int64 range.`);
210
+ }
211
+ const nanos =
212
+ money.nanos === undefined ? 0n : integerComponent(money.nanos, description, "nano units", MAX_NANOS_CHARS);
213
+ if (nanos <= -NANOS_PER_UNIT || nanos >= NANOS_PER_UNIT) {
214
+ throw new Error(`Fireworks billing ${description} nano units exceeded the Money range.`);
215
+ }
216
+ if ((units > 0n && nanos < 0n) || (units < 0n && nanos > 0n)) {
217
+ throw new Error(`Fireworks billing ${description} mixed unit and nano signs.`);
218
+ }
219
+ return { currency, amount: units * NANOS_PER_UNIT + nanos };
228
220
  }
229
221
 
230
- function integerComponent(
231
- value: unknown,
232
- description: string,
233
- component: string,
234
- maxChars: number,
235
- ): bigint {
236
- const text =
237
- typeof value === "number" && Number.isSafeInteger(value)
238
- ? String(value)
239
- : typeof value === "string" && INTEGER_PATTERN.test(value)
240
- ? value
241
- : undefined;
242
- if (text === undefined || text.length > maxChars) {
243
- throw new Error(`Fireworks billing ${description} ${component} was not a bounded integer.`);
244
- }
245
- return BigInt(text);
222
+ function integerComponent(value: unknown, description: string, component: string, maxChars: number): bigint {
223
+ const text =
224
+ typeof value === "number" && Number.isSafeInteger(value)
225
+ ? String(value)
226
+ : typeof value === "string" && INTEGER_PATTERN.test(value)
227
+ ? value
228
+ : undefined;
229
+ if (text === undefined || text.length > maxChars) {
230
+ throw new Error(`Fireworks billing ${description} ${component} was not a bounded integer.`);
231
+ }
232
+ return BigInt(text);
246
233
  }
247
234
 
248
235
  function seriesKey(value: unknown): SeriesKey {
249
- if (value === undefined || value === null) return "other";
250
- if (typeof value !== "string") throw new Error("Fireworks billing line item series was invalid.");
251
- if (value === "SERVERLESS") return "serverless";
252
- if (value === "DEDICATED_DEPLOYMENT") return "dedicated";
253
- if (value === "TRAINING") return "training";
254
- return "other";
236
+ if (value === undefined || value === null) return "other";
237
+ if (typeof value !== "string") throw new Error("Fireworks billing line item series was invalid.");
238
+ if (value === "SERVERLESS") return "serverless";
239
+ if (value === "DEDICATED_DEPLOYMENT") return "dedicated";
240
+ if (value === "TRAINING") return "training";
241
+ return "other";
255
242
  }
256
243
 
257
244
  function sumSeries(amounts: ReadonlyMap<SeriesKey, bigint>): bigint {
258
- let total = 0n;
259
- for (const amount of amounts.values()) total += amount;
260
- return total;
245
+ let total = 0n;
246
+ for (const amount of amounts.values()) total += amount;
247
+ return total;
261
248
  }
262
249
 
263
250
  function formatMoneyAmount(amount: bigint): string {
264
- const negative = amount < 0n;
265
- const magnitude = negative ? -amount : amount;
266
- const units = magnitude / NANOS_PER_UNIT;
267
- const nanos = (magnitude % NANOS_PER_UNIT).toString().padStart(9, "0").replace(/0+$/u, "");
268
- return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
251
+ const negative = amount < 0n;
252
+ const magnitude = negative ? -amount : amount;
253
+ const units = magnitude / NANOS_PER_UNIT;
254
+ const nanos = (magnitude % NANOS_PER_UNIT).toString().padStart(9, "0").replace(/0+$/u, "");
255
+ return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
269
256
  }
270
257
 
271
258
  function fireworksAccountsUrl(pageToken: string | undefined): string {
272
- const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
273
- url.searchParams.set("pageSize", "200");
274
- if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
275
- return url.toString();
259
+ const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
260
+ url.searchParams.set("pageSize", "200");
261
+ if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
262
+ return url.toString();
276
263
  }
277
264
 
278
265
  function fireworksNextPageToken(value: unknown): string | undefined {
279
- if (value === undefined || value === null) return undefined;
280
- if (typeof value !== "string" || !value || value.length > 512) {
281
- throw new Error("Fireworks accounts listing returned an invalid page token.");
282
- }
283
- return value;
266
+ if (value === undefined || value === null) return undefined;
267
+ if (typeof value !== "string" || !value || value.length > 512) {
268
+ throw new Error("Fireworks accounts listing returned an invalid page token.");
269
+ }
270
+ return value;
284
271
  }
285
272
 
286
273
  function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
287
- const dayMs = 24 * 60 * 60 * 1000;
288
- const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
289
- const url = new URL(
290
- `/v1/accounts/${accountId}/billing/summary`,
291
- FIREWORKS_BILLING_SUMMARY_ORIGIN,
292
- );
293
- url.searchParams.set(
294
- "startTime",
295
- dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
296
- );
297
- url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
298
- return url.toString();
274
+ const dayMs = 24 * 60 * 60 * 1000;
275
+ const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
276
+ const url = new URL(`/v1/accounts/${accountId}/billing/summary`, FIREWORKS_BILLING_SUMMARY_ORIGIN);
277
+ url.searchParams.set("startTime", dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs));
278
+ url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
279
+ return url.toString();
299
280
  }
300
281
 
301
282
  function remainingTimeout(timeoutMs: number, startedAt: number, description: string): number {
302
- const remaining = timeoutMs - (Date.now() - startedAt);
303
- if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
304
- return remaining;
283
+ const remaining = timeoutMs - (Date.now() - startedAt);
284
+ if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
285
+ return remaining;
305
286
  }
306
287
 
307
288
  function asObject(value: unknown): Record<string, unknown> | undefined {
308
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
309
- return value as Record<string, unknown>;
289
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
290
+ return value as Record<string, unknown>;
310
291
  }