@llblab/pi-codex-usage 0.3.3 → 0.3.4
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 +1 -0
- package/README.md +3 -1
- package/banner.png +0 -0
- package/index.ts +658 -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,815 @@ 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
|
+
failedRefreshes = 0;
|
|
226
|
+
cache = { createdAt: Date.now(), report: result.report };
|
|
227
|
+
setUsageStatusline(ctx, result.report, {
|
|
228
|
+
autoRefresh: true,
|
|
229
|
+
blink: cache !== undefined,
|
|
230
|
+
model,
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
pi.on("session_start", (_event, ctx) => {
|
|
235
|
+
if (isOpenAICodexModel(ctx.model))
|
|
236
|
+
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
237
|
+
else clearUsageStatusline(ctx);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
241
|
+
if (isOpenAICodexModel(ctx.model))
|
|
242
|
+
void refreshCurrentCodexUsageStatusline(ctx, false);
|
|
243
|
+
else clearUsageStatusline(ctx);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.on("model_select", (event, ctx) => {
|
|
247
|
+
if (isOpenAICodexModel(event.model)) {
|
|
248
|
+
void refreshCurrentCodexUsageStatusline(ctx, false, event.model);
|
|
249
|
+
} else {
|
|
250
|
+
clearUsageStatusline(ctx);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
pi.on("session_shutdown", (_event, ctx) => clearUsageStatusline(ctx));
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
function isOpenAICodexModel(
|
|
252
|
-
|
|
258
|
+
model: Pick<PiModel, "provider"> | undefined,
|
|
253
259
|
): boolean {
|
|
254
|
-
|
|
260
|
+
return model?.provider === CODEX_PROVIDER_ID;
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
async function queryUsage(
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
ctx: ExtensionContext,
|
|
265
|
+
options: Pick<QueryUsageOptions, "timeoutMs">,
|
|
260
266
|
): Promise<QueryUsageResult> {
|
|
261
|
-
|
|
267
|
+
const errors: UsageQueryError[] = [];
|
|
262
268
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
try {
|
|
270
|
+
const report = await queryViaPiAuth(ctx, options.timeoutMs);
|
|
271
|
+
return { ok: true, report };
|
|
272
|
+
} catch (cause) {
|
|
273
|
+
errors.push({ source: "pi-auth", message: errorMessage(cause), cause });
|
|
274
|
+
}
|
|
269
275
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
try {
|
|
277
|
+
const report = await queryViaCodexAppServer(options.timeoutMs);
|
|
278
|
+
return { ok: true, report };
|
|
279
|
+
} catch (cause) {
|
|
280
|
+
errors.push({
|
|
281
|
+
source: "codex-app-server",
|
|
282
|
+
message: errorMessage(cause),
|
|
283
|
+
cause,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
280
286
|
|
|
281
|
-
|
|
287
|
+
return { ok: false, errors };
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
async function queryViaPiAuth(
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
ctx: ExtensionContext,
|
|
292
|
+
timeoutMs: number,
|
|
287
293
|
): 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
|
-
|
|
294
|
+
const auth = await resolvePiCodexAuth(ctx);
|
|
295
|
+
if (!auth) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
"No Pi OpenAI Codex subscription auth was available. Use a Pi OpenAI Codex model or run /login for OpenAI ChatGPT Plus/Pro (Codex).",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const response = await fetchWithTimeout(
|
|
302
|
+
CODEX_USAGE_URL,
|
|
303
|
+
{ headers: auth.headers },
|
|
304
|
+
timeoutMs,
|
|
305
|
+
);
|
|
306
|
+
const text = await response.text();
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Codex usage endpoint returned ${response.status} ${response.statusText}: ${redactErrorBody(text)}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const payload = parseJsonObject(text, "Codex usage endpoint response");
|
|
314
|
+
return normalizeBackendPayload(
|
|
315
|
+
payload as RateLimitStatusPayload,
|
|
316
|
+
Date.now(),
|
|
317
|
+
"pi-auth",
|
|
318
|
+
);
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
async function resolvePiCodexAuth(
|
|
316
|
-
|
|
322
|
+
ctx: ExtensionContext,
|
|
317
323
|
): 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
|
-
|
|
324
|
+
const models = codexAuthCandidateModels(ctx);
|
|
325
|
+
const errors: string[] = [];
|
|
326
|
+
|
|
327
|
+
for (const model of models) {
|
|
328
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
329
|
+
if (!auth.ok) {
|
|
330
|
+
errors.push(auth.error);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const headers = { ...(auth.headers ?? {}) };
|
|
335
|
+
if (!hasHeader(headers, "Authorization") && auth.apiKey) {
|
|
336
|
+
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
337
|
+
}
|
|
338
|
+
if (!hasHeader(headers, "User-Agent")) {
|
|
339
|
+
headers["User-Agent"] = "pi-codex-usage";
|
|
340
|
+
}
|
|
341
|
+
if (hasHeader(headers, "Authorization")) {
|
|
342
|
+
return { headers };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (errors.length > 0) {
|
|
347
|
+
throw new Error(errors.join("; "));
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
function codexAuthCandidateModels(ctx: ExtensionContext): PiModel[] {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
353
|
+
const candidates: PiModel[] = [];
|
|
354
|
+
const seen = new Set<string>();
|
|
355
|
+
const add = (model: PiModel | undefined) => {
|
|
356
|
+
if (!model || model.provider !== CODEX_PROVIDER_ID) return;
|
|
357
|
+
const key = `${model.provider}/${model.id}`;
|
|
358
|
+
if (seen.has(key)) return;
|
|
359
|
+
seen.add(key);
|
|
360
|
+
candidates.push(model);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
add(ctx.model);
|
|
364
|
+
for (const model of ctx.modelRegistry.getAvailable()) add(model);
|
|
365
|
+
for (const model of ctx.modelRegistry.getAll()) add(model);
|
|
366
|
+
return candidates;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
async function fetchWithTimeout(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
url: string,
|
|
371
|
+
init: RequestInit,
|
|
372
|
+
timeoutMs: number,
|
|
367
373
|
): Promise<Response> {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
376
|
+
try {
|
|
377
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (controller.signal.aborted) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching Codex usage.`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
} finally {
|
|
386
|
+
clearTimeout(timeout);
|
|
387
|
+
}
|
|
382
388
|
}
|
|
383
389
|
|
|
384
390
|
async function queryViaCodexAppServer(
|
|
385
|
-
|
|
391
|
+
timeoutMs: number,
|
|
386
392
|
): 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
|
-
|
|
393
|
+
const client = new CodexAppServerClient(timeoutMs);
|
|
394
|
+
try {
|
|
395
|
+
await client.start();
|
|
396
|
+
await client.request("initialize", {
|
|
397
|
+
clientInfo: {
|
|
398
|
+
name: "pi_codex_usage",
|
|
399
|
+
title: "Pi Codex Usage",
|
|
400
|
+
version: "0.1.0",
|
|
401
|
+
},
|
|
402
|
+
capabilities: {
|
|
403
|
+
experimentalApi: false,
|
|
404
|
+
requestAttestation: false,
|
|
405
|
+
optOutNotificationMethods: [],
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
client.notify("initialized");
|
|
409
|
+
const result = await client.request("account/rateLimits/read", undefined);
|
|
410
|
+
return normalizeAppServerResponse(
|
|
411
|
+
assertObject(
|
|
412
|
+
result,
|
|
413
|
+
"account/rateLimits/read result",
|
|
414
|
+
) as AppServerRateLimitResponse,
|
|
415
|
+
Date.now(),
|
|
416
|
+
);
|
|
417
|
+
} finally {
|
|
418
|
+
client.dispose();
|
|
419
|
+
}
|
|
414
420
|
}
|
|
415
421
|
|
|
416
422
|
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
|
-
|
|
423
|
+
private child?: ChildProcessWithoutNullStreams;
|
|
424
|
+
private nextId = 1;
|
|
425
|
+
private stderr = "";
|
|
426
|
+
private readonly pending = new Map<number, PendingRpc>();
|
|
427
|
+
private startPromise?: Promise<void>;
|
|
428
|
+
private exitError?: Error;
|
|
429
|
+
private readonly timeoutMs: number;
|
|
430
|
+
|
|
431
|
+
constructor(timeoutMs: number) {
|
|
432
|
+
this.timeoutMs = timeoutMs;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
start(): Promise<void> {
|
|
436
|
+
if (this.startPromise) return this.startPromise;
|
|
437
|
+
|
|
438
|
+
this.startPromise = new Promise((resolve, reject) => {
|
|
439
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
440
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
441
|
+
});
|
|
442
|
+
this.child = child;
|
|
443
|
+
|
|
444
|
+
const startupTimeout = setTimeout(() => {
|
|
445
|
+
reject(
|
|
446
|
+
new Error(
|
|
447
|
+
`Timed out after ${Math.round(this.timeoutMs / 1000)}s starting codex app-server.`,
|
|
448
|
+
),
|
|
449
|
+
);
|
|
450
|
+
}, this.timeoutMs);
|
|
451
|
+
|
|
452
|
+
child.once("spawn", () => {
|
|
453
|
+
clearTimeout(startupTimeout);
|
|
454
|
+
resolve();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
child.once("error", (error) => {
|
|
458
|
+
clearTimeout(startupTimeout);
|
|
459
|
+
reject(new Error(`Failed to start codex app-server: ${error.message}`));
|
|
460
|
+
this.rejectAll(error);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
child.once("exit", (code, signal) => {
|
|
464
|
+
const suffix = this.stderr
|
|
465
|
+
? ` stderr: ${redactErrorBody(this.stderr)}`
|
|
466
|
+
: "";
|
|
467
|
+
this.exitError = new Error(
|
|
468
|
+
`codex app-server exited before completing the request (code ${code ?? "unknown"}, signal ${signal ?? "none"}).${suffix}`,
|
|
469
|
+
);
|
|
470
|
+
this.rejectAll(this.exitError);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
child.stderr.setEncoding("utf8");
|
|
474
|
+
child.stderr.on("data", (chunk: string) => {
|
|
475
|
+
this.stderr = truncateEnd(this.stderr + chunk, MAX_ERROR_BODY_CHARS);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const lines = createInterface({ input: child.stdout });
|
|
479
|
+
lines.on("line", (line) => this.handleLine(line));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return this.startPromise;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
request(method: string, params: unknown): Promise<unknown> {
|
|
486
|
+
const child = this.child;
|
|
487
|
+
if (!child?.stdin.writable) {
|
|
488
|
+
throw new Error("codex app-server is not running.");
|
|
489
|
+
}
|
|
490
|
+
if (this.exitError) throw this.exitError;
|
|
491
|
+
|
|
492
|
+
const id = this.nextId++;
|
|
493
|
+
const payload =
|
|
494
|
+
params === undefined ? { method, id } : { method, id, params };
|
|
495
|
+
const response = new Promise<unknown>((resolve, reject) => {
|
|
496
|
+
const timeout = setTimeout(() => {
|
|
497
|
+
this.pending.delete(id);
|
|
498
|
+
reject(
|
|
499
|
+
new Error(
|
|
500
|
+
`Timed out after ${Math.round(this.timeoutMs / 1000)}s waiting for ${method}.`,
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}, this.timeoutMs);
|
|
504
|
+
|
|
505
|
+
this.pending.set(id, {
|
|
506
|
+
resolve: (value) => {
|
|
507
|
+
clearTimeout(timeout);
|
|
508
|
+
resolve(value);
|
|
509
|
+
},
|
|
510
|
+
reject: (error) => {
|
|
511
|
+
clearTimeout(timeout);
|
|
512
|
+
reject(error);
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
518
|
+
return response;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
notify(method: string): void {
|
|
522
|
+
const child = this.child;
|
|
523
|
+
if (!child?.stdin.writable) return;
|
|
524
|
+
child.stdin.write(`${JSON.stringify({ method })}\n`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
dispose(): void {
|
|
528
|
+
for (const [id, pending] of this.pending) {
|
|
529
|
+
pending.reject(new Error(`codex app-server request ${id} cancelled.`));
|
|
530
|
+
}
|
|
531
|
+
this.pending.clear();
|
|
532
|
+
|
|
533
|
+
const child = this.child;
|
|
534
|
+
if (!child) return;
|
|
535
|
+
child.stdin.end();
|
|
536
|
+
if (!child.killed) child.kill();
|
|
537
|
+
this.child = undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private handleLine(line: string): void {
|
|
541
|
+
let parsed: RpcResponse;
|
|
542
|
+
try {
|
|
543
|
+
parsed = JSON.parse(line) as RpcResponse;
|
|
544
|
+
} catch {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (typeof parsed.id !== "number") return;
|
|
549
|
+
const pending = this.pending.get(parsed.id);
|
|
550
|
+
if (!pending) return;
|
|
551
|
+
this.pending.delete(parsed.id);
|
|
552
|
+
|
|
553
|
+
if (parsed.error) {
|
|
554
|
+
const message =
|
|
555
|
+
typeof parsed.error.message === "string"
|
|
556
|
+
? parsed.error.message
|
|
557
|
+
: "unknown error";
|
|
558
|
+
pending.reject(new Error(`codex app-server request failed: ${message}`));
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
pending.resolve(parsed.result);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private rejectAll(error: Error): void {
|
|
566
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
567
|
+
this.pending.clear();
|
|
568
|
+
}
|
|
563
569
|
}
|
|
564
570
|
|
|
565
571
|
export function normalizeBackendPayload(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
572
|
+
payload: RateLimitStatusPayload,
|
|
573
|
+
_capturedAt: number,
|
|
574
|
+
_source: UsageSource,
|
|
569
575
|
): CodexUsageReport {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
576
|
+
const snapshot = normalizeBackendSnapshot("codex", payload.rate_limit);
|
|
577
|
+
if (!snapshot) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
"Codex usage endpoint returned no displayable rate-limit windows.",
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return { snapshots: [snapshot] };
|
|
577
583
|
}
|
|
578
584
|
|
|
579
585
|
function normalizeBackendSnapshot(
|
|
580
|
-
|
|
581
|
-
|
|
586
|
+
limitId: string,
|
|
587
|
+
rateLimit: unknown,
|
|
582
588
|
): NormalizedRateLimitSnapshot | undefined {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
if (rateLimit === null || rateLimit === undefined) return undefined;
|
|
590
|
+
const details = assertObject(
|
|
591
|
+
rateLimit,
|
|
592
|
+
"rate limit",
|
|
593
|
+
) as BackendRateLimitDetails;
|
|
594
|
+
const primary = normalizeBackendWindow(details.primary_window);
|
|
595
|
+
const secondary = normalizeBackendWindow(details.secondary_window);
|
|
596
|
+
if (!primary && !secondary) return undefined;
|
|
597
|
+
return { limitId, primary, secondary };
|
|
592
598
|
}
|
|
593
599
|
|
|
594
600
|
function normalizeBackendWindow(
|
|
595
|
-
|
|
601
|
+
value: unknown,
|
|
596
602
|
): NormalizedRateLimitWindow | undefined {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
603
|
+
if (value === null || value === undefined) return undefined;
|
|
604
|
+
const window = assertObject(
|
|
605
|
+
value,
|
|
606
|
+
"rate-limit window",
|
|
607
|
+
) as BackendWindowSnapshot;
|
|
608
|
+
const usedPercent = asNumber(window.used_percent);
|
|
609
|
+
if (usedPercent === undefined) return undefined;
|
|
610
|
+
return { usedPercent };
|
|
605
611
|
}
|
|
606
612
|
|
|
607
613
|
export function normalizeAppServerResponse(
|
|
608
|
-
|
|
609
|
-
|
|
614
|
+
response: AppServerRateLimitResponse,
|
|
615
|
+
_capturedAt: number,
|
|
610
616
|
): CodexUsageReport {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
617
|
+
const snapshots: NormalizedRateLimitSnapshot[] = [];
|
|
618
|
+
const addSnapshot = (raw: unknown, fallbackId: string) => {
|
|
619
|
+
const snapshot = normalizeAppServerSnapshot(raw, fallbackId);
|
|
620
|
+
if (!snapshot) return;
|
|
621
|
+
const existingIndex = snapshots.findIndex(
|
|
622
|
+
(item) => item.limitId === snapshot.limitId,
|
|
623
|
+
);
|
|
624
|
+
if (existingIndex >= 0)
|
|
625
|
+
snapshots[existingIndex] = mergeSnapshot(
|
|
626
|
+
snapshots[existingIndex],
|
|
627
|
+
snapshot,
|
|
628
|
+
);
|
|
629
|
+
else snapshots.push(snapshot);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
addSnapshot(response.rateLimits, "codex");
|
|
633
|
+
if (snapshots.length === 0) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
"codex app-server returned no displayable rate-limit windows.",
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return { snapshots };
|
|
634
640
|
}
|
|
635
641
|
|
|
636
642
|
function normalizeAppServerSnapshot(
|
|
637
|
-
|
|
638
|
-
|
|
643
|
+
raw: unknown,
|
|
644
|
+
fallbackId: string,
|
|
639
645
|
): NormalizedRateLimitSnapshot | undefined {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
646
|
+
if (raw === null || raw === undefined) return undefined;
|
|
647
|
+
const snapshot = assertObject(
|
|
648
|
+
raw,
|
|
649
|
+
"app-server rate-limit snapshot",
|
|
650
|
+
) as AppServerRateLimitSnapshot;
|
|
651
|
+
const limitId = asString(snapshot.limitId) ?? fallbackId;
|
|
652
|
+
const primary = normalizeAppServerWindow(snapshot.primary);
|
|
653
|
+
const secondary = normalizeAppServerWindow(snapshot.secondary);
|
|
654
|
+
if (!primary && !secondary) return undefined;
|
|
655
|
+
return { limitId, primary, secondary };
|
|
650
656
|
}
|
|
651
657
|
|
|
652
658
|
function normalizeAppServerWindow(
|
|
653
|
-
|
|
659
|
+
value: unknown,
|
|
654
660
|
): NormalizedRateLimitWindow | undefined {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
if (value === null || value === undefined) return undefined;
|
|
662
|
+
const window = assertObject(
|
|
663
|
+
value,
|
|
664
|
+
"app-server rate-limit window",
|
|
665
|
+
) as AppServerWindowSnapshot;
|
|
666
|
+
const usedPercent = asNumber(window.usedPercent);
|
|
667
|
+
if (usedPercent === undefined) return undefined;
|
|
668
|
+
return { usedPercent };
|
|
663
669
|
}
|
|
664
670
|
|
|
665
671
|
function mergeSnapshot(
|
|
666
|
-
|
|
667
|
-
|
|
672
|
+
left: NormalizedRateLimitSnapshot,
|
|
673
|
+
right: NormalizedRateLimitSnapshot,
|
|
668
674
|
): NormalizedRateLimitSnapshot {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
675
|
+
return {
|
|
676
|
+
limitId: right.limitId || left.limitId,
|
|
677
|
+
primary: right.primary ?? left.primary,
|
|
678
|
+
secondary: right.secondary ?? left.secondary,
|
|
679
|
+
};
|
|
674
680
|
}
|
|
675
681
|
|
|
676
682
|
export function formatCodexUsageStatusline(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
683
|
+
report: CodexUsageReport,
|
|
684
|
+
ctx: ExtensionContext,
|
|
685
|
+
_model?: CodexUsageModel,
|
|
680
686
|
): string {
|
|
681
|
-
|
|
682
|
-
|
|
687
|
+
const snapshot = selectPrimaryCodexSnapshot(report);
|
|
688
|
+
if (!snapshot) return formatStatuslineText(ctx, "n/a");
|
|
683
689
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
690
|
+
if (!snapshot.primary && !snapshot.secondary)
|
|
691
|
+
return formatStatuslineText(ctx, "n/a");
|
|
692
|
+
return formatStatuslineText(
|
|
693
|
+
ctx,
|
|
694
|
+
`[${formatDualLimitBar(snapshot.primary, snapshot.secondary)}]`,
|
|
695
|
+
);
|
|
689
696
|
}
|
|
690
697
|
|
|
691
698
|
function formatStatuslineText(ctx: ExtensionContext, value: string): string {
|
|
692
|
-
|
|
693
|
-
|
|
699
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
700
|
+
return `${label} ${ctx.ui.theme.fg("muted", value)}`;
|
|
694
701
|
}
|
|
695
702
|
|
|
696
703
|
function formatEmptyStatuslineBar(ctx: ExtensionContext): string {
|
|
697
|
-
|
|
704
|
+
return formatStatuslineText(ctx, "[\u00a0\u00a0\u00a0\u00a0\u00a0]");
|
|
698
705
|
}
|
|
699
706
|
|
|
700
707
|
function formatStatuslineProblem(
|
|
701
|
-
|
|
702
|
-
|
|
708
|
+
ctx: ExtensionContext,
|
|
709
|
+
errors: UsageQueryError[],
|
|
703
710
|
): string {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
711
|
+
const label = ctx.ui.theme.fg("accent", STATUS_LABEL_TEXT);
|
|
712
|
+
const value = isUnavailable(errors)
|
|
713
|
+
? ctx.ui.theme.fg("muted", "n/a")
|
|
714
|
+
: ctx.ui.theme.fg("error", "error");
|
|
715
|
+
return `${label} ${value}`;
|
|
709
716
|
}
|
|
710
717
|
|
|
711
718
|
function isUnavailable(errors: UsageQueryError[]): boolean {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
719
|
+
return errors.some((error) => {
|
|
720
|
+
const message = error.message.toLowerCase();
|
|
721
|
+
return (
|
|
722
|
+
message.includes("no pi openai codex subscription auth") ||
|
|
723
|
+
message.includes("no displayable rate-limit windows") ||
|
|
724
|
+
message.includes("returned no displayable rate-limit windows") ||
|
|
725
|
+
message.includes("returned 401") ||
|
|
726
|
+
message.includes("returned 403") ||
|
|
727
|
+
message.includes("unauthorized") ||
|
|
728
|
+
message.includes("forbidden") ||
|
|
729
|
+
message.includes("subscription") ||
|
|
730
|
+
message.includes("no active plan") ||
|
|
731
|
+
message.includes("plan unavailable") ||
|
|
732
|
+
message.includes("quota unavailable") ||
|
|
733
|
+
message.includes("rate limits unavailable")
|
|
734
|
+
);
|
|
735
|
+
});
|
|
729
736
|
}
|
|
730
737
|
|
|
731
738
|
function selectPrimaryCodexSnapshot(
|
|
732
|
-
|
|
739
|
+
report: CodexUsageReport,
|
|
733
740
|
): NormalizedRateLimitSnapshot | undefined {
|
|
734
|
-
|
|
741
|
+
return report.snapshots.find(isPrimaryCodexSnapshot) ?? report.snapshots[0];
|
|
735
742
|
}
|
|
736
743
|
|
|
737
744
|
function normalizedUsageKey(value: string | undefined): string | undefined {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
745
|
+
const key = value
|
|
746
|
+
?.toLowerCase()
|
|
747
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
748
|
+
.replace(/^-+|-+$/g, "");
|
|
749
|
+
return key || undefined;
|
|
743
750
|
}
|
|
744
751
|
|
|
745
752
|
function formatDualLimitBar(
|
|
746
|
-
|
|
747
|
-
|
|
753
|
+
primary: NormalizedRateLimitWindow | undefined,
|
|
754
|
+
secondary: NormalizedRateLimitWindow | undefined,
|
|
748
755
|
): string {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
756
|
+
const primaryParts = filledTenths(primary);
|
|
757
|
+
const secondaryParts = filledTenths(secondary);
|
|
758
|
+
let value = "";
|
|
759
|
+
for (let index = 0; index < 5; index++) {
|
|
760
|
+
const leftPart = index * 2 + 1;
|
|
761
|
+
const rightPart = leftPart + 1;
|
|
762
|
+
let mask = 0;
|
|
763
|
+
if (primaryParts >= leftPart) mask |= 1;
|
|
764
|
+
if (primaryParts >= rightPart) mask |= 2;
|
|
765
|
+
if (secondaryParts >= leftPart) mask |= 4;
|
|
766
|
+
if (secondaryParts >= rightPart) mask |= 8;
|
|
767
|
+
value += DUAL_BAR_CHARS[mask];
|
|
768
|
+
}
|
|
769
|
+
return value;
|
|
763
770
|
}
|
|
764
771
|
|
|
765
772
|
function filledTenths(window: NormalizedRateLimitWindow | undefined): number {
|
|
766
|
-
|
|
767
|
-
|
|
773
|
+
if (!window) return 0;
|
|
774
|
+
return Math.round(remainingPercent(window) / 10);
|
|
768
775
|
}
|
|
769
776
|
|
|
770
777
|
function remainingPercent(window: NormalizedRateLimitWindow): number {
|
|
771
|
-
|
|
778
|
+
return 100 - clampPercent(window.usedPercent);
|
|
772
779
|
}
|
|
773
780
|
|
|
774
781
|
function isPrimaryCodexSnapshot(
|
|
775
|
-
|
|
782
|
+
snapshot: NormalizedRateLimitSnapshot,
|
|
776
783
|
): boolean {
|
|
777
|
-
|
|
784
|
+
return normalizedUsageKey(snapshot.limitId) === "codex";
|
|
778
785
|
}
|
|
779
786
|
|
|
780
787
|
function clampPercent(value: number): number {
|
|
781
|
-
|
|
782
|
-
|
|
788
|
+
if (!Number.isFinite(value)) return 0;
|
|
789
|
+
return Math.min(100, Math.max(0, value));
|
|
783
790
|
}
|
|
784
791
|
|
|
785
792
|
function parseJsonObject(
|
|
786
|
-
|
|
787
|
-
|
|
793
|
+
text: string,
|
|
794
|
+
description: string,
|
|
788
795
|
): Record<string, unknown> {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
let parsed: unknown;
|
|
797
|
+
try {
|
|
798
|
+
parsed = JSON.parse(text) as unknown;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
`${description} was not valid JSON: ${errorMessage(error)}`,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
return assertObject(parsed, description);
|
|
798
805
|
}
|
|
799
806
|
|
|
800
807
|
function assertObject(
|
|
801
|
-
|
|
802
|
-
|
|
808
|
+
value: unknown,
|
|
809
|
+
description: string,
|
|
803
810
|
): Record<string, unknown> {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
811
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
812
|
+
throw new Error(`${description} was not an object.`);
|
|
813
|
+
}
|
|
814
|
+
return value as Record<string, unknown>;
|
|
808
815
|
}
|
|
809
816
|
|
|
810
817
|
function asString(value: unknown): string | undefined {
|
|
811
|
-
|
|
818
|
+
return typeof value === "string" ? value : undefined;
|
|
812
819
|
}
|
|
813
820
|
|
|
814
821
|
function asNumber(value: unknown): number | undefined {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
822
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
823
|
+
if (typeof value === "string" && value.trim()) {
|
|
824
|
+
const parsed = Number(value);
|
|
825
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
826
|
+
}
|
|
827
|
+
return undefined;
|
|
821
828
|
}
|
|
822
829
|
|
|
823
830
|
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
831
|
+
return Object.keys(headers).some(
|
|
832
|
+
(key) => key.toLowerCase() === name.toLowerCase(),
|
|
833
|
+
);
|
|
827
834
|
}
|
|
828
835
|
|
|
829
836
|
function redactErrorBody(body: string): string {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
+
return truncateEnd(
|
|
838
|
+
body
|
|
839
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
840
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/gi, '"access_token":"<redacted>"')
|
|
841
|
+
.trim(),
|
|
842
|
+
MAX_ERROR_BODY_CHARS,
|
|
843
|
+
);
|
|
837
844
|
}
|
|
838
845
|
|
|
839
846
|
function truncateEnd(value: string, maxChars: number): string {
|
|
840
|
-
|
|
841
|
-
|
|
847
|
+
if (value.length <= maxChars) return value;
|
|
848
|
+
return `${value.slice(0, maxChars - 1)}…`;
|
|
842
849
|
}
|
|
843
850
|
|
|
844
851
|
function errorMessage(error: unknown): string {
|
|
845
|
-
|
|
852
|
+
return error instanceof Error ? error.message : String(error);
|
|
846
853
|
}
|