@narumitw/pi-codex-usage 0.13.0 → 0.13.1
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 +3 -2
- package/package.json +1 -1
- package/src/app-server-client.ts +216 -0
- package/src/codex-usage.ts +27 -898
- package/src/format.ts +264 -0
- package/src/normalize.ts +210 -0
- package/src/query.ts +179 -0
- package/src/types.ts +121 -0
package/src/codex-usage.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
1
|
import type {
|
|
4
2
|
ExtensionAPI,
|
|
5
3
|
ExtensionCommandContext,
|
|
6
4
|
ExtensionContext,
|
|
7
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
formatCodexUsageReport,
|
|
8
|
+
formatCodexUsageStatusline,
|
|
9
|
+
formatQueryErrors,
|
|
10
|
+
showReport,
|
|
11
|
+
} from "./format.js";
|
|
12
|
+
import {
|
|
13
|
+
isOpenAICodexModel,
|
|
14
|
+
isStaleExtensionContextError,
|
|
15
|
+
queryUsage,
|
|
16
|
+
} from "./query.js";
|
|
17
|
+
import type {
|
|
18
|
+
CachedReport,
|
|
19
|
+
CodexUsageModel,
|
|
20
|
+
CodexUsageReport,
|
|
21
|
+
QueryUsageOptions,
|
|
22
|
+
} from "./types.js";
|
|
8
23
|
|
|
9
24
|
const COMMAND_NAME = "codex-status";
|
|
10
|
-
const CODEX_PROVIDER_ID = "openai-codex";
|
|
11
|
-
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
12
25
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
26
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
14
27
|
const STATUS_KEY = "codex-usage";
|
|
15
|
-
const USAGE_SETTINGS_URL = "https://chatgpt.com/codex/settings/usage";
|
|
16
|
-
const BAR_SEGMENTS = 20;
|
|
17
|
-
const LIMIT_VALUE_COLUMN = 29;
|
|
18
|
-
const MAX_ERROR_BODY_CHARS = 600;
|
|
19
|
-
const RESET_FOREGROUND = "\x1b[39m";
|
|
20
28
|
|
|
21
29
|
interface CommandArgumentCompletion {
|
|
22
30
|
value: string;
|
|
@@ -35,126 +43,6 @@ const COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
|
35
43
|
{ value: "--timeout ", label: "--timeout", description: "Set query timeout in seconds" },
|
|
36
44
|
];
|
|
37
45
|
|
|
38
|
-
type UsageSource = "pi-auth" | "codex-app-server";
|
|
39
|
-
type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
40
|
-
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
41
|
-
|
|
42
|
-
type QueryUsageOptions = {
|
|
43
|
-
clearStatusline: boolean;
|
|
44
|
-
refresh: boolean;
|
|
45
|
-
statusline: boolean;
|
|
46
|
-
timeoutMs: number;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type CachedReport = {
|
|
50
|
-
createdAt: number;
|
|
51
|
-
report: CodexUsageReport;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type QueryUsageResult =
|
|
55
|
-
| { ok: true; report: CodexUsageReport }
|
|
56
|
-
| { ok: false; errors: UsageQueryError[] };
|
|
57
|
-
|
|
58
|
-
type UsageQueryError = {
|
|
59
|
-
source: UsageSource;
|
|
60
|
-
message: string;
|
|
61
|
-
cause?: unknown;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export type CodexUsageReport = {
|
|
65
|
-
source: UsageSource;
|
|
66
|
-
capturedAt: number;
|
|
67
|
-
planType?: string;
|
|
68
|
-
snapshots: NormalizedRateLimitSnapshot[];
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export type NormalizedRateLimitSnapshot = {
|
|
72
|
-
limitId: string;
|
|
73
|
-
limitName?: string;
|
|
74
|
-
primary?: NormalizedRateLimitWindow;
|
|
75
|
-
secondary?: NormalizedRateLimitWindow;
|
|
76
|
-
credits?: NormalizedCredits;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export type NormalizedRateLimitWindow = {
|
|
80
|
-
usedPercent: number;
|
|
81
|
-
windowMinutes?: number;
|
|
82
|
-
resetsAt?: number;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export type NormalizedCredits = {
|
|
86
|
-
hasCredits: boolean;
|
|
87
|
-
unlimited: boolean;
|
|
88
|
-
balance?: string;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
type RateLimitStatusPayload = {
|
|
92
|
-
plan_type?: unknown;
|
|
93
|
-
rate_limit?: unknown;
|
|
94
|
-
additional_rate_limits?: unknown;
|
|
95
|
-
credits?: unknown;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
type BackendRateLimitDetails = {
|
|
99
|
-
primary_window?: unknown;
|
|
100
|
-
secondary_window?: unknown;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
type BackendWindowSnapshot = {
|
|
104
|
-
used_percent?: unknown;
|
|
105
|
-
limit_window_seconds?: unknown;
|
|
106
|
-
reset_at?: unknown;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
type BackendAdditionalRateLimit = {
|
|
110
|
-
limit_name?: unknown;
|
|
111
|
-
metered_feature?: unknown;
|
|
112
|
-
rate_limit?: unknown;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
type BackendCreditsSnapshot = {
|
|
116
|
-
has_credits?: unknown;
|
|
117
|
-
unlimited?: unknown;
|
|
118
|
-
balance?: unknown;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
type AppServerRateLimitResponse = {
|
|
122
|
-
rateLimits?: unknown;
|
|
123
|
-
rateLimitsByLimitId?: unknown;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
type AppServerRateLimitSnapshot = {
|
|
127
|
-
limitId?: unknown;
|
|
128
|
-
limitName?: unknown;
|
|
129
|
-
primary?: unknown;
|
|
130
|
-
secondary?: unknown;
|
|
131
|
-
credits?: unknown;
|
|
132
|
-
planType?: unknown;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
type AppServerWindowSnapshot = {
|
|
136
|
-
usedPercent?: unknown;
|
|
137
|
-
windowDurationMins?: unknown;
|
|
138
|
-
resetsAt?: unknown;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
type AppServerCreditsSnapshot = {
|
|
142
|
-
hasCredits?: unknown;
|
|
143
|
-
unlimited?: unknown;
|
|
144
|
-
balance?: unknown;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
type RpcResponse = {
|
|
148
|
-
id?: unknown;
|
|
149
|
-
result?: unknown;
|
|
150
|
-
error?: { message?: unknown; code?: unknown };
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
type PendingRpc = {
|
|
154
|
-
resolve: (value: unknown) => void;
|
|
155
|
-
reject: (error: Error) => void;
|
|
156
|
-
};
|
|
157
|
-
|
|
158
46
|
export default function codexUsage(pi: ExtensionAPI) {
|
|
159
47
|
let cache: CachedReport | undefined;
|
|
160
48
|
let statuslineClearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -438,772 +326,13 @@ export function parseArgs(
|
|
|
438
326
|
return { ok: true, value: { clearStatusline, refresh, statusline, timeoutMs } };
|
|
439
327
|
}
|
|
440
328
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function queryUsage(
|
|
453
|
-
ctx: ExtensionContext,
|
|
454
|
-
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
455
|
-
): Promise<QueryUsageResult> {
|
|
456
|
-
const errors: UsageQueryError[] = [];
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
const report = await queryViaPiAuth(ctx, options.timeoutMs);
|
|
460
|
-
return { ok: true, report };
|
|
461
|
-
} catch (cause) {
|
|
462
|
-
if (isStaleExtensionContextError(cause)) throw cause;
|
|
463
|
-
errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
const report = await queryViaCodexAppServer(options.timeoutMs);
|
|
468
|
-
return { ok: true, report };
|
|
469
|
-
} catch (cause) {
|
|
470
|
-
if (isStaleExtensionContextError(cause)) throw cause;
|
|
471
|
-
errors.push({ source: "codex-app-server", message: errorMessage(cause), cause });
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return { ok: false, errors };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function queryViaPiAuth(
|
|
478
|
-
ctx: ExtensionContext,
|
|
479
|
-
timeoutMs: number,
|
|
480
|
-
): Promise<CodexUsageReport> {
|
|
481
|
-
const auth = await resolvePiCodexAuth(ctx);
|
|
482
|
-
if (!auth) {
|
|
483
|
-
throw new Error(
|
|
484
|
-
"No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const response = await fetchWithTimeout(CODEX_USAGE_URL, { headers: auth.headers }, timeoutMs);
|
|
489
|
-
const text = await response.text();
|
|
490
|
-
if (!response.ok) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`Codex usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const payload = parseJsonObject(text, "Codex usage endpoint response");
|
|
497
|
-
return normalizeBackendPayload(payload as RateLimitStatusPayload, Date.now(), "pi-auth");
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function resolvePiCodexAuth(
|
|
501
|
-
ctx: ExtensionContext,
|
|
502
|
-
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
503
|
-
const models = codexAuthCandidateModels(ctx);
|
|
504
|
-
const errors: string[] = [];
|
|
505
|
-
|
|
506
|
-
for (const model of models) {
|
|
507
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
508
|
-
if (!auth.ok) {
|
|
509
|
-
errors.push(auth.error);
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const headers = { ...(auth.headers ?? {}) };
|
|
514
|
-
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
515
|
-
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
516
|
-
}
|
|
517
|
-
if (!hasHeader(headers, "User-Agent")) {
|
|
518
|
-
headers["User-Agent"] = "pi-codex-usage";
|
|
519
|
-
}
|
|
520
|
-
if (hasHeader(headers, "Authorization")) {
|
|
521
|
-
return { headers };
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
if (errors.length > 0) {
|
|
526
|
-
throw new Error(errors.join("; "));
|
|
527
|
-
}
|
|
528
|
-
return undefined;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
532
|
-
const candidates: PiModel[] = [];
|
|
533
|
-
const seen = new Set<string>();
|
|
534
|
-
const add = (model: PiModel | undefined) => {
|
|
535
|
-
if (!model || model.provider !== CODEX_PROVIDER_ID) return;
|
|
536
|
-
const key = `${model.provider}/${model.id}`;
|
|
537
|
-
if (seen.has(key)) return;
|
|
538
|
-
seen.add(key);
|
|
539
|
-
candidates.push(model);
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
add(ctx.model);
|
|
543
|
-
for (const model of ctx.modelRegistry.getAvailable()) add(model);
|
|
544
|
-
for (const model of ctx.modelRegistry.getAll()) add(model);
|
|
545
|
-
return candidates;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async function fetchWithTimeout(
|
|
549
|
-
url: string,
|
|
550
|
-
init: RequestInit,
|
|
551
|
-
timeoutMs: number,
|
|
552
|
-
): Promise<Response> {
|
|
553
|
-
const controller = new AbortController();
|
|
554
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
555
|
-
try {
|
|
556
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
557
|
-
} catch (error) {
|
|
558
|
-
if (controller.signal.aborted) {
|
|
559
|
-
throw new Error(
|
|
560
|
-
`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
throw error;
|
|
564
|
-
} finally {
|
|
565
|
-
clearTimeout(timeout);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function queryViaCodexAppServer(timeoutMs: number): Promise<CodexUsageReport> {
|
|
570
|
-
const client = new CodexAppServerClient(timeoutMs);
|
|
571
|
-
try {
|
|
572
|
-
await client.start();
|
|
573
|
-
await client.request("initialize", {
|
|
574
|
-
clientInfo: {
|
|
575
|
-
name: "pi_codex_usage",
|
|
576
|
-
title: "Pi Codex Usage",
|
|
577
|
-
version: "0.1.0",
|
|
578
|
-
},
|
|
579
|
-
capabilities: {
|
|
580
|
-
experimentalApi: false,
|
|
581
|
-
requestAttestation: false,
|
|
582
|
-
optOutNotificationMethods: [],
|
|
583
|
-
},
|
|
584
|
-
});
|
|
585
|
-
client.notify("initialized");
|
|
586
|
-
const result = await client.request("account/rateLimits/read", undefined);
|
|
587
|
-
return normalizeAppServerResponse(
|
|
588
|
-
assertObject(result, "account/rateLimits/read result") as AppServerRateLimitResponse,
|
|
589
|
-
Date.now(),
|
|
590
|
-
);
|
|
591
|
-
} finally {
|
|
592
|
-
client.dispose();
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
class CodexAppServerClient {
|
|
597
|
-
private child?: ChildProcessWithoutNullStreams;
|
|
598
|
-
private nextId = 1;
|
|
599
|
-
private stderr = "";
|
|
600
|
-
private readonly pending = new Map<number, PendingRpc>();
|
|
601
|
-
private startPromise?: Promise<void>;
|
|
602
|
-
private exitError?: Error;
|
|
603
|
-
private readonly timeoutMs: number;
|
|
604
|
-
|
|
605
|
-
constructor(timeoutMs: number) {
|
|
606
|
-
this.timeoutMs = timeoutMs;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
start(): Promise<void> {
|
|
610
|
-
if (this.startPromise) return this.startPromise;
|
|
611
|
-
|
|
612
|
-
this.startPromise = new Promise((resolve, reject) => {
|
|
613
|
-
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
614
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
615
|
-
});
|
|
616
|
-
this.child = child;
|
|
617
|
-
|
|
618
|
-
const startupTimeout = setTimeout(() => {
|
|
619
|
-
reject(
|
|
620
|
-
new Error(
|
|
621
|
-
`Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
|
|
622
|
-
),
|
|
623
|
-
);
|
|
624
|
-
}, this.timeoutMs);
|
|
625
|
-
|
|
626
|
-
child.once("spawn", () => {
|
|
627
|
-
clearTimeout(startupTimeout);
|
|
628
|
-
resolve();
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
child.once("error", (error) => {
|
|
632
|
-
clearTimeout(startupTimeout);
|
|
633
|
-
reject(new Error(`Failed to start codex app-server: ${error.message}`));
|
|
634
|
-
this.rejectAll(error);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
child.once("exit", (code, signal) => {
|
|
638
|
-
const suffix = this.stderr ? ` stderr: ${redactErrorBody(this.stderr)}` : "";
|
|
639
|
-
this.exitError = new Error(
|
|
640
|
-
`codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
|
|
641
|
-
);
|
|
642
|
-
this.rejectAll(this.exitError);
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
child.stderr.setEncoding("utf8");
|
|
646
|
-
child.stderr.on("data", (chunk: string) => {
|
|
647
|
-
this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
const lines = createInterface({ input: child.stdout });
|
|
651
|
-
lines.on("line", (line) => this.handleLine(line));
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
return this.startPromise;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
request(method: string, params: unknown): Promise<unknown> {
|
|
658
|
-
const child = this.child;
|
|
659
|
-
if (!child?.stdin.writable) {
|
|
660
|
-
throw new Error("codex app-server is not running.");
|
|
661
|
-
}
|
|
662
|
-
if (this.exitError) throw this.exitError;
|
|
663
|
-
|
|
664
|
-
const id = this.nextId++;
|
|
665
|
-
const payload = params === undefined ? { method, id } : { method, id, params };
|
|
666
|
-
const response = new Promise<unknown>((resolve, reject) => {
|
|
667
|
-
const timeout = setTimeout(() => {
|
|
668
|
-
this.pending.delete(id);
|
|
669
|
-
reject(
|
|
670
|
-
new Error(`Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`),
|
|
671
|
-
);
|
|
672
|
-
}, this.timeoutMs);
|
|
673
|
-
|
|
674
|
-
this.pending.set(id, {
|
|
675
|
-
resolve: (value) => {
|
|
676
|
-
clearTimeout(timeout);
|
|
677
|
-
resolve(value);
|
|
678
|
-
},
|
|
679
|
-
reject: (error) => {
|
|
680
|
-
clearTimeout(timeout);
|
|
681
|
-
reject(error);
|
|
682
|
-
},
|
|
683
|
-
});
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
687
|
-
return response;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
notify(method: string): void {
|
|
691
|
-
const child = this.child;
|
|
692
|
-
if (!child?.stdin.writable) return;
|
|
693
|
-
child.stdin.write(`${JSON.stringify({ method })}\n`);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
dispose(): void {
|
|
697
|
-
for (const [id, pending] of this.pending) {
|
|
698
|
-
pending.reject(new Error(`codex app-server request ${id} cancelled.`));
|
|
699
|
-
}
|
|
700
|
-
this.pending.clear();
|
|
701
|
-
|
|
702
|
-
const child = this.child;
|
|
703
|
-
if (!child) return;
|
|
704
|
-
child.stdin.end();
|
|
705
|
-
if (!child.killed) child.kill();
|
|
706
|
-
this.child = undefined;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
private handleLine(line: string): void {
|
|
710
|
-
let parsed: RpcResponse;
|
|
711
|
-
try {
|
|
712
|
-
parsed = JSON.parse(line) as RpcResponse;
|
|
713
|
-
} catch {
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (typeof parsed.id !== "number") return;
|
|
718
|
-
const pending = this.pending.get(parsed.id);
|
|
719
|
-
if (!pending) return;
|
|
720
|
-
this.pending.delete(parsed.id);
|
|
721
|
-
|
|
722
|
-
if (parsed.error) {
|
|
723
|
-
const message =
|
|
724
|
-
typeof parsed.error.message === "string" ? parsed.error.message : "unknown error";
|
|
725
|
-
pending.reject(new Error(`codex app-server request failed: ${message}`));
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
pending.resolve(parsed.result);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private rejectAll(error: Error): void {
|
|
733
|
-
for (const pending of this.pending.values()) pending.reject(error);
|
|
734
|
-
this.pending.clear();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
export function normalizeBackendPayload(
|
|
739
|
-
payload: RateLimitStatusPayload,
|
|
740
|
-
capturedAt: number,
|
|
741
|
-
source: UsageSource,
|
|
742
|
-
): CodexUsageReport {
|
|
743
|
-
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
744
|
-
const planType = asString(payload.plan_type);
|
|
745
|
-
const primary = normalizeBackendSnapshot("codex", undefined, payload.rate_limit, payload.credits);
|
|
746
|
-
if (primary) snapshots.push(primary);
|
|
747
|
-
|
|
748
|
-
const additional = Array.isArray(payload.additional_rate_limits)
|
|
749
|
-
? payload.additional_rate_limits
|
|
750
|
-
: [];
|
|
751
|
-
for (const item of additional) {
|
|
752
|
-
const additionalLimit = assertObject(
|
|
753
|
-
item,
|
|
754
|
-
"additional rate limit",
|
|
755
|
-
) as BackendAdditionalRateLimit;
|
|
756
|
-
const limitId =
|
|
757
|
-
asString(additionalLimit.metered_feature) ?? asString(additionalLimit.limit_name);
|
|
758
|
-
if (!limitId) continue;
|
|
759
|
-
const snapshot = normalizeBackendSnapshot(
|
|
760
|
-
limitId,
|
|
761
|
-
asString(additionalLimit.limit_name),
|
|
762
|
-
additionalLimit.rate_limit,
|
|
763
|
-
undefined,
|
|
764
|
-
);
|
|
765
|
-
if (snapshot) snapshots.push(snapshot);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (snapshots.length === 0) {
|
|
769
|
-
throw new Error("Codex usage endpoint returned no displayable rate-limit windows.");
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return { source, capturedAt, planType, snapshots };
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function normalizeBackendSnapshot(
|
|
776
|
-
limitId: string,
|
|
777
|
-
limitName: string | undefined,
|
|
778
|
-
rateLimit: unknown,
|
|
779
|
-
credits: unknown,
|
|
780
|
-
): NormalizedRateLimitSnapshot | undefined {
|
|
781
|
-
if (rateLimit === null || rateLimit === undefined) {
|
|
782
|
-
const normalizedCredits = normalizeBackendCredits(credits);
|
|
783
|
-
return normalizedCredits ? { limitId, limitName, credits: normalizedCredits } : undefined;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const details = assertObject(rateLimit, "rate limit") as BackendRateLimitDetails;
|
|
787
|
-
const primary = normalizeBackendWindow(details.primary_window);
|
|
788
|
-
const secondary = normalizeBackendWindow(details.secondary_window);
|
|
789
|
-
const normalizedCredits = normalizeBackendCredits(credits);
|
|
790
|
-
|
|
791
|
-
if (!primary && !secondary && !normalizedCredits) return undefined;
|
|
792
|
-
return { limitId, limitName, primary, secondary, credits: normalizedCredits };
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function normalizeBackendWindow(value: unknown): NormalizedRateLimitWindow | undefined {
|
|
796
|
-
if (value === null || value === undefined) return undefined;
|
|
797
|
-
const window = assertObject(value, "rate-limit window") as BackendWindowSnapshot;
|
|
798
|
-
const usedPercent = asNumber(window.used_percent);
|
|
799
|
-
if (usedPercent === undefined) return undefined;
|
|
800
|
-
const limitSeconds = asNumber(window.limit_window_seconds);
|
|
801
|
-
const resetsAt = asNumber(window.reset_at);
|
|
802
|
-
return {
|
|
803
|
-
usedPercent,
|
|
804
|
-
windowMinutes: limitSeconds && limitSeconds > 0 ? Math.ceil(limitSeconds / 60) : undefined,
|
|
805
|
-
resetsAt,
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function normalizeBackendCredits(value: unknown): NormalizedCredits | undefined {
|
|
810
|
-
if (value === null || value === undefined) return undefined;
|
|
811
|
-
const credits = assertObject(value, "credits") as BackendCreditsSnapshot;
|
|
812
|
-
const hasCredits = asBoolean(credits.has_credits);
|
|
813
|
-
const unlimited = asBoolean(credits.unlimited);
|
|
814
|
-
if (hasCredits === undefined || unlimited === undefined) return undefined;
|
|
815
|
-
return { hasCredits, unlimited, balance: asString(credits.balance) };
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
export function normalizeAppServerResponse(
|
|
819
|
-
response: AppServerRateLimitResponse,
|
|
820
|
-
capturedAt: number,
|
|
821
|
-
): CodexUsageReport {
|
|
822
|
-
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
823
|
-
const addSnapshot = (raw: unknown, fallbackId: string) => {
|
|
824
|
-
const snapshot = normalizeAppServerSnapshot(raw, fallbackId);
|
|
825
|
-
if (!snapshot) return;
|
|
826
|
-
const existingIndex = snapshots.findIndex((item) => item.limitId === snapshot.limitId);
|
|
827
|
-
if (existingIndex >= 0)
|
|
828
|
-
snapshots[existingIndex] = mergeSnapshot(snapshots[existingIndex], snapshot);
|
|
829
|
-
else snapshots.push(snapshot);
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
addSnapshot(response.rateLimits, "codex");
|
|
833
|
-
if (response.rateLimitsByLimitId && typeof response.rateLimitsByLimitId === "object") {
|
|
834
|
-
for (const [limitId, raw] of Object.entries(response.rateLimitsByLimitId)) {
|
|
835
|
-
addSnapshot(raw, limitId);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
if (snapshots.length === 0) {
|
|
840
|
-
throw new Error("codex app-server returned no displayable rate-limit windows.");
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const planType = asAppServerPlanType(response.rateLimits);
|
|
844
|
-
return { source: "codex-app-server", capturedAt, planType, snapshots };
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function asAppServerPlanType(raw: unknown): string | undefined {
|
|
848
|
-
if (raw === null || raw === undefined) return undefined;
|
|
849
|
-
const snapshot = assertObject(
|
|
850
|
-
raw,
|
|
851
|
-
"app-server rate-limit snapshot",
|
|
852
|
-
) as AppServerRateLimitSnapshot;
|
|
853
|
-
return asString(snapshot.planType);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function normalizeAppServerSnapshot(
|
|
857
|
-
raw: unknown,
|
|
858
|
-
fallbackId: string,
|
|
859
|
-
): NormalizedRateLimitSnapshot | undefined {
|
|
860
|
-
if (raw === null || raw === undefined) return undefined;
|
|
861
|
-
const snapshot = assertObject(
|
|
862
|
-
raw,
|
|
863
|
-
"app-server rate-limit snapshot",
|
|
864
|
-
) as AppServerRateLimitSnapshot;
|
|
865
|
-
const limitId = asString(snapshot.limitId) ?? fallbackId;
|
|
866
|
-
const limitName = asString(snapshot.limitName);
|
|
867
|
-
const primary = normalizeAppServerWindow(snapshot.primary);
|
|
868
|
-
const secondary = normalizeAppServerWindow(snapshot.secondary);
|
|
869
|
-
const credits = normalizeAppServerCredits(snapshot.credits);
|
|
870
|
-
if (!primary && !secondary && !credits) return undefined;
|
|
871
|
-
return { limitId, limitName, primary, secondary, credits };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function normalizeAppServerWindow(value: unknown): NormalizedRateLimitWindow | undefined {
|
|
875
|
-
if (value === null || value === undefined) return undefined;
|
|
876
|
-
const window = assertObject(value, "app-server rate-limit window") as AppServerWindowSnapshot;
|
|
877
|
-
const usedPercent = asNumber(window.usedPercent);
|
|
878
|
-
if (usedPercent === undefined) return undefined;
|
|
879
|
-
return {
|
|
880
|
-
usedPercent,
|
|
881
|
-
windowMinutes: asNumber(window.windowDurationMins),
|
|
882
|
-
resetsAt: asNumber(window.resetsAt),
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function normalizeAppServerCredits(value: unknown): NormalizedCredits | undefined {
|
|
887
|
-
if (value === null || value === undefined) return undefined;
|
|
888
|
-
const credits = assertObject(value, "app-server credits") as AppServerCreditsSnapshot;
|
|
889
|
-
const hasCredits = asBoolean(credits.hasCredits);
|
|
890
|
-
const unlimited = asBoolean(credits.unlimited);
|
|
891
|
-
if (hasCredits === undefined || unlimited === undefined) return undefined;
|
|
892
|
-
return { hasCredits, unlimited, balance: asString(credits.balance) };
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function mergeSnapshot(
|
|
896
|
-
left: NormalizedRateLimitSnapshot,
|
|
897
|
-
right: NormalizedRateLimitSnapshot,
|
|
898
|
-
): NormalizedRateLimitSnapshot {
|
|
899
|
-
return {
|
|
900
|
-
limitId: right.limitId || left.limitId,
|
|
901
|
-
limitName: right.limitName ?? left.limitName,
|
|
902
|
-
primary: right.primary ?? left.primary,
|
|
903
|
-
secondary: right.secondary ?? left.secondary,
|
|
904
|
-
credits: right.credits ?? left.credits,
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
export function formatCodexUsageReport(report: CodexUsageReport, _cacheAgeMs?: number): string {
|
|
909
|
-
const lines = [
|
|
910
|
-
" >_ OpenAI Codex Usage",
|
|
911
|
-
"",
|
|
912
|
-
`Visit ${USAGE_SETTINGS_URL} for up-to-date`,
|
|
913
|
-
"information on rate limits and credits",
|
|
914
|
-
"",
|
|
915
|
-
];
|
|
916
|
-
|
|
917
|
-
for (const snapshot of report.snapshots) {
|
|
918
|
-
const label = snapshot.limitName ?? snapshot.limitId;
|
|
919
|
-
if (!isPrimaryCodexSnapshot(snapshot)) {
|
|
920
|
-
lines.push(` ${label} limit:`);
|
|
921
|
-
}
|
|
922
|
-
if (snapshot.primary) lines.push(formatWindowLine("5h limit:", snapshot.primary));
|
|
923
|
-
if (snapshot.secondary) lines.push(formatWindowLine("Weekly limit:", snapshot.secondary));
|
|
924
|
-
if (!snapshot.primary && !snapshot.secondary) {
|
|
925
|
-
lines.push(" Limits unavailable for this account");
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
return lines.join("\n");
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
export function formatCodexUsageStatusline(
|
|
933
|
-
report: CodexUsageReport,
|
|
934
|
-
model?: CodexUsageModel,
|
|
935
|
-
): string {
|
|
936
|
-
const snapshot = selectSnapshotForModel(report, model);
|
|
937
|
-
if (!snapshot) return "usage unavailable";
|
|
938
|
-
|
|
939
|
-
const parts = [formatStatuslinePrefix(snapshot)];
|
|
940
|
-
if (snapshot.primary) parts.push(`${formatRemainingPercent(snapshot.primary)} 5h`);
|
|
941
|
-
if (snapshot.secondary) parts.push(`${formatRemainingPercent(snapshot.secondary)} wk`);
|
|
942
|
-
if (parts.length === 1 && snapshot.credits) parts.push(formatCredits(snapshot.credits));
|
|
943
|
-
return parts.join(" ");
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
function selectSnapshotForModel(
|
|
947
|
-
report: CodexUsageReport,
|
|
948
|
-
model: CodexUsageModel | undefined,
|
|
949
|
-
): NormalizedRateLimitSnapshot | undefined {
|
|
950
|
-
const codexSnapshot = report.snapshots.find(isPrimaryCodexSnapshot);
|
|
951
|
-
if (!model || !isOpenAICodexModel(model)) return codexSnapshot ?? report.snapshots[0];
|
|
952
|
-
|
|
953
|
-
const modelKeys = normalizedModelUsageKeys(model);
|
|
954
|
-
const exactMatch = report.snapshots.find((snapshot) =>
|
|
955
|
-
normalizedSnapshotUsageKeys(snapshot).some((key) => modelKeys.has(key)),
|
|
956
|
-
);
|
|
957
|
-
if (exactMatch) return exactMatch;
|
|
958
|
-
|
|
959
|
-
const variants = codexModelVariantKeys(modelKeys);
|
|
960
|
-
for (const variant of variants) {
|
|
961
|
-
const matches = report.snapshots.filter(
|
|
962
|
-
(snapshot) =>
|
|
963
|
-
!isPrimaryCodexSnapshot(snapshot) &&
|
|
964
|
-
normalizedSnapshotUsageKeys(snapshot).some((key) => normalizedKeyHasToken(key, variant)),
|
|
965
|
-
);
|
|
966
|
-
if (matches.length === 1) return matches[0];
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
return codexSnapshot ?? report.snapshots[0];
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function normalizedModelUsageKeys(model: CodexUsageModel): Set<string> {
|
|
973
|
-
const keys = new Set<string>();
|
|
974
|
-
addNormalizedUsageKey(keys, model.id);
|
|
975
|
-
addNormalizedUsageKey(keys, model.name);
|
|
976
|
-
|
|
977
|
-
for (const key of [...keys]) {
|
|
978
|
-
const codexIndex = key.indexOf("codex");
|
|
979
|
-
if (codexIndex >= 0) keys.add(key.slice(codexIndex));
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
return keys;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function normalizedSnapshotUsageKeys(snapshot: NormalizedRateLimitSnapshot): string[] {
|
|
986
|
-
return [normalizedUsageKey(snapshot.limitId), normalizedUsageKey(snapshot.limitName)].filter(
|
|
987
|
-
(key): key is string => key !== undefined,
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function addNormalizedUsageKey(keys: Set<string>, value: string | undefined): void {
|
|
992
|
-
const key = normalizedUsageKey(value);
|
|
993
|
-
if (key) keys.add(key);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
997
|
-
const key = value
|
|
998
|
-
?.toLowerCase()
|
|
999
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
1000
|
-
.replace(/^-+|-+$/g, "");
|
|
1001
|
-
return key || undefined;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function codexModelVariantKeys(modelKeys: Set<string>): string[] {
|
|
1005
|
-
const variants = new Set<string>();
|
|
1006
|
-
for (const key of modelKeys) {
|
|
1007
|
-
const match = key.match(/(?:^|-)codex-(.+)$/);
|
|
1008
|
-
if (match?.[1]) variants.add(match[1]);
|
|
1009
|
-
}
|
|
1010
|
-
return [...variants];
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
function normalizedKeyHasToken(key: string, token: string): boolean {
|
|
1014
|
-
return (
|
|
1015
|
-
key === token ||
|
|
1016
|
-
key.startsWith(`${token}-`) ||
|
|
1017
|
-
key.endsWith(`-${token}`) ||
|
|
1018
|
-
key.includes(`-${token}-`)
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function formatStatuslinePrefix(snapshot: NormalizedRateLimitSnapshot): string {
|
|
1023
|
-
if (isPrimaryCodexSnapshot(snapshot)) return "codex";
|
|
1024
|
-
const label = snapshot.limitName ?? snapshot.limitId;
|
|
1025
|
-
return `codex ${compactLimitLabel(label)}`;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function compactLimitLabel(label: string): string {
|
|
1029
|
-
const normalized = label.replace(/[_-]+/g, " ").trim();
|
|
1030
|
-
const codexVariant = normalized.match(/\bcodex\s+(.+)$/i)?.[1]?.trim();
|
|
1031
|
-
const compact = codexVariant || normalized;
|
|
1032
|
-
return compact.toLowerCase().replace(/\s+/g, " ");
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
function formatRemainingPercent(window: NormalizedRateLimitWindow): string {
|
|
1036
|
-
return `${(100 - clampPercent(window.usedPercent)).toFixed(0)}%`;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
function showReport(
|
|
1040
|
-
ctx: ExtensionCommandContext,
|
|
1041
|
-
report: CodexUsageReport,
|
|
1042
|
-
fromCache: boolean,
|
|
1043
|
-
): void {
|
|
1044
|
-
const text = formatCodexUsageReport(
|
|
1045
|
-
report,
|
|
1046
|
-
fromCache ? Date.now() - report.capturedAt : undefined,
|
|
1047
|
-
);
|
|
1048
|
-
ctx.ui.notify(ctx.hasUI ? brightenInfoNotification(text) : text, "info");
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function brightenInfoNotification(text: string): string {
|
|
1052
|
-
return `${RESET_FOREGROUND}${text}`;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function isPrimaryCodexSnapshot(snapshot: NormalizedRateLimitSnapshot): boolean {
|
|
1056
|
-
return (
|
|
1057
|
-
normalizedUsageKey(snapshot.limitId) === "codex" ||
|
|
1058
|
-
normalizedUsageKey(snapshot.limitName) === "codex"
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function formatWindowLine(label: string, window: NormalizedRateLimitWindow): string {
|
|
1063
|
-
return ` ${label.padEnd(LIMIT_VALUE_COLUMN)}${formatWindow(window)}`;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function formatWindow(window: NormalizedRateLimitWindow): string {
|
|
1067
|
-
const remaining = 100 - clampPercent(window.usedPercent);
|
|
1068
|
-
const reset = window.resetsAt ? ` (resets ${formatReset(window.resetsAt)})` : "";
|
|
1069
|
-
return `${progressBar(remaining)} ${remaining.toFixed(0)}% left${reset}`;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function progressBar(percentRemaining: number): string {
|
|
1073
|
-
const filled = Math.round((clampPercent(percentRemaining) / 100) * BAR_SEGMENTS);
|
|
1074
|
-
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}]`;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function formatCredits(credits: NormalizedCredits): string {
|
|
1078
|
-
if (!credits.hasCredits) return "no credits";
|
|
1079
|
-
if (credits.unlimited) return "unlimited credits";
|
|
1080
|
-
const balance = credits.balance?.trim();
|
|
1081
|
-
if (!balance) return "credits available";
|
|
1082
|
-
return `${formatNumber(Number(balance), balance)} credits`;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function formatReset(epochSeconds: number): string {
|
|
1086
|
-
const reset = new Date(epochSeconds * 1000);
|
|
1087
|
-
if (Number.isNaN(reset.getTime())) return "at an unknown time";
|
|
1088
|
-
|
|
1089
|
-
const now = new Date();
|
|
1090
|
-
const time = `${reset.getHours().toString().padStart(2, "0")}:${reset
|
|
1091
|
-
.getMinutes()
|
|
1092
|
-
.toString()
|
|
1093
|
-
.padStart(2, "0")}`;
|
|
1094
|
-
if (reset.toDateString() === now.toDateString()) return time;
|
|
1095
|
-
const day = reset.getDate().toString();
|
|
1096
|
-
const month = reset.toLocaleDateString(undefined, { month: "short" });
|
|
1097
|
-
return `${time} on ${day} ${month}`;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function formatQueryErrors(errors: UsageQueryError[]): string {
|
|
1101
|
-
const lines = ["Unable to read Codex usage."];
|
|
1102
|
-
for (const error of errors) {
|
|
1103
|
-
const source = error.source === "pi-auth" ? "Pi auth direct" : "Codex app-server fallback";
|
|
1104
|
-
lines.push(`- ${source}: ${error.message}`);
|
|
1105
|
-
}
|
|
1106
|
-
lines.push("");
|
|
1107
|
-
lines.push(
|
|
1108
|
-
"Tip: use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro. If Pi auth is unavailable, install Codex CLI and run codex login for the fallback.",
|
|
1109
|
-
);
|
|
1110
|
-
return lines.join("\n");
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
function formatPlanType(planType: string): string {
|
|
1114
|
-
const key = planType
|
|
1115
|
-
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
1116
|
-
.toLowerCase()
|
|
1117
|
-
.replace(/[^a-z0-9]+/g, "_");
|
|
1118
|
-
if (key === "pro_lite" || key === "prolite") return "Pro Lite";
|
|
1119
|
-
if (key === "team" || key === "self_serve_business_usage_based" || key === "business") {
|
|
1120
|
-
return "Business";
|
|
1121
|
-
}
|
|
1122
|
-
if (key === "enterprise_cbp_usage_based") return "Enterprise";
|
|
1123
|
-
|
|
1124
|
-
const normalized = planType
|
|
1125
|
-
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
1126
|
-
.replace(/[_-]+/g, " ")
|
|
1127
|
-
.trim();
|
|
1128
|
-
if (!normalized) return planType;
|
|
1129
|
-
return normalized
|
|
1130
|
-
.split(/\s+/)
|
|
1131
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
1132
|
-
.join(" ");
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
function formatDuration(milliseconds: number): string {
|
|
1136
|
-
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
1137
|
-
if (seconds < 60) return `${seconds}s`;
|
|
1138
|
-
const minutes = Math.round(seconds / 60);
|
|
1139
|
-
if (minutes < 60) return `${minutes}m`;
|
|
1140
|
-
const hours = Math.round(minutes / 60);
|
|
1141
|
-
return `${hours}h`;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function formatNumber(value: number, fallback: string): string {
|
|
1145
|
-
if (!Number.isFinite(value)) return fallback;
|
|
1146
|
-
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function clampPercent(value: number): number {
|
|
1150
|
-
if (!Number.isFinite(value)) return 0;
|
|
1151
|
-
return Math.min(100, Math.max(0, value));
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function parseJsonObject(text: string, description: string): Record<string, unknown> {
|
|
1155
|
-
let parsed: unknown;
|
|
1156
|
-
try {
|
|
1157
|
-
parsed = JSON.parse(text) as unknown;
|
|
1158
|
-
} catch (error) {
|
|
1159
|
-
throw new Error(`${description} was not valid JSON: ${errorMessage(error)}`);
|
|
1160
|
-
}
|
|
1161
|
-
return assertObject(parsed, description);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function assertObject(value: unknown, description: string): Record<string, unknown> {
|
|
1165
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1166
|
-
throw new Error(`${description} was not an object.`);
|
|
1167
|
-
}
|
|
1168
|
-
return value as Record<string, unknown>;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function asString(value: unknown): string | undefined {
|
|
1172
|
-
return typeof value === "string" ? value : undefined;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
function asNumber(value: unknown): number | undefined {
|
|
1176
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1177
|
-
if (typeof value === "string" && value.trim()) {
|
|
1178
|
-
const parsed = Number(value);
|
|
1179
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1180
|
-
}
|
|
1181
|
-
return undefined;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function asBoolean(value: unknown): boolean | undefined {
|
|
1185
|
-
return typeof value === "boolean" ? value : undefined;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
1189
|
-
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function redactErrorBody(body: string): string {
|
|
1193
|
-
return truncateEnd(
|
|
1194
|
-
body
|
|
1195
|
-
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
1196
|
-
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
1197
|
-
.trim(),
|
|
1198
|
-
MAX_ERROR_BODY_CHARS,
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
function truncateEnd(value: string, maxChars: number): string {
|
|
1203
|
-
if (value.length <= maxChars) return value;
|
|
1204
|
-
return `${value.slice(0, maxChars - 1)}…`;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
function errorMessage(error: unknown): string {
|
|
1208
|
-
return error instanceof Error ? error.message : String(error);
|
|
1209
|
-
}
|
|
329
|
+
export { formatCodexUsageReport, formatCodexUsageStatusline } from "./format.js";
|
|
330
|
+
export { normalizeAppServerResponse, normalizeBackendPayload } from "./normalize.js";
|
|
331
|
+
export { isStaleExtensionContextError } from "./query.js";
|
|
332
|
+
export type {
|
|
333
|
+
CodexUsageModel,
|
|
334
|
+
CodexUsageReport,
|
|
335
|
+
NormalizedCredits,
|
|
336
|
+
NormalizedRateLimitSnapshot,
|
|
337
|
+
NormalizedRateLimitWindow,
|
|
338
|
+
} from "./types.js";
|