feat(usage): show expiry dates for banked Codex rate-limit resets (#3339)

The /usage display (TUI, ACP text, and omp usage CLI) now shows when
banked Codex rate-limit resets expire, so users can plan when to redeem
them before the 30-day window lapses.

Changes:
- Added UsageResetCreditDetail interface (grantedAt, expiresAt, status)
  and optional credits field to UsageResetCredits in packages/ai
- Updated both ArkType schema copies (usage.ts + wire-schemas.ts)
- The OpenAI Codex usage provider now calls listCodexResetCredits to
  fetch individual credit details when availableCount > 0, filtering
  out redeemed credits. Errors are logged and swallowed (graceful
  degradation: count shows without expiry dates)
- TUI (command-controller.ts): shows per-credit expiry as relative time
  + absolute date under each account line
- ACP text (usage-report.ts): same per-credit expiry rendering
- CLI (usage-cli.ts): shows the soonest expiry date in the account header
- 2 new tests: credit detail fetching with expiry dates, and no extra
  API call when availableCount is 0
This commit is contained in:
oldschoola
2026-06-23 11:58:29 -07:00
parent 93d730c01d
commit 639b6195fa
9 changed files with 181 additions and 1 deletions
+17 -1
View File
@@ -326,7 +326,23 @@ function formatAccountHeader(
const planType = report.metadata?.planType;
if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
const savedResets = report.resetCredits?.availableCount ?? 0;
if (savedResets > 0) header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
if (savedResets > 0) {
header += chalk.cyan(` · ✦ ${savedResets} saved reset${savedResets === 1 ? "" : "s"}`);
const credits = report.resetCredits?.credits;
if (credits) {
const upcoming = credits
.filter(c => c.expiresAt)
.map(c => ({ date: c.expiresAt!, ms: Date.parse(c.expiresAt!) }))
.filter(c => !Number.isNaN(c.ms))
.sort((a, b) => a.ms - b.ms)
.find(c => c.ms > nowMs);
if (upcoming) {
header += chalk.dim(
` · soonest expires in ${formatDuration(upcoming.ms - nowMs)} (${upcoming.date.slice(0, 10)})`,
);
}
}
}
if (report.fetchedAt && nowMs - report.fetchedAt > 90_000) {
header += chalk.dim(` · fetched ${formatDuration(nowMs - report.fetchedAt)} ago`);
}