osdy-pi 1.2.0 → 1.3.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.
Files changed (44) hide show
  1. package/README.md +160 -30
  2. package/bin/osdy-pi.mjs +29 -6
  3. package/bin/osdy.mjs +43 -0
  4. package/extensions/osdy-pi/account-profiles.test.ts +111 -97
  5. package/extensions/osdy-pi/account-profiles.ts +91 -162
  6. package/extensions/osdy-pi/animation.test.ts +84 -0
  7. package/extensions/osdy-pi/animation.ts +17 -8
  8. package/extensions/osdy-pi/codex-usage-ui.test.ts +1145 -0
  9. package/extensions/osdy-pi/codex-usage-ui.ts +728 -0
  10. package/extensions/osdy-pi/codex-usage.test.ts +276 -0
  11. package/extensions/osdy-pi/codex-usage.ts +276 -0
  12. package/extensions/osdy-pi/constants.ts +134 -34
  13. package/extensions/osdy-pi/context-usage-ui.test.ts +168 -0
  14. package/extensions/osdy-pi/context-usage-ui.ts +60 -0
  15. package/extensions/osdy-pi/editor-settings.test.ts +121 -23
  16. package/extensions/osdy-pi/editor-settings.ts +39 -3
  17. package/extensions/osdy-pi/metrics.test.ts +95 -0
  18. package/extensions/osdy-pi/metrics.ts +39 -1
  19. package/extensions/osdy-pi/profile-label.ts +2 -2
  20. package/extensions/osdy-pi/runtime-helpers.test.ts +220 -1
  21. package/extensions/osdy-pi/runtime-helpers.ts +42 -19
  22. package/extensions/osdy-pi/runtime.test.ts +355 -7
  23. package/extensions/osdy-pi/runtime.ts +344 -66
  24. package/extensions/osdy-pi/types.ts +26 -1
  25. package/extensions/osdy-pi/ui.test.ts +128 -0
  26. package/extensions/osdy-pi/ui.ts +57 -40
  27. package/package.json +5 -2
  28. package/scripts/osdy-pi-account-profiles.mjs +521 -76
  29. package/scripts/osdy-pi-gentle-coexistence.mjs +200 -0
  30. package/scripts/osdy-pi-profile-setup.mjs +175 -0
  31. package/themes/osdy-pi-catppuccin-frappe.json +6 -2
  32. package/themes/osdy-pi-catppuccin-latte.json +6 -2
  33. package/themes/osdy-pi-catppuccin-macchiato.json +6 -2
  34. package/themes/osdy-pi-catppuccin-mocha.json +6 -2
  35. package/themes/osdy-pi-dark.json +6 -2
  36. package/themes/osdy-pi-dracula.json +6 -2
  37. package/themes/osdy-pi-kanagawa-dragon.json +6 -2
  38. package/themes/osdy-pi-kanagawa-lotus.json +6 -2
  39. package/themes/osdy-pi-kanagawa-wave.json +6 -2
  40. package/themes/osdy-pi-lucent-orange.json +6 -2
  41. package/themes/osdy-pi-matrix.json +6 -2
  42. package/themes/osdy-pi-new.json +6 -2
  43. package/themes/osdy-pi-sexy.json +6 -2
  44. package/themes/osdy-pi-tokyo-night.json +6 -2
@@ -0,0 +1,276 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { registerHooks } from "node:module";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import ts from "typescript";
7
+
8
+ registerHooks({
9
+ resolve(specifier, context, nextResolve) {
10
+ if (
11
+ specifier.startsWith(".") &&
12
+ specifier.endsWith(".js") &&
13
+ context.parentURL?.endsWith(".ts")
14
+ ) {
15
+ const sourceUrl = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
16
+ if (existsSync(fileURLToPath(sourceUrl))) {
17
+ return { shortCircuit: true, url: sourceUrl.href };
18
+ }
19
+ }
20
+ return nextResolve(specifier, context);
21
+ },
22
+ load(url, context, nextLoad) {
23
+ if (url.endsWith(".ts")) {
24
+ const source = readFileSync(fileURLToPath(url), "utf8");
25
+ return {
26
+ format: "module",
27
+ shortCircuit: true,
28
+ source: ts.transpileModule(source, {
29
+ compilerOptions: {
30
+ module: ts.ModuleKind.ESNext,
31
+ target: ts.ScriptTarget.ES2022,
32
+ },
33
+ }).outputText,
34
+ };
35
+ }
36
+ return nextLoad(url, context);
37
+ },
38
+ });
39
+
40
+ const { extractCodexAccountId, fetchCodexUsage, parseCodexUsagePayload } =
41
+ await import("./codex-usage.js");
42
+
43
+ function codexToken(accountId: string): string {
44
+ const encode = (value: unknown): string =>
45
+ Buffer.from(JSON.stringify(value)).toString("base64url");
46
+ return `${encode({ alg: "none" })}.${encode({
47
+ "https://api.openai.com/auth": { chatgpt_account_id: accountId },
48
+ })}.signature`;
49
+ }
50
+
51
+ const payload = {
52
+ plan_type: "plus",
53
+ ordinary_usage_allowed: true,
54
+ rate_limit: {
55
+ primary_window: {
56
+ used_percent: 37,
57
+ limit_window_seconds: 18_000,
58
+ reset_at: 2_000_000_000,
59
+ },
60
+ secondary_window: {
61
+ used_percent: 71,
62
+ limit_window_seconds: 604_800,
63
+ reset_at: 2_000_100_000,
64
+ },
65
+ },
66
+ credits: {
67
+ has_credits: true,
68
+ unlimited: false,
69
+ balance: "12.50",
70
+ reset_credit_count: 3,
71
+ },
72
+ additional_rate_limits: [
73
+ {
74
+ metered_feature: "codex_spark",
75
+ limit_name: "Spark",
76
+ rate_limit: {
77
+ primary_window: {
78
+ used_percent: 12,
79
+ limit_window_seconds: 3_600,
80
+ reset_at: 2_000_200_000,
81
+ },
82
+ },
83
+ },
84
+ ],
85
+ };
86
+
87
+ void test("parses subscription windows, credits, and additional buckets", () => {
88
+ const snapshot = parseCodexUsagePayload(payload, 1_900_000_000_000);
89
+
90
+ assert.equal(snapshot.planType, "plus");
91
+ assert.equal(snapshot.ordinaryUsageAllowed, true);
92
+ assert.equal(snapshot.fetchedAt, 1_900_000_000_000);
93
+ assert.deepEqual(snapshot.buckets[0], {
94
+ id: "codex",
95
+ label: undefined,
96
+ primary: {
97
+ usedPercent: 37,
98
+ windowMinutes: 300,
99
+ resetsAt: 2_000_000_000,
100
+ },
101
+ secondary: {
102
+ usedPercent: 71,
103
+ windowMinutes: 10_080,
104
+ resetsAt: 2_000_100_000,
105
+ },
106
+ });
107
+ assert.equal(snapshot.buckets[1]?.id, "codex_spark");
108
+ assert.equal(snapshot.buckets[1]?.label, "Spark");
109
+ assert.deepEqual(snapshot.credits, {
110
+ hasCredits: true,
111
+ unlimited: false,
112
+ balance: "12.50",
113
+ resetCreditCount: 3,
114
+ });
115
+ });
116
+
117
+ void test("accepts null additional rate limits as no additional buckets", () => {
118
+ const snapshot = parseCodexUsagePayload(
119
+ { ...payload, additional_rate_limits: null },
120
+ 1_900_000_000_000,
121
+ );
122
+
123
+ assert.deepEqual(snapshot.buckets, [
124
+ {
125
+ id: "codex",
126
+ label: undefined,
127
+ primary: {
128
+ usedPercent: 37,
129
+ windowMinutes: 300,
130
+ resetsAt: 2_000_000_000,
131
+ },
132
+ secondary: {
133
+ usedPercent: 71,
134
+ windowMinutes: 10_080,
135
+ resetsAt: 2_000_100_000,
136
+ },
137
+ },
138
+ ]);
139
+ });
140
+
141
+ void test("skips valid additional buckets without nested rate limits", () => {
142
+ for (const rate_limit of [undefined, null]) {
143
+ const snapshot = parseCodexUsagePayload({
144
+ ...payload,
145
+ additional_rate_limits: [{ metered_feature: "codex_spark", rate_limit }],
146
+ });
147
+
148
+ assert.equal(snapshot.buckets.length, 1);
149
+ }
150
+ });
151
+
152
+ void test("rejects invalid additional rate limit containers", () => {
153
+ for (const additional_rate_limits of ["invalid", {}, 1, ["invalid"]]) {
154
+ assert.throws(
155
+ () => parseCodexUsagePayload({ ...payload, additional_rate_limits }),
156
+ /additional(?:_| )rate.limit/i,
157
+ );
158
+ }
159
+ });
160
+
161
+ void test("rejects additional buckets without a non-empty metered feature", () => {
162
+ for (const entry of [
163
+ {},
164
+ { metered_feature: null },
165
+ { metered_feature: "" },
166
+ { metered_feature: 1 },
167
+ { metered_feature: {} },
168
+ ]) {
169
+ assert.throws(
170
+ () =>
171
+ parseCodexUsagePayload({
172
+ ...payload,
173
+ additional_rate_limits: [entry],
174
+ }),
175
+ /metered_feature/i,
176
+ );
177
+ }
178
+ });
179
+
180
+ void test("rejects malformed quota payloads without guessing", () => {
181
+ assert.throws(
182
+ () => parseCodexUsagePayload({ plan_type: "plus", rate_limit: {} }),
183
+ /does not contain any quota windows/i,
184
+ );
185
+ assert.throws(
186
+ () =>
187
+ parseCodexUsagePayload({
188
+ plan_type: "plus",
189
+ rate_limit: { primary_window: { used_percent: "37" } },
190
+ }),
191
+ /used_percent/i,
192
+ );
193
+ });
194
+
195
+ void test("extracts the current ChatGPT account id without exposing the token", () => {
196
+ assert.equal(extractCodexAccountId(codexToken("acct_123")), "acct_123");
197
+ assert.throws(() => extractCodexAccountId("not-a-token"), /account id/i);
198
+ });
199
+
200
+ void test("fetches usage with bounded, non-redirecting authenticated request", async () => {
201
+ const accessToken = codexToken("acct_123");
202
+ let receivedUrl: string | undefined;
203
+ let receivedInit: RequestInit | undefined;
204
+ const fakeFetch: typeof fetch = (url, init) => {
205
+ receivedUrl =
206
+ typeof url === "string"
207
+ ? url
208
+ : url instanceof URL
209
+ ? url.toString()
210
+ : url.url;
211
+ receivedInit = init;
212
+ return Promise.resolve(
213
+ new Response(JSON.stringify(payload), {
214
+ status: 200,
215
+ headers: { "content-type": "application/json" },
216
+ }),
217
+ );
218
+ };
219
+
220
+ const snapshot = await fetchCodexUsage(
221
+ { accessToken, accountId: "acct_123" },
222
+ { fetch: fakeFetch, now: () => 1_900_000_000_000 },
223
+ );
224
+
225
+ assert.equal(receivedUrl, "https://chatgpt.com/backend-api/wham/usage");
226
+ assert.equal(receivedInit?.method, "GET");
227
+ assert.equal(receivedInit?.redirect, "error");
228
+ const headers = new Headers(receivedInit?.headers);
229
+ assert.equal(headers.get("authorization"), `Bearer ${accessToken}`);
230
+ assert.equal(headers.get("chatgpt-account-id"), "acct_123");
231
+ assert.equal(headers.get("originator"), "pi");
232
+ assert.equal(headers.get("accept"), "application/json");
233
+ assert.match(headers.get("user-agent") ?? "", /^pi \(.+; .+\)$/);
234
+ assert.equal(snapshot.planType, "plus");
235
+ });
236
+
237
+ void test("reports expired Codex sessions on 401 without exposing response bodies or tokens", async () => {
238
+ const accessToken = "secret-access-token";
239
+ const fakeFetch: typeof fetch = () =>
240
+ Promise.resolve(new Response("sensitive upstream response", { status: 401 }));
241
+
242
+ await assert.rejects(
243
+ fetchCodexUsage({ accessToken, accountId: "acct_123" }, { fetch: fakeFetch }),
244
+ (error: unknown) => {
245
+ assert(error instanceof Error);
246
+ assert.match(
247
+ error.message,
248
+ /session expired.*\/login|\/login.*session expired/i,
249
+ );
250
+ assert.doesNotMatch(
251
+ error.message,
252
+ /sensitive upstream response|secret-access-token/i,
253
+ );
254
+ return true;
255
+ },
256
+ );
257
+ });
258
+
259
+ void test("reports unavailable usage access on 403 without claiming the session is logged out", async () => {
260
+ const accessToken = "secret-access-token";
261
+ const fakeFetch: typeof fetch = () =>
262
+ Promise.resolve(new Response("sensitive upstream response", { status: 403 }));
263
+
264
+ await assert.rejects(
265
+ fetchCodexUsage({ accessToken, accountId: "acct_123" }, { fetch: fakeFetch }),
266
+ (error: unknown) => {
267
+ assert(error instanceof Error);
268
+ assert.match(error.message, /usage access.*unavailable|forbidden/i);
269
+ assert.doesNotMatch(
270
+ error.message,
271
+ /login|logged out|sensitive upstream response|secret-access-token/i,
272
+ );
273
+ return true;
274
+ },
275
+ );
276
+ });
@@ -0,0 +1,276 @@
1
+ import { arch, platform, release } from "node:os";
2
+
3
+ export type CodexUsageWindow = {
4
+ usedPercent: number;
5
+ windowMinutes: number | undefined;
6
+ resetsAt: number | undefined;
7
+ };
8
+
9
+ export type CodexUsageBucket = {
10
+ id: string;
11
+ label: string | undefined;
12
+ primary: CodexUsageWindow | undefined;
13
+ secondary: CodexUsageWindow | undefined;
14
+ };
15
+
16
+ export type CodexUsageCredits = {
17
+ hasCredits: boolean;
18
+ unlimited: boolean;
19
+ balance: string | undefined;
20
+ resetCreditCount: number | undefined;
21
+ };
22
+
23
+ export type CodexUsageSnapshot = {
24
+ planType: string | undefined;
25
+ ordinaryUsageAllowed: boolean | undefined;
26
+ buckets: CodexUsageBucket[];
27
+ credits: CodexUsageCredits | undefined;
28
+ fetchedAt: number;
29
+ };
30
+
31
+ export type CodexUsageAuth = {
32
+ accessToken: string;
33
+ accountId: string;
34
+ };
35
+
36
+ type FetchCodexUsageOptions = {
37
+ fetch?: typeof globalThis.fetch;
38
+ now?: () => number;
39
+ signal?: AbortSignal;
40
+ timeoutMs?: number;
41
+ };
42
+
43
+ const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
44
+ const MAX_RESPONSE_BYTES = 1_000_000;
45
+ const DEFAULT_TIMEOUT_MS = 10_000;
46
+
47
+ type UnknownRecord = Record<string, unknown>;
48
+
49
+ function createPiUserAgent(): string {
50
+ return `pi (${platform()} ${release()}; ${arch()})`;
51
+ }
52
+
53
+ function asRecord(value: unknown, label: string): UnknownRecord {
54
+ if (!value || typeof value !== "object" || Array.isArray(value))
55
+ throw new Error(`Codex usage ${label} is invalid`);
56
+ return value as UnknownRecord;
57
+ }
58
+
59
+ function optionalString(value: unknown, label: string): string | undefined {
60
+ if (value === undefined || value === null) return undefined;
61
+ if (typeof value !== "string")
62
+ throw new Error(`Codex usage ${label} is invalid`);
63
+ return value;
64
+ }
65
+
66
+ function optionalBoolean(value: unknown, label: string): boolean | undefined {
67
+ if (value === undefined || value === null) return undefined;
68
+ if (typeof value !== "boolean")
69
+ throw new Error(`Codex usage ${label} is invalid`);
70
+ return value;
71
+ }
72
+
73
+ function optionalNumber(value: unknown, label: string): number | undefined {
74
+ if (value === undefined || value === null) return undefined;
75
+ if (typeof value !== "number" || !Number.isFinite(value))
76
+ throw new Error(`Codex usage ${label} is invalid`);
77
+ return value;
78
+ }
79
+
80
+ function parseWindow(
81
+ value: unknown,
82
+ label: string,
83
+ ): CodexUsageWindow | undefined {
84
+ if (value === undefined || value === null) return undefined;
85
+ const window = asRecord(value, label);
86
+ const usedPercent = window.used_percent;
87
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent))
88
+ throw new Error("Codex usage used_percent is invalid");
89
+ if (usedPercent < 0 || usedPercent > 100)
90
+ throw new Error("Codex usage used_percent is out of range");
91
+ const seconds = window.limit_window_seconds;
92
+ if (
93
+ seconds !== undefined &&
94
+ (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0)
95
+ )
96
+ throw new Error("Codex usage limit_window_seconds is invalid");
97
+ const resetAt = window.reset_at;
98
+ if (
99
+ resetAt !== undefined &&
100
+ (typeof resetAt !== "number" || !Number.isFinite(resetAt))
101
+ )
102
+ throw new Error("Codex usage reset_at is invalid");
103
+ return {
104
+ usedPercent,
105
+ windowMinutes:
106
+ typeof seconds === "number" ? Math.round(seconds / 60) : undefined,
107
+ resetsAt: typeof resetAt === "number" ? resetAt : undefined,
108
+ };
109
+ }
110
+
111
+ function parseBucket(value: unknown): CodexUsageBucket | undefined {
112
+ const bucket = asRecord(value, "additional rate limit");
113
+ const id = bucket.metered_feature;
114
+ if (typeof id !== "string" || id.trim().length === 0)
115
+ throw new Error("Codex usage metered_feature is invalid");
116
+ const rateLimit = bucket.rate_limit;
117
+ if (rateLimit === undefined || rateLimit === null) return undefined;
118
+ const rate = asRecord(rateLimit, "additional rate limit");
119
+ const primary = parseWindow(rate.primary_window, "primary_window");
120
+ const secondary = parseWindow(rate.secondary_window, "secondary_window");
121
+ if (!primary && !secondary) return undefined;
122
+ return {
123
+ id,
124
+ label: optionalString(bucket.limit_name, "limit_name"),
125
+ primary,
126
+ secondary,
127
+ };
128
+ }
129
+
130
+ export function parseCodexUsagePayload(
131
+ input: unknown,
132
+ fetchedAt = Date.now(),
133
+ ): CodexUsageSnapshot {
134
+ const payload = asRecord(input, "payload");
135
+ const rateLimit = asRecord(payload.rate_limit, "rate_limit");
136
+ const primary = parseWindow(rateLimit.primary_window, "primary_window");
137
+ const secondary = parseWindow(rateLimit.secondary_window, "secondary_window");
138
+ if (!primary && !secondary)
139
+ throw new Error("Codex usage does not contain any quota windows");
140
+ const additional = payload.additional_rate_limits;
141
+ if (
142
+ additional !== undefined &&
143
+ additional !== null &&
144
+ !Array.isArray(additional)
145
+ )
146
+ throw new Error("Codex usage additional_rate_limits is invalid");
147
+ const buckets: CodexUsageBucket[] = [
148
+ { id: "codex", label: undefined, primary, secondary },
149
+ ];
150
+ for (const bucket of additional ?? []) {
151
+ const parsed = parseBucket(bucket);
152
+ if (parsed) buckets.push(parsed);
153
+ }
154
+ const creditsValue = payload.credits;
155
+ let credits: CodexUsageCredits | undefined;
156
+ if (creditsValue !== undefined && creditsValue !== null) {
157
+ const rawCredits = asRecord(creditsValue, "credits");
158
+ const hasCredits = optionalBoolean(rawCredits.has_credits, "has_credits");
159
+ const unlimited = optionalBoolean(rawCredits.unlimited, "unlimited");
160
+ if (hasCredits === undefined || unlimited === undefined)
161
+ throw new Error("Codex usage credits are invalid");
162
+ credits = {
163
+ hasCredits,
164
+ unlimited,
165
+ balance: optionalString(rawCredits.balance, "credit balance"),
166
+ resetCreditCount: optionalNumber(
167
+ rawCredits.reset_credit_count,
168
+ "reset_credit_count",
169
+ ),
170
+ };
171
+ }
172
+ return {
173
+ planType: optionalString(payload.plan_type, "plan_type"),
174
+ ordinaryUsageAllowed: optionalBoolean(
175
+ payload.ordinary_usage_allowed,
176
+ "ordinary_usage_allowed",
177
+ ),
178
+ buckets,
179
+ credits,
180
+ fetchedAt,
181
+ };
182
+ }
183
+
184
+ export function extractCodexAccountId(accessToken: string): string {
185
+ const payload = accessToken.split(".")[1];
186
+ if (!payload) throw new Error("Codex account ID is unavailable");
187
+ try {
188
+ const decoded = JSON.parse(
189
+ Buffer.from(payload, "base64url").toString("utf8"),
190
+ ) as unknown;
191
+ const token = asRecord(decoded, "token");
192
+ const auth = asRecord(token["https://api.openai.com/auth"], "token auth");
193
+ const accountId = auth.chatgpt_account_id;
194
+ if (typeof accountId !== "string" || accountId.length === 0)
195
+ throw new Error("Codex account ID is unavailable");
196
+ return accountId;
197
+ } catch {
198
+ throw new Error("Codex account ID is unavailable");
199
+ }
200
+ }
201
+
202
+ async function readBoundedJson(response: Response): Promise<unknown> {
203
+ const length = response.headers.get("content-length");
204
+ if (
205
+ length !== null &&
206
+ (!/^\d+$/.test(length) || Number(length) > MAX_RESPONSE_BYTES)
207
+ )
208
+ throw new Error("Codex usage response is too large");
209
+ if (!response.body) throw new Error("Codex usage response is unavailable");
210
+ const reader = response.body.getReader();
211
+ const chunks: Uint8Array[] = [];
212
+ let size = 0;
213
+ try {
214
+ while (true) {
215
+ const next = await reader.read();
216
+ if (next.done) break;
217
+ size += next.value.byteLength;
218
+ if (size > MAX_RESPONSE_BYTES)
219
+ throw new Error("Codex usage response is too large");
220
+ chunks.push(next.value);
221
+ }
222
+ } finally {
223
+ reader.releaseLock();
224
+ }
225
+ const merged = new Uint8Array(size);
226
+ let offset = 0;
227
+ for (const chunk of chunks) {
228
+ merged.set(chunk, offset);
229
+ offset += chunk.byteLength;
230
+ }
231
+ try {
232
+ return JSON.parse(new TextDecoder().decode(merged)) as unknown;
233
+ } catch {
234
+ throw new Error("Codex usage response is invalid");
235
+ }
236
+ }
237
+
238
+ export async function fetchCodexUsage(
239
+ auth: CodexUsageAuth,
240
+ options: FetchCodexUsageOptions = {},
241
+ ): Promise<CodexUsageSnapshot> {
242
+ const fetcher = options.fetch ?? globalThis.fetch;
243
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
244
+ const signal = options.signal
245
+ ? AbortSignal.any([options.signal, timeout])
246
+ : timeout;
247
+ let response: Response;
248
+ try {
249
+ response = await fetcher(USAGE_URL, {
250
+ method: "GET",
251
+ headers: {
252
+ accept: "application/json",
253
+ authorization: `Bearer ${auth.accessToken}`,
254
+ "chatgpt-account-id": auth.accountId,
255
+ originator: "pi",
256
+ "user-agent": createPiUserAgent(),
257
+ },
258
+ redirect: "error",
259
+ signal,
260
+ });
261
+ } catch (error) {
262
+ if (signal.aborted)
263
+ throw new Error("Codex usage request timed out or was cancelled");
264
+ void error;
265
+ throw new Error("Codex usage is temporarily unavailable");
266
+ }
267
+ if (response.status === 401)
268
+ throw new Error("Codex session expired; use /login to sign in again");
269
+ if (response.status === 403)
270
+ throw new Error("Codex usage access is unavailable or forbidden");
271
+ if (!response.ok) throw new Error("Codex usage is temporarily unavailable");
272
+ return parseCodexUsagePayload(
273
+ await readBoundedJson(response),
274
+ (options.now ?? Date.now)(),
275
+ );
276
+ }