@narumitw/pi-usage 0.60.7 → 0.60.9
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 +1 -1
- package/dist/index.ts +91 -340
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +73 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { sanitizeDisplayText } from "../core.js";
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
+
return typeof value === "string" && ACCOUNT_ID_PATTERN.test(value);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayload): string[] {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
payload: FireworksBillingSummaryPayload,
|
|
132
|
+
accountId: string,
|
|
133
|
+
capturedAt: number,
|
|
136
134
|
): UsageReport {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
289
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
290
|
+
return value as Record<string, unknown>;
|
|
310
291
|
}
|