@99percentpeople/pi-codex-api 0.1.4 → 0.2.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 +106 -178
- package/dist/index.ts +309 -28
- package/dist/index.ts.map +7 -7
- package/package.json +1 -1
package/dist/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ var DEFAULT_CODEX_API_CONFIG = {
|
|
|
11
11
|
searchMode: "auto",
|
|
12
12
|
searchContextSize: "medium",
|
|
13
13
|
imageQuality: "auto",
|
|
14
|
-
usageStatus: true
|
|
14
|
+
usageStatus: true,
|
|
15
|
+
usagePollInterval: 5
|
|
15
16
|
};
|
|
16
17
|
function oneOf(value, values, fallback) {
|
|
17
18
|
return typeof value === "string" && values.includes(value) ? value : fallback;
|
|
@@ -26,7 +27,8 @@ function normalizeCodexApiConfig(value) {
|
|
|
26
27
|
searchMode: oneOf(input.searchMode, ["auto", "cached", "indexed", "live"], DEFAULT_CODEX_API_CONFIG.searchMode),
|
|
27
28
|
searchContextSize: oneOf(input.searchContextSize, ["low", "medium", "high"], DEFAULT_CODEX_API_CONFIG.searchContextSize),
|
|
28
29
|
imageQuality: oneOf(input.imageQuality, ["auto", "low", "medium", "high"], DEFAULT_CODEX_API_CONFIG.imageQuality),
|
|
29
|
-
usageStatus: typeof input.usageStatus === "boolean" ? input.usageStatus : DEFAULT_CODEX_API_CONFIG.usageStatus
|
|
30
|
+
usageStatus: typeof input.usageStatus === "boolean" ? input.usageStatus : DEFAULT_CODEX_API_CONFIG.usageStatus,
|
|
31
|
+
usagePollInterval: typeof input.usagePollInterval === "number" && Number.isFinite(input.usagePollInterval) && input.usagePollInterval > 0 ? Math.min(60, Math.round(input.usagePollInterval)) : DEFAULT_CODEX_API_CONFIG.usagePollInterval
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
function getCodexApiConfigPath() {
|
|
@@ -381,7 +383,7 @@ function imagePhaseLabel(phase) {
|
|
|
381
383
|
return "Saving generated PNG…";
|
|
382
384
|
return "Image completed";
|
|
383
385
|
}
|
|
384
|
-
function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG
|
|
386
|
+
function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG) {
|
|
385
387
|
pi.registerTool({
|
|
386
388
|
name: "codex_image",
|
|
387
389
|
label: "Codex Image",
|
|
@@ -466,7 +468,6 @@ function registerCodexImageTool(pi, getConfig = () => DEFAULT_CODEX_API_CONFIG,
|
|
|
466
468
|
update("saving");
|
|
467
469
|
await mkdir(dirname(savedPath), { recursive: true });
|
|
468
470
|
await writeFile(savedPath, Buffer.from(data, "base64"));
|
|
469
|
-
refreshUsageInBackground?.(ctx);
|
|
470
471
|
return {
|
|
471
472
|
content: [
|
|
472
473
|
{ type: "text", text: `${operation === "edit" ? "Edited" : "Generated"} image saved to ${savedPath}` },
|
|
@@ -1113,6 +1114,15 @@ var IMAGE_QUALITY_LABELS = {
|
|
|
1113
1114
|
medium: "Medium",
|
|
1114
1115
|
high: "High"
|
|
1115
1116
|
};
|
|
1117
|
+
function usagePollLabel(minutes) {
|
|
1118
|
+
if (minutes <= 0)
|
|
1119
|
+
return "Off";
|
|
1120
|
+
return `${minutes}m`;
|
|
1121
|
+
}
|
|
1122
|
+
function usagePollMinutes(label) {
|
|
1123
|
+
const match = /^(\d+)m$/.exec(label);
|
|
1124
|
+
return match ? Number(match[1]) : 0;
|
|
1125
|
+
}
|
|
1116
1126
|
function keyForLabel(labels, value) {
|
|
1117
1127
|
return Object.entries(labels).find(([, label]) => label === value)?.[0];
|
|
1118
1128
|
}
|
|
@@ -1164,6 +1174,13 @@ function registerCodexApiSettings(pi, controller) {
|
|
|
1164
1174
|
description: "Show remaining Codex subscription usage in the status area",
|
|
1165
1175
|
currentValue: config.usageStatus ? "Show" : "Hide",
|
|
1166
1176
|
values: ["Show", "Hide"]
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
id: "usagePollInterval",
|
|
1180
|
+
label: "Usage poll",
|
|
1181
|
+
description: "Periodically refresh the usage status while a session is active (Off disables polling)",
|
|
1182
|
+
currentValue: usagePollLabel(config.usagePollInterval),
|
|
1183
|
+
values: ["Off", "1m", "5m", "15m"]
|
|
1167
1184
|
}
|
|
1168
1185
|
];
|
|
1169
1186
|
},
|
|
@@ -1190,6 +1207,8 @@ function registerCodexApiSettings(pi, controller) {
|
|
|
1190
1207
|
}, ctx);
|
|
1191
1208
|
} else if (id === "usageStatus") {
|
|
1192
1209
|
controller.updateConfig({ ...config, usageStatus: value === "Show" }, ctx);
|
|
1210
|
+
} else if (id === "usagePollInterval") {
|
|
1211
|
+
controller.updateConfig({ ...config, usagePollInterval: usagePollMinutes(value) }, ctx);
|
|
1193
1212
|
}
|
|
1194
1213
|
}
|
|
1195
1214
|
});
|
|
@@ -1202,6 +1221,11 @@ import {
|
|
|
1202
1221
|
getAgentDir
|
|
1203
1222
|
} from "@earendil-works/pi-coding-agent";
|
|
1204
1223
|
var USAGE_PATH = "../wham/usage";
|
|
1224
|
+
var REDEEM_CREDITS_PATH = "../wham/rate-limit-reset-credits";
|
|
1225
|
+
var REDEEM_PATH = "../wham/rate-limit-reset-credits/consume";
|
|
1226
|
+
var REDEEM_CONFIRM_WINDOW_MS = 1e4;
|
|
1227
|
+
var REDEEM_DIALOG_TIMEOUT_MS = 30000;
|
|
1228
|
+
var REDEEM_RETRY_WINDOW_MS = 5 * 60000;
|
|
1205
1229
|
var USAGE_REFRESH_INTERVAL_MS = 60000;
|
|
1206
1230
|
var AUTH_WATCH_DEBOUNCE_MS = 100;
|
|
1207
1231
|
var STATUS_KEY = "codex-api-usage";
|
|
@@ -1215,6 +1239,18 @@ function payloadNumber(value) {
|
|
|
1215
1239
|
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
1216
1240
|
return Number.isFinite(number) ? number : undefined;
|
|
1217
1241
|
}
|
|
1242
|
+
function payloadDate(value) {
|
|
1243
|
+
if (typeof value === "number")
|
|
1244
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1245
|
+
if (typeof value === "string") {
|
|
1246
|
+
const ms = Date.parse(value);
|
|
1247
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
function payloadString(value) {
|
|
1252
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1253
|
+
}
|
|
1218
1254
|
function payloadBool(value) {
|
|
1219
1255
|
if (typeof value === "boolean")
|
|
1220
1256
|
return value;
|
|
@@ -1253,14 +1289,65 @@ function payloadCredits(value) {
|
|
|
1253
1289
|
balance: typeof balance === "string" && balance ? balance : undefined
|
|
1254
1290
|
};
|
|
1255
1291
|
}
|
|
1256
|
-
function payloadSnapshot(limitId, limitName, rateLimitValue, creditsValue) {
|
|
1292
|
+
function payloadSnapshot(limitId, limitName, rateLimitValue, creditsValue, limitReached) {
|
|
1257
1293
|
const rateLimit = object(rateLimitValue);
|
|
1258
1294
|
return {
|
|
1259
1295
|
limitId,
|
|
1260
1296
|
limitName,
|
|
1261
1297
|
primary: payloadWindow(rateLimit && property(rateLimit, "primary_window", "primaryWindow")),
|
|
1262
1298
|
secondary: payloadWindow(rateLimit && property(rateLimit, "secondary_window", "secondaryWindow")),
|
|
1263
|
-
credits: payloadCredits(creditsValue)
|
|
1299
|
+
credits: payloadCredits(creditsValue),
|
|
1300
|
+
limitReached
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function parseCodexAccountInfo(value) {
|
|
1304
|
+
const input = object(value);
|
|
1305
|
+
if (!input)
|
|
1306
|
+
return;
|
|
1307
|
+
const planType = payloadString(property(input, "plan_type", "planType"));
|
|
1308
|
+
const email = payloadString(property(input, "email", "email"));
|
|
1309
|
+
if (planType === undefined && email === undefined)
|
|
1310
|
+
return;
|
|
1311
|
+
return { planType, email };
|
|
1312
|
+
}
|
|
1313
|
+
function maskCodexEmail(email) {
|
|
1314
|
+
const [local, domain] = email.split("@");
|
|
1315
|
+
if (!domain)
|
|
1316
|
+
return "***";
|
|
1317
|
+
const head = local.length > 3 ? local.slice(0, 3) : local.slice(0, 1);
|
|
1318
|
+
return `${head}***@${domain}`;
|
|
1319
|
+
}
|
|
1320
|
+
function parseCodexRedeemCredits(value) {
|
|
1321
|
+
const input = object(value);
|
|
1322
|
+
if (!input)
|
|
1323
|
+
return;
|
|
1324
|
+
const availableCount = payloadNumber(property(input, "available_count", "availableCount"));
|
|
1325
|
+
if (availableCount === undefined)
|
|
1326
|
+
return;
|
|
1327
|
+
const credits = [];
|
|
1328
|
+
const list = property(input, "credits", "credits");
|
|
1329
|
+
if (Array.isArray(list)) {
|
|
1330
|
+
for (const item of list) {
|
|
1331
|
+
const credit = object(item);
|
|
1332
|
+
if (!credit)
|
|
1333
|
+
continue;
|
|
1334
|
+
const id = payloadString(property(credit, "id", "id"));
|
|
1335
|
+
if (!id)
|
|
1336
|
+
continue;
|
|
1337
|
+
credits.push({
|
|
1338
|
+
id,
|
|
1339
|
+
title: payloadString(property(credit, "title", "title")),
|
|
1340
|
+
description: payloadString(property(credit, "description", "description")),
|
|
1341
|
+
status: payloadString(property(credit, "status", "status")),
|
|
1342
|
+
grantedAt: payloadDate(property(credit, "granted_at", "grantedAt")),
|
|
1343
|
+
expiresAt: payloadDate(property(credit, "expires_at", "expiresAt"))
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
availableCount,
|
|
1349
|
+
totalEarnedCount: payloadNumber(property(input, "total_earned_count", "totalEarnedCount")),
|
|
1350
|
+
credits
|
|
1264
1351
|
};
|
|
1265
1352
|
}
|
|
1266
1353
|
function parseCodexUsagePayload(value) {
|
|
@@ -1268,7 +1355,9 @@ function parseCodexUsagePayload(value) {
|
|
|
1268
1355
|
if (!input)
|
|
1269
1356
|
return [];
|
|
1270
1357
|
const rateLimit = property(input, "rate_limit", "rateLimit");
|
|
1271
|
-
const
|
|
1358
|
+
const rateLimitObject = object(rateLimit);
|
|
1359
|
+
const limitReached = payloadBool(rateLimitObject && property(rateLimitObject, "limit_reached", "limitReached"));
|
|
1360
|
+
const snapshots = rateLimit !== undefined || input.credits !== undefined ? [payloadSnapshot("codex", undefined, rateLimit, input.credits, limitReached)] : [];
|
|
1272
1361
|
const additional = property(input, "additional_rate_limits", "additionalRateLimits");
|
|
1273
1362
|
if (Array.isArray(additional)) {
|
|
1274
1363
|
for (const value2 of additional) {
|
|
@@ -1330,14 +1419,16 @@ function parseCodexRateLimits(input) {
|
|
|
1330
1419
|
unlimited,
|
|
1331
1420
|
balance: headers["x-codex-credits-balance"]
|
|
1332
1421
|
} : undefined;
|
|
1333
|
-
|
|
1422
|
+
const limitReached = prefix === "x-codex" ? headers["x-codex-rate-limit-reached-type"] !== undefined : undefined;
|
|
1423
|
+
if (!primary && !secondary && !credits && !limitReached)
|
|
1334
1424
|
return [];
|
|
1335
1425
|
return [{
|
|
1336
1426
|
limitId: prefix.slice(2).replace(/-/g, "_"),
|
|
1337
1427
|
limitName: headers[`${prefix}-limit-name`],
|
|
1338
1428
|
primary,
|
|
1339
1429
|
secondary,
|
|
1340
|
-
credits
|
|
1430
|
+
credits,
|
|
1431
|
+
limitReached
|
|
1341
1432
|
}];
|
|
1342
1433
|
});
|
|
1343
1434
|
}
|
|
@@ -1350,13 +1441,17 @@ function resetText(epochSeconds, now = Date.now()) {
|
|
|
1350
1441
|
const remainingMs = epochSeconds * 1000 - now;
|
|
1351
1442
|
if (remainingMs <= 0)
|
|
1352
1443
|
return;
|
|
1353
|
-
const
|
|
1354
|
-
if (
|
|
1355
|
-
return `${
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1444
|
+
const totalMinutes = Math.ceil(remainingMs / 60000);
|
|
1445
|
+
if (totalMinutes < 60)
|
|
1446
|
+
return `${totalMinutes}m`;
|
|
1447
|
+
if (totalMinutes < 24 * 60) {
|
|
1448
|
+
const hours2 = Math.floor(totalMinutes / 60);
|
|
1449
|
+
const minutes = totalMinutes % 60;
|
|
1450
|
+
return minutes > 0 ? `${hours2}h ${minutes}m` : `${hours2}h`;
|
|
1451
|
+
}
|
|
1452
|
+
const days = Math.floor(totalMinutes / (24 * 60));
|
|
1453
|
+
const hours = Math.floor(totalMinutes % (24 * 60) / 60);
|
|
1454
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
1360
1455
|
}
|
|
1361
1456
|
var KNOWN_WINDOWS = [
|
|
1362
1457
|
{ minutes: 5 * 60, label: "5h" },
|
|
@@ -1393,10 +1488,11 @@ function usageBar(remaining) {
|
|
|
1393
1488
|
const filled = Math.round(remaining / 100 * USAGE_BAR_WIDTH);
|
|
1394
1489
|
return `[${"█".repeat(filled)}${"░".repeat(USAGE_BAR_WIDTH - filled)}]`;
|
|
1395
1490
|
}
|
|
1396
|
-
function windowText(item, labelWidth, now) {
|
|
1491
|
+
function windowText(item, labelWidth, now, limitReached) {
|
|
1397
1492
|
const reset = resetText(item.window.resetsAt, now);
|
|
1398
1493
|
const remaining = remainingPercent(item.window);
|
|
1399
|
-
|
|
1494
|
+
const state = limitReached ? "limit reached" : `${percent(remaining)}% left`;
|
|
1495
|
+
return `${item.label.padEnd(labelWidth)} ${usageBar(limitReached ? 0 : remaining)} ${state}${reset ? ` resets in ${reset}` : ""}`;
|
|
1400
1496
|
}
|
|
1401
1497
|
function creditsText(credits) {
|
|
1402
1498
|
if (credits.unlimited)
|
|
@@ -1406,11 +1502,41 @@ function creditsText(credits) {
|
|
|
1406
1502
|
}
|
|
1407
1503
|
return "no additional credits";
|
|
1408
1504
|
}
|
|
1409
|
-
function
|
|
1505
|
+
function planLabel(planType) {
|
|
1506
|
+
return planType.charAt(0).toUpperCase() + planType.slice(1);
|
|
1507
|
+
}
|
|
1508
|
+
function formatDateTime(epochMs) {
|
|
1509
|
+
const date = new Date(epochMs);
|
|
1510
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1511
|
+
const offsetMinutes = -new Date(epochMs).getTimezoneOffset();
|
|
1512
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
1513
|
+
const abs = Math.abs(offsetMinutes);
|
|
1514
|
+
const offset = abs % 60 === 0 ? `UTC${sign}${abs / 60}` : `UTC${sign}${Math.floor(abs / 60)}:${pad(abs % 60)}`;
|
|
1515
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())} ${offset}`;
|
|
1516
|
+
}
|
|
1517
|
+
function formatCodexRedeemCredits(redeemCredits, now = Date.now()) {
|
|
1518
|
+
if (!redeemCredits || redeemCredits.availableCount <= 0)
|
|
1519
|
+
return [];
|
|
1520
|
+
const lines = [`rate limit redeem${redeemCredits.availableCount === 1 ? "" : ` ×${redeemCredits.availableCount}`}`];
|
|
1521
|
+
const available = redeemCredits.credits.filter((credit) => credit.status === undefined || credit.status === "available").sort((left, right) => (left.expiresAt ?? Number.POSITIVE_INFINITY) - (right.expiresAt ?? Number.POSITIVE_INFINITY));
|
|
1522
|
+
for (const credit of available) {
|
|
1523
|
+
const details = ["available"];
|
|
1524
|
+
if (credit.expiresAt !== undefined) {
|
|
1525
|
+
details.push(credit.expiresAt > now ? `expires ${formatDateTime(credit.expiresAt)}` : "expired");
|
|
1526
|
+
}
|
|
1527
|
+
lines.push(` ${credit.title ?? "reset credit"} (${details.join(", ")})`);
|
|
1528
|
+
}
|
|
1529
|
+
return lines.length > 1 ? lines : [];
|
|
1530
|
+
}
|
|
1531
|
+
function formatCodexUsage(snapshots, now = Date.now(), extras = {}) {
|
|
1410
1532
|
if (snapshots.length === 0) {
|
|
1411
1533
|
return "No Codex usage data is available. Run /codex-usage with an active Codex subscription model to refresh it.";
|
|
1412
1534
|
}
|
|
1413
1535
|
const lines = ["Codex usage"];
|
|
1536
|
+
if (extras.account) {
|
|
1537
|
+
const plan = extras.account.planType ? planLabel(extras.account.planType) : "unknown plan";
|
|
1538
|
+
lines.push("", `account · ${plan}${extras.account.email ? ` (${maskCodexEmail(extras.account.email)})` : ""}`);
|
|
1539
|
+
}
|
|
1414
1540
|
for (const snapshot of snapshots) {
|
|
1415
1541
|
const name = snapshot.limitName ?? snapshot.limitId;
|
|
1416
1542
|
const windows = activeWindows(snapshot, now);
|
|
@@ -1419,10 +1545,13 @@ function formatCodexUsage(snapshots, now = Date.now()) {
|
|
|
1419
1545
|
if (windows.length === 0)
|
|
1420
1546
|
lines.push(" no active usage windows");
|
|
1421
1547
|
else
|
|
1422
|
-
lines.push(...windows.map((window) => ` ${windowText(window, labelWidth, now)}`));
|
|
1548
|
+
lines.push(...windows.map((window) => ` ${windowText(window, labelWidth, now, snapshot.limitReached === true)}`));
|
|
1423
1549
|
if (snapshot.credits)
|
|
1424
1550
|
lines.push(` ${creditsText(snapshot.credits)}`);
|
|
1425
1551
|
}
|
|
1552
|
+
const redeemLines = formatCodexRedeemCredits(extras.redeemCredits, now);
|
|
1553
|
+
if (redeemLines.length > 0)
|
|
1554
|
+
lines.push("", ...redeemLines);
|
|
1426
1555
|
return lines.join(`
|
|
1427
1556
|
`);
|
|
1428
1557
|
}
|
|
@@ -1441,15 +1570,20 @@ function formatCodexStatus(snapshots, fastMode, now = Date.now()) {
|
|
|
1441
1570
|
return;
|
|
1442
1571
|
const remaining = remainingPercent(shortest.window);
|
|
1443
1572
|
const reset = resetText(shortest.window.resetsAt, now);
|
|
1444
|
-
|
|
1573
|
+
const usage = snapshot.limitReached === true ? "limit reached" : `${percent(remaining)}%`;
|
|
1574
|
+
return `Codex ${shortest.label} ${usage}${reset ? ` ${reset}` : ""}${fastMode ? " Fast" : ""}`;
|
|
1445
1575
|
}
|
|
1446
1576
|
function applyFastModePayload(payload, enabled) {
|
|
1447
1577
|
if (!enabled || !payload || typeof payload !== "object" || Array.isArray(payload))
|
|
1448
1578
|
return payload;
|
|
1449
1579
|
return { ...payload, service_tier: "priority" };
|
|
1450
1580
|
}
|
|
1581
|
+
function usageRefreshNeeded(prev, next) {
|
|
1582
|
+
return next.usageStatus && (prev.usageStatus !== next.usageStatus || prev.allowOtherProviders !== next.allowOtherProviders);
|
|
1583
|
+
}
|
|
1451
1584
|
function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
1452
1585
|
const usageByAccount = new Map;
|
|
1586
|
+
const pendingRedeemByAccount = new Map;
|
|
1453
1587
|
let activeAccountId;
|
|
1454
1588
|
let credentialRevision = 0;
|
|
1455
1589
|
let latestContext;
|
|
@@ -1457,6 +1591,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1457
1591
|
let accountObserverActive = false;
|
|
1458
1592
|
let authWatcher;
|
|
1459
1593
|
let authWatchDebounce;
|
|
1594
|
+
let pollDelay;
|
|
1460
1595
|
const usageEnabled = (ctx) => {
|
|
1461
1596
|
const config = controller.getConfig();
|
|
1462
1597
|
return config.usageStatus && (ctx.model?.provider === "openai-codex" || config.allowOtherProviders);
|
|
@@ -1481,6 +1616,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1481
1616
|
credentialRevision += 1;
|
|
1482
1617
|
activeAccountId = undefined;
|
|
1483
1618
|
usageByAccount.clear();
|
|
1619
|
+
pendingRedeemByAccount.clear();
|
|
1484
1620
|
if (action === "set")
|
|
1485
1621
|
showSyncingStatus(ctx);
|
|
1486
1622
|
else
|
|
@@ -1492,6 +1628,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1492
1628
|
credentialRevision += 1;
|
|
1493
1629
|
activeAccountId = accountId;
|
|
1494
1630
|
usageByAccount.clear();
|
|
1631
|
+
pendingRedeemByAccount.clear();
|
|
1495
1632
|
showSyncingStatus(ctx);
|
|
1496
1633
|
return true;
|
|
1497
1634
|
};
|
|
@@ -1542,6 +1679,7 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1542
1679
|
if (parsed.length === 0)
|
|
1543
1680
|
throw new Error("Codex usage API returned no usage data");
|
|
1544
1681
|
state.snapshots = parsed;
|
|
1682
|
+
state.account = parseCodexAccountInfo(payload);
|
|
1545
1683
|
state.lastFetchAt = Date.now();
|
|
1546
1684
|
})();
|
|
1547
1685
|
let nextFetch;
|
|
@@ -1562,6 +1700,36 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1562
1700
|
latestContext = ctx;
|
|
1563
1701
|
refreshUsage(ctx, force).catch(() => refreshStatus(ctx));
|
|
1564
1702
|
};
|
|
1703
|
+
const stopPolling = () => {
|
|
1704
|
+
if (pollDelay) {
|
|
1705
|
+
clearTimeout(pollDelay);
|
|
1706
|
+
pollDelay = undefined;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
const scheduleNextPoll = () => {
|
|
1710
|
+
if (pollDelay)
|
|
1711
|
+
return;
|
|
1712
|
+
const intervalMinutes = Math.round(controller.getConfig().usagePollInterval);
|
|
1713
|
+
if (intervalMinutes <= 0)
|
|
1714
|
+
return;
|
|
1715
|
+
pollDelay = setTimeout(() => {
|
|
1716
|
+
pollDelay = undefined;
|
|
1717
|
+
const active = latestContext;
|
|
1718
|
+
const state = currentState();
|
|
1719
|
+
if (active && usageEnabled(active)) {
|
|
1720
|
+
const intervalMs = intervalMinutes * 60000;
|
|
1721
|
+
if (!state || state.snapshots.length === 0 || Date.now() - state.lastFetchAt >= intervalMs) {
|
|
1722
|
+
refreshUsage(active).catch(() => refreshStatus(active));
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
scheduleNextPoll();
|
|
1726
|
+
}, intervalMinutes * 60000);
|
|
1727
|
+
pollDelay.unref?.();
|
|
1728
|
+
};
|
|
1729
|
+
const startPolling = (ctx) => {
|
|
1730
|
+
latestContext = ctx;
|
|
1731
|
+
scheduleNextPoll();
|
|
1732
|
+
};
|
|
1565
1733
|
const codexOAuthAvailable = (ctx, config) => {
|
|
1566
1734
|
const model = ctx.model?.provider === "openai-codex" ? ctx.model : config.allowOtherProviders ? ctx.modelRegistry.getAll().find((candidate) => candidate.provider === "openai-codex" && ctx.modelRegistry.isUsingOAuth(candidate)) : undefined;
|
|
1567
1735
|
return !!model && ctx.modelRegistry.isUsingOAuth(model);
|
|
@@ -1646,20 +1814,120 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1646
1814
|
}
|
|
1647
1815
|
};
|
|
1648
1816
|
pi.registerCommand("codex-usage", {
|
|
1649
|
-
description: "Refresh and show Codex subscription usage
|
|
1817
|
+
description: "Refresh and show Codex subscription usage, plan, and rate limit redeems",
|
|
1650
1818
|
handler: async (_args, ctx) => {
|
|
1651
1819
|
try {
|
|
1652
1820
|
await refreshUsage(ctx, true);
|
|
1653
1821
|
} catch (error) {
|
|
1654
|
-
const
|
|
1822
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1655
1823
|
const snapshots = currentState()?.snapshots ?? [];
|
|
1656
1824
|
if (snapshots.length === 0) {
|
|
1657
|
-
ctx.ui.notify(`Failed to refresh Codex usage: ${
|
|
1825
|
+
ctx.ui.notify(`Failed to refresh Codex usage: ${message2}`, "error");
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
ctx.ui.notify(`Failed to refresh Codex usage; showing the latest snapshot: ${message2}`, "warning");
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
const resolved = await resolveActiveClient(ctx, controller.getConfig());
|
|
1832
|
+
const state2 = accountState(resolved.accountId);
|
|
1833
|
+
const payload = await resolved.client.get(REDEEM_CREDITS_PATH);
|
|
1834
|
+
state2.redeemCredits = parseCodexRedeemCredits(payload);
|
|
1835
|
+
} catch {}
|
|
1836
|
+
const state = currentState();
|
|
1837
|
+
const message = formatCodexUsage(state?.snapshots ?? [], Date.now(), {
|
|
1838
|
+
account: state?.account,
|
|
1839
|
+
redeemCredits: state?.redeemCredits
|
|
1840
|
+
});
|
|
1841
|
+
ctx.ui.notify(ctx.ui.theme ? ctx.ui.theme.fg("muted", message) : message, "info");
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
pi.registerCommand("codex-redeem", {
|
|
1845
|
+
description: "Preview and redeem an earned Codex rate limit reset credit (confirmation required)",
|
|
1846
|
+
handler: async (_args, ctx) => {
|
|
1847
|
+
const config = controller.getConfig();
|
|
1848
|
+
if (ctx.model?.provider !== "openai-codex" && !config.allowOtherProviders) {
|
|
1849
|
+
ctx.ui.notify("An active openai-codex model is required to redeem a rate limit reset. " + "Enable Other providers in /99settings to use the logged-in Codex subscription from another model.", "error");
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
let resolved;
|
|
1853
|
+
try {
|
|
1854
|
+
resolved = await resolveActiveClient(ctx, config);
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1857
|
+
ctx.ui.notify(`Failed to resolve the Codex subscription: ${message}`, "error");
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const state = accountState(resolved.accountId);
|
|
1861
|
+
try {
|
|
1862
|
+
const payload = await resolved.client.get(REDEEM_CREDITS_PATH);
|
|
1863
|
+
const redeemCredits = parseCodexRedeemCredits(payload);
|
|
1864
|
+
state.redeemCredits = redeemCredits;
|
|
1865
|
+
const now = Date.now();
|
|
1866
|
+
if (!redeemCredits || redeemCredits.availableCount <= 0) {
|
|
1867
|
+
pendingRedeemByAccount.delete(resolved.accountId);
|
|
1868
|
+
ctx.ui.notify("No Codex rate limit reset credits are available to redeem.", "info");
|
|
1658
1869
|
return;
|
|
1659
1870
|
}
|
|
1660
|
-
|
|
1871
|
+
const availableCredits = [...redeemCredits.credits].filter((item) => item.status === undefined || item.status === "available").sort((left, right) => (left.expiresAt ?? Number.POSITIVE_INFINITY) - (right.expiresAt ?? Number.POSITIVE_INFINITY));
|
|
1872
|
+
const credit = availableCredits[0] ?? redeemCredits.credits[0];
|
|
1873
|
+
const expiryOf = (item) => item?.expiresAt !== undefined && item.expiresAt > now ? ` (expires ${formatDateTime(item.expiresAt)})` : "";
|
|
1874
|
+
let selected = credit;
|
|
1875
|
+
if (ctx.hasUI) {
|
|
1876
|
+
if (availableCredits.length > 1) {
|
|
1877
|
+
const options2 = availableCredits.map((item) => `${item.title ?? "reset credit"}${expiryOf(item)}`);
|
|
1878
|
+
const choice2 = await ctx.ui.select("Select a reset credit to redeem", options2, {
|
|
1879
|
+
timeout: REDEEM_DIALOG_TIMEOUT_MS
|
|
1880
|
+
});
|
|
1881
|
+
if (choice2 === undefined) {
|
|
1882
|
+
pendingRedeemByAccount.delete(resolved.accountId);
|
|
1883
|
+
ctx.ui.notify("Redeem cancelled — no reset credit was consumed.", "info");
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
const index = options2.indexOf(choice2);
|
|
1887
|
+
selected = availableCredits[index] ?? credit;
|
|
1888
|
+
}
|
|
1889
|
+
const confirmOptions = ["No", "Yes"];
|
|
1890
|
+
const choice = await ctx.ui.select(`Redeem ${selected?.title ?? "Full reset"}${expiryOf(selected)}?`, confirmOptions, { timeout: REDEEM_DIALOG_TIMEOUT_MS });
|
|
1891
|
+
if (choice !== confirmOptions[1]) {
|
|
1892
|
+
pendingRedeemByAccount.delete(resolved.accountId);
|
|
1893
|
+
ctx.ui.notify("Redeem cancelled — no reset credit was consumed.", "info");
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const targetId = selected?.id;
|
|
1898
|
+
const existing = pendingRedeemByAccount.get(resolved.accountId);
|
|
1899
|
+
const pending = existing && existing.expiresAt > now && existing.creditId === targetId ? existing : {
|
|
1900
|
+
redeemRequestId: crypto.randomUUID(),
|
|
1901
|
+
creditId: targetId,
|
|
1902
|
+
expiresAt: now + REDEEM_CONFIRM_WINDOW_MS
|
|
1903
|
+
};
|
|
1904
|
+
pendingRedeemByAccount.set(resolved.accountId, pending);
|
|
1905
|
+
if (!ctx.hasUI && (!existing || existing.expiresAt <= now || existing.creditId !== targetId)) {
|
|
1906
|
+
ctx.ui.notify(`${redeemCredits.availableCount} rate limit reset redeem available: ${credit?.title ?? "Full reset"}${expiryOf(credit)}. ` + `Run /codex-redeem again within ${REDEEM_CONFIRM_WINDOW_MS / 1000}s to confirm.`, "warning");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
try {
|
|
1910
|
+
await resolved.client.post(REDEEM_PATH, {
|
|
1911
|
+
redeem_request_id: pending.redeemRequestId,
|
|
1912
|
+
credit_id: pending.creditId
|
|
1913
|
+
});
|
|
1914
|
+
pendingRedeemByAccount.delete(resolved.accountId);
|
|
1915
|
+
try {
|
|
1916
|
+
await refreshUsage(ctx, true);
|
|
1917
|
+
} catch {}
|
|
1918
|
+
const status = formatCodexStatus(currentState()?.snapshots ?? [], config.fastMode);
|
|
1919
|
+
ctx.ui.notify(`✓ Rate limit reset redeemed — usage reset.${status ? `
|
|
1920
|
+
${status}` : ""}`, "info");
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
pending.expiresAt = Date.now() + REDEEM_RETRY_WINDOW_MS;
|
|
1923
|
+
pendingRedeemByAccount.set(resolved.accountId, pending);
|
|
1924
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1925
|
+
ctx.ui.notify(`Failed to redeem a rate limit reset: ${message}. ` + "Run /codex-redeem again to retry with the same request ID.", "error");
|
|
1926
|
+
}
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1929
|
+
ctx.ui.notify(`Failed to redeem a rate limit reset: ${message}`, "error");
|
|
1661
1930
|
}
|
|
1662
|
-
ctx.ui.notify(formatCodexUsage(currentState()?.snapshots ?? []), "info");
|
|
1663
1931
|
}
|
|
1664
1932
|
});
|
|
1665
1933
|
pi.on("before_provider_request", (event, ctx) => {
|
|
@@ -1682,15 +1950,19 @@ function registerCodexUsageAndFast(pi, controller, options = {}) {
|
|
|
1682
1950
|
startAccountObserver(ctx);
|
|
1683
1951
|
checkCurrentAccount(ctx).catch(() => {});
|
|
1684
1952
|
refreshInBackground(ctx, true);
|
|
1953
|
+
startPolling(ctx);
|
|
1685
1954
|
});
|
|
1686
1955
|
pi.on("session_start", (_event, ctx) => {
|
|
1687
1956
|
startAccountObserver(ctx);
|
|
1688
1957
|
refreshInBackground(ctx, true);
|
|
1958
|
+
startPolling(ctx);
|
|
1689
1959
|
});
|
|
1690
1960
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
1961
|
+
stopPolling();
|
|
1691
1962
|
credentialRevision += 1;
|
|
1692
1963
|
activeAccountId = undefined;
|
|
1693
1964
|
usageByAccount.clear();
|
|
1965
|
+
pendingRedeemByAccount.clear();
|
|
1694
1966
|
latestContext = undefined;
|
|
1695
1967
|
accountObserverActive = false;
|
|
1696
1968
|
if (authWatchDebounce)
|
|
@@ -1715,6 +1987,7 @@ function codex_api_default(pi) {
|
|
|
1715
1987
|
const controller = {
|
|
1716
1988
|
getConfig: () => config,
|
|
1717
1989
|
updateConfig: (next, ctx) => {
|
|
1990
|
+
const prev = config;
|
|
1718
1991
|
config = next;
|
|
1719
1992
|
try {
|
|
1720
1993
|
saveCodexApiConfig(config);
|
|
@@ -1722,17 +1995,21 @@ function codex_api_default(pi) {
|
|
|
1722
1995
|
ctx.ui.notify(`Failed to save Codex API settings: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1723
1996
|
}
|
|
1724
1997
|
usageHandle?.refreshStatus(ctx);
|
|
1998
|
+
if (usageRefreshNeeded(prev, next)) {
|
|
1999
|
+
usageHandle?.refreshUsage(ctx, true).catch(() => {});
|
|
2000
|
+
}
|
|
1725
2001
|
}
|
|
1726
2002
|
};
|
|
1727
2003
|
usageHandle = registerCodexUsageAndFast(pi, controller);
|
|
1728
2004
|
const refreshUsageInBackground = (ctx) => {
|
|
1729
2005
|
usageHandle?.refreshUsage(ctx).catch(() => {});
|
|
1730
2006
|
};
|
|
1731
|
-
registerCodexImageTool(pi, () => config
|
|
2007
|
+
registerCodexImageTool(pi, () => config);
|
|
1732
2008
|
registerCodexSearchTool(pi, () => config, refreshUsageInBackground);
|
|
1733
2009
|
registerCodexApiSettings(pi, controller);
|
|
1734
2010
|
}
|
|
1735
2011
|
export {
|
|
2012
|
+
usageRefreshNeeded,
|
|
1736
2013
|
saveCodexApiConfig,
|
|
1737
2014
|
resolveSearchMode,
|
|
1738
2015
|
resolveCodexApiRoot,
|
|
@@ -1741,14 +2018,18 @@ export {
|
|
|
1741
2018
|
registerCodexImageTool,
|
|
1742
2019
|
registerCodexApiSettings,
|
|
1743
2020
|
parseCodexUsagePayload,
|
|
2021
|
+
parseCodexRedeemCredits,
|
|
1744
2022
|
parseCodexRateLimits,
|
|
2023
|
+
parseCodexAccountInfo,
|
|
1745
2024
|
normalizeCodexImageSize,
|
|
1746
2025
|
normalizeCodexApiConfig,
|
|
2026
|
+
maskCodexEmail,
|
|
1747
2027
|
loadCodexApiConfig,
|
|
1748
2028
|
getCodexApiConfigPath,
|
|
1749
2029
|
formatCodexUsage,
|
|
1750
2030
|
formatCodexStatus,
|
|
1751
2031
|
formatCodexSearchDisplay,
|
|
2032
|
+
formatCodexRedeemCredits,
|
|
1752
2033
|
extractCodexAccountId,
|
|
1753
2034
|
codex_api_default as default,
|
|
1754
2035
|
createCodexSearchDisplay,
|
|
@@ -1765,5 +2046,5 @@ export {
|
|
|
1765
2046
|
CODEX_API_SETTINGS_NAMESPACE
|
|
1766
2047
|
};
|
|
1767
2048
|
|
|
1768
|
-
//# debugId=
|
|
2049
|
+
//# debugId=F65EB4B48F9B1F0B64756E2164756E21
|
|
1769
2050
|
//# sourceMappingURL=index.ts.map
|