@llblab/pi-codex-usage 0.3.3 โ 0.3.5
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/CHANGELOG.md +2 -0
- package/README.md +6 -4
- package/banner.png +0 -0
- package/index.ts +660 -651
- package/package.json +5 -3
package/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
8
|
|
|
6
9
|
const CODEX_PROVIDER_ID = "openai-codex";
|
|
7
10
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -12,22 +15,22 @@ const STATUS_KEY = "codex-usage";
|
|
|
12
15
|
const MAX_ERROR_BODY_CHARS = 600;
|
|
13
16
|
const STATUS_LABEL_TEXT = "codex";
|
|
14
17
|
const DUAL_BAR_CHARS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
" ",
|
|
19
|
+
"โ",
|
|
20
|
+
"โ",
|
|
21
|
+
"โ",
|
|
22
|
+
"โ",
|
|
23
|
+
"โ",
|
|
24
|
+
"โ",
|
|
25
|
+
"โ",
|
|
26
|
+
"โ",
|
|
27
|
+
"โ",
|
|
28
|
+
"โ",
|
|
29
|
+
"โ",
|
|
30
|
+
"โ",
|
|
31
|
+
"โ",
|
|
32
|
+
"โ",
|
|
33
|
+
"โ",
|
|
31
34
|
];
|
|
32
35
|
|
|
33
36
|
type UsageSource = "pi-auth" | "codex-app-server";
|
|
@@ -36,811 +39,817 @@ type PiModel = NonNullable<ExtensionContext["model"]>;
|
|
|
36
39
|
export type CodexUsageModel = Pick<PiModel, "id" | "name" | "provider">;
|
|
37
40
|
|
|
38
41
|
type QueryUsageOptions = {
|
|
39
|
-
|
|
42
|
+
timeoutMs: number;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
type CachedReport = {
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
createdAt: number;
|
|
47
|
+
report: CodexUsageReport;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
type QueryUsageResult =
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
| { ok: true; report: CodexUsageReport }
|
|
52
|
+
| { ok: false; errors: UsageQueryError[] };
|
|
50
53
|
|
|
51
54
|
type UsageQueryError = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
source: UsageSource;
|
|
56
|
+
message: string;
|
|
57
|
+
cause?: unknown;
|
|
55
58
|
};
|
|
56
59
|
|
|
57
60
|
export type CodexUsageReport = {
|
|
58
|
-
|
|
61
|
+
snapshots: NormalizedRateLimitSnapshot[];
|
|
59
62
|
};
|
|
60
63
|
|
|
61
64
|
export type NormalizedRateLimitSnapshot = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
limitId: string;
|
|
66
|
+
primary?: NormalizedRateLimitWindow;
|
|
67
|
+
secondary?: NormalizedRateLimitWindow;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
export type NormalizedRateLimitWindow = {
|
|
68
|
-
|
|
71
|
+
usedPercent: number;
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
type RateLimitStatusPayload = {
|
|
72
|
-
|
|
75
|
+
rate_limit?: unknown;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
type BackendRateLimitDetails = {
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
primary_window?: unknown;
|
|
80
|
+
secondary_window?: unknown;
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
type BackendWindowSnapshot = {
|
|
81
|
-
|
|
84
|
+
used_percent?: unknown;
|
|
82
85
|
};
|
|
83
86
|
|
|
84
87
|
type AppServerRateLimitResponse = {
|
|
85
|
-
|
|
88
|
+
rateLimits?: unknown;
|
|
86
89
|
};
|
|
87
90
|
|
|
88
91
|
type AppServerRateLimitSnapshot = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
limitId?: unknown;
|
|
93
|
+
primary?: unknown;
|
|
94
|
+
secondary?: unknown;
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
type AppServerWindowSnapshot = {
|
|
95
|
-
|
|
98
|
+
usedPercent?: unknown;
|
|
96
99
|
};
|
|
97
100
|
|
|
98
101
|
type RpcResponse = {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
id?: unknown;
|
|
103
|
+
result?: unknown;
|
|
104
|
+
error?: { message?: unknown; code?: unknown };
|
|
102
105
|
};
|
|
103
106
|
|
|
104
107
|
type PendingRpc = {
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
resolve: (value: unknown) => void;
|
|
109
|
+
reject: (error: Error) => void;
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
export default function codexUsage(pi: ExtensionAPI) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
113
|
+
let cache: CachedReport | undefined;
|
|
114
|
+
let failedRefreshes = 0;
|
|
115
|
+
let statuslineBlinkTimer: TimeoutHandle | undefined;
|
|
116
|
+
let statuslineClearTimer: TimeoutHandle | undefined;
|
|
117
|
+
let statuslineRefreshTimer: TimeoutHandle | undefined;
|
|
118
|
+
let statuslineRequestId = 0;
|
|
119
|
+
|
|
120
|
+
const clearStatuslineTimers = () => {
|
|
121
|
+
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
122
|
+
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
123
|
+
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
124
|
+
statuslineBlinkTimer = undefined;
|
|
125
|
+
statuslineClearTimer = undefined;
|
|
126
|
+
statuslineRefreshTimer = undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clearUsageStatusline = (ctx: ExtensionContext) => {
|
|
130
|
+
statuslineRequestId += 1;
|
|
131
|
+
clearStatuslineTimers();
|
|
132
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const scheduleTemporaryStatuslineClear = (ctx: ExtensionContext) => {
|
|
136
|
+
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
137
|
+
statuslineClearTimer = setTimeout(() => {
|
|
138
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
139
|
+
statuslineClearTimer = undefined;
|
|
140
|
+
}, REFRESH_INTERVAL_MS) as TimeoutHandle;
|
|
141
|
+
statuslineClearTimer.unref?.();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const scheduleStatuslineRefresh = (ctx: ExtensionContext) => {
|
|
145
|
+
if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
|
|
146
|
+
statuslineRefreshTimer = setTimeout(() => {
|
|
147
|
+
void refreshCurrentCodexUsageStatusline(ctx, true);
|
|
148
|
+
}, REFRESH_INTERVAL_MS) as TimeoutHandle;
|
|
149
|
+
statuslineRefreshTimer.unref?.();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const setUsageStatusline = (
|
|
153
|
+
ctx: ExtensionContext,
|
|
154
|
+
report: CodexUsageReport,
|
|
155
|
+
options: {
|
|
156
|
+
autoRefresh: boolean;
|
|
157
|
+
blink: boolean;
|
|
158
|
+
model: CodexUsageModel | undefined;
|
|
159
|
+
},
|
|
160
|
+
) => {
|
|
161
|
+
if (statuslineBlinkTimer) clearTimeout(statuslineBlinkTimer);
|
|
162
|
+
if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
|
|
163
|
+
statuslineBlinkTimer = undefined;
|
|
164
|
+
statuslineClearTimer = undefined;
|
|
165
|
+
const text = formatCodexUsageStatusline(report, ctx, options.model);
|
|
166
|
+
if (options.blink) {
|
|
167
|
+
ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
|
|
168
|
+
statuslineBlinkTimer = setTimeout(() => {
|
|
169
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
170
|
+
statuslineBlinkTimer = undefined;
|
|
171
|
+
}, REDRAW_BLINK_MS) as TimeoutHandle;
|
|
172
|
+
statuslineBlinkTimer.unref?.();
|
|
173
|
+
} else {
|
|
174
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
175
|
+
}
|
|
176
|
+
if (options.autoRefresh) scheduleStatuslineRefresh(ctx);
|
|
177
|
+
else scheduleTemporaryStatuslineClear(ctx);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const refreshCurrentCodexUsageStatusline = async (
|
|
181
|
+
ctx: ExtensionContext,
|
|
182
|
+
force: boolean,
|
|
183
|
+
model = ctx.model,
|
|
184
|
+
) => {
|
|
185
|
+
if (!isOpenAICodexModel(model)) {
|
|
186
|
+
clearUsageStatusline(ctx);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!cache) ctx.ui.setStatus(STATUS_KEY, formatEmptyStatuslineBar(ctx));
|
|
191
|
+
const requestId = statuslineRequestId + 1;
|
|
192
|
+
statuslineRequestId = requestId;
|
|
193
|
+
const cached =
|
|
194
|
+
cache && Date.now() - cache.createdAt < REFRESH_INTERVAL_MS
|
|
195
|
+
? cache
|
|
196
|
+
: undefined;
|
|
197
|
+
if (cached && !force) {
|
|
198
|
+
setUsageStatusline(ctx, cached.report, {
|
|
199
|
+
autoRefresh: true,
|
|
200
|
+
blink: false,
|
|
201
|
+
model,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await queryUsage(ctx, { timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
207
|
+
if (requestId !== statuslineRequestId) return;
|
|
208
|
+
if (!isOpenAICodexModel(ctx.model)) {
|
|
209
|
+
clearUsageStatusline(ctx);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
failedRefreshes += 1;
|
|
215
|
+
if (!cache || failedRefreshes >= 5) {
|
|
216
|
+
ctx.ui.setStatus(
|
|
217
|
+
STATUS_KEY,
|
|
218
|
+
formatStatuslineProblem(ctx, result.errors),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
scheduleStatuslineRefresh(ctx);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const previousReport = cache?.report;
|
|
226
|
+
const blink = previousReport
|
|
227
|
+
? formatReportBar(previousReport) !== formatReportBar(result.report)
|
|
228
|
+
: false;
|
|
229
|
+
failedRefreshes = 0;
|
|
230
|
+
cache = { createdAt: Date.now(), report: result.report };
|
|
231
|
+
setUsageStatusline(ctx, result.report, {
|
|
232
|
+
autoRefresh: true,
|
|
233
|
+
blink,
|
|
234
|
+
model,
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
pi.on("session_start", (_event, ctx) => {
|
|
239
|
+
if (isOpenAICodexModel(ctx.model))
|
|
240
|
+
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
241
|
+
else clearUsageStatusline(ctx);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
245
|
+
if (isOpenAICodexModel(ctx.model))
|
|
246
|
+
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
247
|
+
else clearUsageStatusline(ctx);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
pi.on("model_select", (event, ctx) => {
|
|
251
|
+
if (isOpenAICodexModel(event.model)) {
|
|
252
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
|
|
253
|
+
} else {
|
|
254
|
+
clearUsageStatusline(ctx);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
|
|
249
259
|
}
|
|
250
260
|
|
|
251
261
|
function isOpenAICodexModel(
|
|
252
|
-
|
|
262
|
+
model: Pick<PiModel, "provider"> | undefined,
|
|
253
263
|
): boolean {
|
|
254
|
-
|
|
264
|
+
return model?.provider === CODEX_PROVIDER_ID;
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
async function queryUsage(
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
ctx: ExtensionContext,
|
|
269
|
+
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
260
270
|
): Promise<QueryUsageResult> {
|
|
261
|
-
|
|
271
|
+
const errors: UsageQueryError[] = [];
|
|
262
272
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
273
|
+
try {
|
|
274
|
+
const report = await queryViaPiAuth(ctx, options.timeoutMs);
|
|
275
|
+
return { ok: true, report };
|
|
276
|
+
} catch (cause) {
|
|
277
|
+
errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
|
|
278
|
+
}
|
|
269
279
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
try {
|
|
281
|
+
const report = await queryViaCodexAppServer(options.timeoutMs);
|
|
282
|
+
return { ok: true, report };
|
|
283
|
+
} catch (cause) {
|
|
284
|
+
errors.push({
|
|
285
|
+
source: "codex-app-server",
|
|
286
|
+
message: errorMessage(cause),
|
|
287
|
+
cause,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
280
290
|
|
|
281
|
-
|
|
291
|
+
return { ok: false, errors };
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
async function queryViaPiAuth(
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
ctx: ExtensionContext,
|
|
296
|
+
timeoutMs: number,
|
|
287
297
|
): Promise<CodexUsageReport> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
298
|
+
const auth = await resolvePiCodexAuth(ctx);
|
|
299
|
+
if (!auth) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
"No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const response = await fetchWithTimeout(
|
|
306
|
+
CODEX_USAGE_URL,
|
|
307
|
+
{ headers: auth.headers },
|
|
308
|
+
timeoutMs,
|
|
309
|
+
);
|
|
310
|
+
const text = await response.text();
|
|
311
|
+
if (!response.ok) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Codex usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const payload = parseJsonObject(text, "Codex usage endpoint response");
|
|
318
|
+
return normalizeBackendPayload(
|
|
319
|
+
payload as RateLimitStatusPayload,
|
|
320
|
+
Date.now(),
|
|
321
|
+
"pi-auth",
|
|
322
|
+
);
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
async function resolvePiCodexAuth(
|
|
316
|
-
|
|
326
|
+
ctx: ExtensionContext,
|
|
317
327
|
): Promise<{ headers: Record<string, string> } | undefined> {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
328
|
+
const models = codexAuthCandidateModels(ctx);
|
|
329
|
+
const errors: string[] = [];
|
|
330
|
+
|
|
331
|
+
for (const model of models) {
|
|
332
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
333
|
+
if (!auth.ok) {
|
|
334
|
+
errors.push(auth.error);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const headers = { ...(auth.headers ?? {}) };
|
|
339
|
+
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
340
|
+
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
341
|
+
}
|
|
342
|
+
if (!hasHeader(headers, "User-Agent")) {
|
|
343
|
+
headers["User-Agent"] = "pi-codex-usage";
|
|
344
|
+
}
|
|
345
|
+
if (hasHeader(headers, "Authorization")) {
|
|
346
|
+
return { headers };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (errors.length > 0) {
|
|
351
|
+
throw new Error(errors.join("; "));
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
344
354
|
}
|
|
345
355
|
|
|
346
356
|
function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
357
|
+
const candidates: PiModel[] = [];
|
|
358
|
+
const seen = new Set<string>();
|
|
359
|
+
const add = (model: PiModel | undefined) => {
|
|
360
|
+
if (!model || model.provider !== CODEX_PROVIDER_ID) return;
|
|
361
|
+
const key = `${model.provider}/${model.id}`;
|
|
362
|
+
if (seen.has(key)) return;
|
|
363
|
+
seen.add(key);
|
|
364
|
+
candidates.push(model);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
add(ctx.model);
|
|
368
|
+
for (const model of ctx.modelRegistry.getAvailable()) add(model);
|
|
369
|
+
for (const model of ctx.modelRegistry.getAll()) add(model);
|
|
370
|
+
return candidates;
|
|
361
371
|
}
|
|
362
372
|
|
|
363
373
|
async function fetchWithTimeout(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
374
|
+
url: string,
|
|
375
|
+
init: RequestInit,
|
|
376
|
+
timeoutMs: number,
|
|
367
377
|
): Promise<Response> {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
const controller = new AbortController();
|
|
379
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
380
|
+
try {
|
|
381
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (controller.signal.aborted) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
} finally {
|
|
390
|
+
clearTimeout(timeout);
|
|
391
|
+
}
|
|
382
392
|
}
|
|
383
393
|
|
|
384
394
|
async function queryViaCodexAppServer(
|
|
385
|
-
|
|
395
|
+
timeoutMs: number,
|
|
386
396
|
): Promise<CodexUsageReport> {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
397
|
+
const client = new CodexAppServerClient(timeoutMs);
|
|
398
|
+
try {
|
|
399
|
+
await client.start();
|
|
400
|
+
await client.request("initialize", {
|
|
401
|
+
clientInfo: {
|
|
402
|
+
name: "pi_codex_usage",
|
|
403
|
+
title: "Pi Codex Usage",
|
|
404
|
+
version: "0.1.0",
|
|
405
|
+
},
|
|
406
|
+
capabilities: {
|
|
407
|
+
experimentalApi: false,
|
|
408
|
+
requestAttestation: false,
|
|
409
|
+
optOutNotificationMethods: [],
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
client.notify("initialized");
|
|
413
|
+
const result = await client.request("account/rateLimits/read", undefined);
|
|
414
|
+
return normalizeAppServerResponse(
|
|
415
|
+
assertObject(
|
|
416
|
+
result,
|
|
417
|
+
"account/rateLimits/read result",
|
|
418
|
+
) as AppServerRateLimitResponse,
|
|
419
|
+
Date.now(),
|
|
420
|
+
);
|
|
421
|
+
} finally {
|
|
422
|
+
client.dispose();
|
|
423
|
+
}
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
class CodexAppServerClient {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
427
|
+
private child?: ChildProcessWithoutNullStreams;
|
|
428
|
+
private nextId = 1;
|
|
429
|
+
private stderr = "";
|
|
430
|
+
private readonly pending = new Map<number, PendingRpc>();
|
|
431
|
+
private startPromise?: Promise<void>;
|
|
432
|
+
private exitError?: Error;
|
|
433
|
+
private readonly timeoutMs: number;
|
|
434
|
+
|
|
435
|
+
constructor(timeoutMs: number) {
|
|
436
|
+
this.timeoutMs = timeoutMs;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
start(): Promise<void> {
|
|
440
|
+
if (this.startPromise) return this.startPromise;
|
|
441
|
+
|
|
442
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
443
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
444
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
445
|
+
});
|
|
446
|
+
this.child = child;
|
|
447
|
+
|
|
448
|
+
const startupTimeout = setTimeout(() => {
|
|
449
|
+
reject(
|
|
450
|
+
new Error(
|
|
451
|
+
`Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
}, this.timeoutMs);
|
|
455
|
+
|
|
456
|
+
child.once("spawn", () => {
|
|
457
|
+
clearTimeout(startupTimeout);
|
|
458
|
+
resolve();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
child.once("error", (error) => {
|
|
462
|
+
clearTimeout(startupTimeout);
|
|
463
|
+
reject(new Error(`Failed to start codex app-server: ${error.message}`));
|
|
464
|
+
this.rejectAll(error);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
child.once("exit", (code, signal) => {
|
|
468
|
+
const suffix = this.stderr
|
|
469
|
+
? ` stderr: ${redactErrorBody(this.stderr)}`
|
|
470
|
+
: "";
|
|
471
|
+
this.exitError = new Error(
|
|
472
|
+
`codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
|
|
473
|
+
);
|
|
474
|
+
this.rejectAll(this.exitError);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
child.stderr.setEncoding("utf8");
|
|
478
|
+
child.stderr.on("data", (chunk: string) => {
|
|
479
|
+
this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const lines = createInterface({ input: child.stdout });
|
|
483
|
+
lines.on("line", (line) => this.handleLine(line));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return this.startPromise;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
request(method: string, params: unknown): Promise<unknown> {
|
|
490
|
+
const child = this.child;
|
|
491
|
+
if (!child?.stdin.writable) {
|
|
492
|
+
throw new Error("codex app-server is not running.");
|
|
493
|
+
}
|
|
494
|
+
if (this.exitError) throw this.exitError;
|
|
495
|
+
|
|
496
|
+
const id = this.nextId++;
|
|
497
|
+
const payload =
|
|
498
|
+
params === undefined ? { method, id } : { method, id, params };
|
|
499
|
+
const response = new Promise<unknown>((resolve, reject) => {
|
|
500
|
+
const timeout = setTimeout(() => {
|
|
501
|
+
this.pending.delete(id);
|
|
502
|
+
reject(
|
|
503
|
+
new Error(
|
|
504
|
+
`Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`,
|
|
505
|
+
),
|
|
506
|
+
);
|
|
507
|
+
}, this.timeoutMs);
|
|
508
|
+
|
|
509
|
+
this.pending.set(id, {
|
|
510
|
+
resolve: (value) => {
|
|
511
|
+
clearTimeout(timeout);
|
|
512
|
+
resolve(value);
|
|
513
|
+
},
|
|
514
|
+
reject: (error) => {
|
|
515
|
+
clearTimeout(timeout);
|
|
516
|
+
reject(error);
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
522
|
+
return response;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
notify(method: string): void {
|
|
526
|
+
const child = this.child;
|
|
527
|
+
if (!child?.stdin.writable) return;
|
|
528
|
+
child.stdin.write(`${JSON.stringify({ method })}\n`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
dispose(): void {
|
|
532
|
+
for (const [id, pending] of this.pending) {
|
|
533
|
+
pending.reject(new Error(`codex app-server request ${id} cancelled.`));
|
|
534
|
+
}
|
|
535
|
+
this.pending.clear();
|
|
536
|
+
|
|
537
|
+
const child = this.child;
|
|
538
|
+
if (!child) return;
|
|
539
|
+
child.stdin.end();
|
|
540
|
+
if (!child.killed) child.kill();
|
|
541
|
+
this.child = undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private handleLine(line: string): void {
|
|
545
|
+
let parsed: RpcResponse;
|
|
546
|
+
try {
|
|
547
|
+
parsed = JSON.parse(line) as RpcResponse;
|
|
548
|
+
} catch {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (typeof parsed.id !== "number") return;
|
|
553
|
+
const pending = this.pending.get(parsed.id);
|
|
554
|
+
if (!pending) return;
|
|
555
|
+
this.pending.delete(parsed.id);
|
|
556
|
+
|
|
557
|
+
if (parsed.error) {
|
|
558
|
+
const message =
|
|
559
|
+
typeof parsed.error.message === "string"
|
|
560
|
+
? parsed.error.message
|
|
561
|
+
: "unknown error";
|
|
562
|
+
pending.reject(new Error(`codex app-server request failed: ${message}`));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
pending.resolve(parsed.result);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private rejectAll(error: Error): void {
|
|
570
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
571
|
+
this.pending.clear();
|
|
572
|
+
}
|
|
563
573
|
}
|
|
564
574
|
|
|
565
575
|
export function normalizeBackendPayload(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
576
|
+
payload: RateLimitStatusPayload,
|
|
577
|
+
_capturedAt: number,
|
|
578
|
+
_source: UsageSource,
|
|
569
579
|
): CodexUsageReport {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
580
|
+
const snapshot = normalizeBackendSnapshot("codex", payload.rate_limit);
|
|
581
|
+
if (!snapshot) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
"Codex usage endpoint returned no displayable rate-limit windows.",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return { snapshots: [snapshot] };
|
|
577
587
|
}
|
|
578
588
|
|
|
579
589
|
function normalizeBackendSnapshot(
|
|
580
|
-
|
|
581
|
-
|
|
590
|
+
limitId: string,
|
|
591
|
+
rateLimit: unknown,
|
|
582
592
|
): NormalizedRateLimitSnapshot | undefined {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
593
|
+
if (rateLimit === null || rateLimit === undefined) return undefined;
|
|
594
|
+
const details = assertObject(
|
|
595
|
+
rateLimit,
|
|
596
|
+
"rate limit",
|
|
597
|
+
) as BackendRateLimitDetails;
|
|
598
|
+
const primary = normalizeBackendWindow(details.primary_window);
|
|
599
|
+
const secondary = normalizeBackendWindow(details.secondary_window);
|
|
600
|
+
if (!primary && !secondary) return undefined;
|
|
601
|
+
return { limitId, primary, secondary };
|
|
592
602
|
}
|
|
593
603
|
|
|
594
604
|
function normalizeBackendWindow(
|
|
595
|
-
|
|
605
|
+
value: unknown,
|
|
596
606
|
): NormalizedRateLimitWindow | undefined {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
607
|
+
if (value === null || value === undefined) return undefined;
|
|
608
|
+
const window = assertObject(
|
|
609
|
+
value,
|
|
610
|
+
"rate-limit window",
|
|
611
|
+
) as BackendWindowSnapshot;
|
|
612
|
+
const usedPercent = asNumber(window.used_percent);
|
|
613
|
+
if (usedPercent === undefined) return undefined;
|
|
614
|
+
return { usedPercent };
|
|
605
615
|
}
|
|
606
616
|
|
|
607
617
|
export function normalizeAppServerResponse(
|
|
608
|
-
|
|
609
|
-
|
|
618
|
+
response: AppServerRateLimitResponse,
|
|
619
|
+
_capturedAt: number,
|
|
610
620
|
): CodexUsageReport {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
621
|
+
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
622
|
+
const addSnapshot = (raw: unknown, fallbackId: string) => {
|
|
623
|
+
const snapshot = normalizeAppServerSnapshot(raw, fallbackId);
|
|
624
|
+
if (!snapshot) return;
|
|
625
|
+
const existingIndex = snapshots.findIndex(
|
|
626
|
+
(item) => item.limitId === snapshot.limitId,
|
|
627
|
+
);
|
|
628
|
+
if (existingIndex >= 0)
|
|
629
|
+
snapshots[existingIndex] = mergeSnapshot(
|
|
630
|
+
snapshots[existingIndex],
|
|
631
|
+
snapshot,
|
|
632
|
+
);
|
|
633
|
+
else snapshots.push(snapshot);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
addSnapshot(response.rateLimits, "codex");
|
|
637
|
+
if (snapshots.length === 0) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
"codex app-server returned no displayable rate-limit windows.",
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return { snapshots };
|
|
634
644
|
}
|
|
635
645
|
|
|
636
646
|
function normalizeAppServerSnapshot(
|
|
637
|
-
|
|
638
|
-
|
|
647
|
+
raw: unknown,
|
|
648
|
+
fallbackId: string,
|
|
639
649
|
): NormalizedRateLimitSnapshot | undefined {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
+
if (raw === null || raw === undefined) return undefined;
|
|
651
|
+
const snapshot = assertObject(
|
|
652
|
+
raw,
|
|
653
|
+
"app-server rate-limit snapshot",
|
|
654
|
+
) as AppServerRateLimitSnapshot;
|
|
655
|
+
const limitId = asString(snapshot.limitId) ?? fallbackId;
|
|
656
|
+
const primary = normalizeAppServerWindow(snapshot.primary);
|
|
657
|
+
const secondary = normalizeAppServerWindow(snapshot.secondary);
|
|
658
|
+
if (!primary && !secondary) return undefined;
|
|
659
|
+
return { limitId, primary, secondary };
|
|
650
660
|
}
|
|
651
661
|
|
|
652
662
|
function normalizeAppServerWindow(
|
|
653
|
-
|
|
663
|
+
value: unknown,
|
|
654
664
|
): NormalizedRateLimitWindow | undefined {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
665
|
+
if (value === null || value === undefined) return undefined;
|
|
666
|
+
const window = assertObject(
|
|
667
|
+
value,
|
|
668
|
+
"app-server rate-limit window",
|
|
669
|
+
) as AppServerWindowSnapshot;
|
|
670
|
+
const usedPercent = asNumber(window.usedPercent);
|
|
671
|
+
if (usedPercent === undefined) return undefined;
|
|
672
|
+
return { usedPercent };
|
|
663
673
|
}
|
|
664
674
|
|
|
665
675
|
function mergeSnapshot(
|
|
666
|
-
|
|
667
|
-
|
|
676
|
+
left: NormalizedRateLimitSnapshot,
|
|
677
|
+
right: NormalizedRateLimitSnapshot,
|
|
668
678
|
): NormalizedRateLimitSnapshot {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
679
|
+
return {
|
|
680
|
+
limitId: right.limitId || left.limitId,
|
|
681
|
+
primary: right.primary ?? left.primary,
|
|
682
|
+
secondary: right.secondary ?? left.secondary,
|
|
683
|
+
};
|
|
674
684
|
}
|
|
675
685
|
|
|
676
686
|
export function formatCodexUsageStatusline(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
687
|
+
report: CodexUsageReport,
|
|
688
|
+
ctx: ExtensionContext,
|
|
689
|
+
_model?: CodexUsageModel,
|
|
680
690
|
): string {
|
|
681
|
-
|
|
682
|
-
|
|
691
|
+
return formatStatuslineText(ctx, formatReportBar(report) ?? "n/a");
|
|
692
|
+
}
|
|
683
693
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
694
|
+
function formatReportBar(report: CodexUsageReport): string | undefined {
|
|
695
|
+
const snapshot = selectPrimaryCodexSnapshot(report);
|
|
696
|
+
if (!snapshot || (!snapshot.primary && !snapshot.secondary)) return undefined;
|
|
697
|
+
return formatDualLimitBar(snapshot.primary, snapshot.secondary);
|
|
689
698
|
}
|
|
690
699
|
|
|
691
700
|
function formatStatuslineText(ctx: ExtensionContext, value: string): string {
|
|
692
|
-
|
|
693
|
-
|
|
701
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
702
|
+
return `${label} ${ctx.ui.theme.fg("dim", value)}`;
|
|
694
703
|
}
|
|
695
704
|
|
|
696
705
|
function formatEmptyStatuslineBar(ctx: ExtensionContext): string {
|
|
697
|
-
|
|
706
|
+
return formatStatuslineText(ctx, "\u00a0\u00a0\u00a0\u00a0\u00a0");
|
|
698
707
|
}
|
|
699
708
|
|
|
700
709
|
function formatStatuslineProblem(
|
|
701
|
-
|
|
702
|
-
|
|
710
|
+
ctx: ExtensionContext,
|
|
711
|
+
errors: UsageQueryError[],
|
|
703
712
|
): string {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
713
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
714
|
+
const value = isUnavailable(errors)
|
|
715
|
+
? ctx.ui.theme.fg("muted", "n/a")
|
|
716
|
+
: ctx.ui.theme.fg("error", "error");
|
|
717
|
+
return `${label} ${value}`;
|
|
709
718
|
}
|
|
710
719
|
|
|
711
720
|
function isUnavailable(errors: UsageQueryError[]): boolean {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
721
|
+
return errors.some((error) => {
|
|
722
|
+
const message = error.message.toLowerCase();
|
|
723
|
+
return (
|
|
724
|
+
message.includes("no pi openai codex subscription auth") ||
|
|
725
|
+
message.includes("no displayable rate-limit windows") ||
|
|
726
|
+
message.includes("returned no displayable rate-limit windows") ||
|
|
727
|
+
message.includes("returned 401") ||
|
|
728
|
+
message.includes("returned 403") ||
|
|
729
|
+
message.includes("unauthorized") ||
|
|
730
|
+
message.includes("forbidden") ||
|
|
731
|
+
message.includes("subscription") ||
|
|
732
|
+
message.includes("no active plan") ||
|
|
733
|
+
message.includes("plan unavailable") ||
|
|
734
|
+
message.includes("quota unavailable") ||
|
|
735
|
+
message.includes("rate limits unavailable")
|
|
736
|
+
);
|
|
737
|
+
});
|
|
729
738
|
}
|
|
730
739
|
|
|
731
740
|
function selectPrimaryCodexSnapshot(
|
|
732
|
-
|
|
741
|
+
report: CodexUsageReport,
|
|
733
742
|
): NormalizedRateLimitSnapshot | undefined {
|
|
734
|
-
|
|
743
|
+
return report.snapshots.find(isPrimaryCodexSnapshot) ?? report.snapshots[0];
|
|
735
744
|
}
|
|
736
745
|
|
|
737
746
|
function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
747
|
+
const key = value
|
|
748
|
+
?.toLowerCase()
|
|
749
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
750
|
+
.replace(/^-+|-+$/g, "");
|
|
751
|
+
return key || undefined;
|
|
743
752
|
}
|
|
744
753
|
|
|
745
754
|
function formatDualLimitBar(
|
|
746
|
-
|
|
747
|
-
|
|
755
|
+
primary: NormalizedRateLimitWindow | undefined,
|
|
756
|
+
secondary: NormalizedRateLimitWindow | undefined,
|
|
748
757
|
): string {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
758
|
+
const primaryParts = filledTenths(primary);
|
|
759
|
+
const secondaryParts = filledTenths(secondary);
|
|
760
|
+
let value = "";
|
|
761
|
+
for (let index = 0; index < 5; index++) {
|
|
762
|
+
const leftPart = index * 2 + 1;
|
|
763
|
+
const rightPart = leftPart + 1;
|
|
764
|
+
let mask = 0;
|
|
765
|
+
if (primaryParts >= leftPart) mask |= 1;
|
|
766
|
+
if (primaryParts >= rightPart) mask |= 2;
|
|
767
|
+
if (secondaryParts >= leftPart) mask |= 4;
|
|
768
|
+
if (secondaryParts >= rightPart) mask |= 8;
|
|
769
|
+
value += DUAL_BAR_CHARS[mask];
|
|
770
|
+
}
|
|
771
|
+
return value;
|
|
763
772
|
}
|
|
764
773
|
|
|
765
774
|
function filledTenths(window: NormalizedRateLimitWindow | undefined): number {
|
|
766
|
-
|
|
767
|
-
|
|
775
|
+
if (!window) return 0;
|
|
776
|
+
return Math.round(remainingPercent(window) / 10);
|
|
768
777
|
}
|
|
769
778
|
|
|
770
779
|
function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
771
|
-
|
|
780
|
+
return 100 - clampPercent(window.usedPercent);
|
|
772
781
|
}
|
|
773
782
|
|
|
774
783
|
function isPrimaryCodexSnapshot(
|
|
775
|
-
|
|
784
|
+
snapshot: NormalizedRateLimitSnapshot,
|
|
776
785
|
): boolean {
|
|
777
|
-
|
|
786
|
+
return normalizedUsageKey(snapshot.limitId) === "codex";
|
|
778
787
|
}
|
|
779
788
|
|
|
780
789
|
function clampPercent(value: number): number {
|
|
781
|
-
|
|
782
|
-
|
|
790
|
+
if (!Number.isFinite(value)) return 0;
|
|
791
|
+
return Math.min(100, Math.max(0, value));
|
|
783
792
|
}
|
|
784
793
|
|
|
785
794
|
function parseJsonObject(
|
|
786
|
-
|
|
787
|
-
|
|
795
|
+
text: string,
|
|
796
|
+
description: string,
|
|
788
797
|
): Record<string, unknown> {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
+
let parsed: unknown;
|
|
799
|
+
try {
|
|
800
|
+
parsed = JSON.parse(text) as unknown;
|
|
801
|
+
} catch (error) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`${description} was not valid JSON: ${errorMessage(error)}`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return assertObject(parsed, description);
|
|
798
807
|
}
|
|
799
808
|
|
|
800
809
|
function assertObject(
|
|
801
|
-
|
|
802
|
-
|
|
810
|
+
value: unknown,
|
|
811
|
+
description: string,
|
|
803
812
|
): Record<string, unknown> {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
813
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
814
|
+
throw new Error(`${description} was not an object.`);
|
|
815
|
+
}
|
|
816
|
+
return value as Record<string, unknown>;
|
|
808
817
|
}
|
|
809
818
|
|
|
810
819
|
function asString(value: unknown): string | undefined {
|
|
811
|
-
|
|
820
|
+
return typeof value === "string" ? value : undefined;
|
|
812
821
|
}
|
|
813
822
|
|
|
814
823
|
function asNumber(value: unknown): number | undefined {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
824
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
825
|
+
if (typeof value === "string" && value.trim()) {
|
|
826
|
+
const parsed = Number(value);
|
|
827
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
828
|
+
}
|
|
829
|
+
return undefined;
|
|
821
830
|
}
|
|
822
831
|
|
|
823
832
|
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
833
|
+
return Object.keys(headers).some(
|
|
834
|
+
(key) => key.toLowerCase() === name.toLowerCase(),
|
|
835
|
+
);
|
|
827
836
|
}
|
|
828
837
|
|
|
829
838
|
function redactErrorBody(body: string): string {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
839
|
+
return truncateEnd(
|
|
840
|
+
body
|
|
841
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
842
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
843
|
+
.trim(),
|
|
844
|
+
MAX_ERROR_BODY_CHARS,
|
|
845
|
+
);
|
|
837
846
|
}
|
|
838
847
|
|
|
839
848
|
function truncateEnd(value: string, maxChars: number): string {
|
|
840
|
-
|
|
841
|
-
|
|
849
|
+
if (value.length <= maxChars) return value;
|
|
850
|
+
return `${value.slice(0, maxChars - 1)}โฆ`;
|
|
842
851
|
}
|
|
843
852
|
|
|
844
853
|
function errorMessage(error: unknown): string {
|
|
845
|
-
|
|
854
|
+
return error instanceof Error ? error.message : String(error);
|
|
846
855
|
}
|