feat(coding-agent): added live saved-reset listing for all Codex accounts

- Added AuthStorage.listResetCredits to query each stored Codex account from the reset-credits endpoint and return live availability plus active or error state.
- Exposed the new status fetch through AgentSession and switched reset-usage selectors and commands to consume it via toResetUsageAccounts.
- Updated reset-usage UI and slash-command output to show per-account errors and updated empty-state messaging when resets could not be loaded.
This commit is contained in:
can1357
2026-06-12 05:32:33 +02:00
parent b980dd24f2
commit e6ff1cb47b
7 changed files with 130 additions and 51 deletions
+61
View File
@@ -689,6 +689,20 @@ export interface ResetCreditRedeemOutcome {
creditId?: string;
}
/** One stored account's live saved-reset status, from {@link AuthStorage.listResetCredits}. */
export interface ResetCreditAccountStatus {
credentialId?: number;
accountId?: string;
email?: string;
/** Resets redeemable for this account right now (live, not cached). */
availableCount: number;
credits: CodexResetCredit[];
/** Whether this is the given session's active account. */
active: boolean;
/** Set when the account's token refresh or list call failed. */
error?: string;
}
function isAbortSignalOption(
value: InvalidateCredentialMatchingOptions | AbortSignal | undefined,
): value is AbortSignal {
@@ -3620,6 +3634,53 @@ export class AuthStorage {
);
}
/**
* List saved rate-limit resets for every stored OAuth account of `provider`
* (Codex), fetched LIVE from the dedicated `rate-limit-reset-credits` route.
*
* This deliberately bypasses the usage-report cache: `/wham/usage` is
* IP-rate-limited and may serve stale (or pre-feature) snapshots when many
* accounts are polled, which would hide redeemable credits. One entry per
* account, with the session's active account flagged and unreachable
* accounts carrying an `error`.
*/
async listResetCredits(options?: {
provider?: string;
sessionId?: string;
baseUrlResolver?: (provider: string) => string | undefined;
signal?: AbortSignal;
}): Promise<ResetCreditAccountStatus[]> {
const provider = options?.provider ?? "openai-codex";
const accesses = await this.getOAuthAccesses(provider);
if (accesses.length === 0) return [];
const baseUrl = options?.baseUrlResolver?.(provider);
const activeId = this.getOAuthAccountIdentity(provider, options?.sessionId);
return Promise.all(
accesses.map(async (access): Promise<ResetCreditAccountStatus> => {
const active =
!!activeId &&
((!!activeId.accountId && activeId.accountId === access.accountId) ||
(!!activeId.email && activeId.email === access.email));
const base = {
credentialId: access.credentialId,
accountId: access.accountId,
email: access.email,
active,
};
if (!access.ok) return { ...base, availableCount: 0, credits: [], error: access.error };
const list = await listCodexResetCredits({
accessToken: access.accessToken,
accountId: access.accountId,
baseUrl,
fetch: this.#usageFetch,
signal: options?.signal,
});
if (!list) return { ...base, availableCount: 0, credits: [], error: "Failed to load saved resets" };
return { ...base, availableCount: list.availableCount, credits: list.credits };
}),
);
}
/**
* Redeem one saved rate-limit reset (OpenAI Codex "saved resets") for a
* specific stored account.
@@ -56,8 +56,14 @@ export class ResetUsageSelectorComponent extends Container {
if (!account) continue;
const isSelected = i === this.#selectedIndex;
const redeemable = account.availableCount > 0;
const countLabel = `${account.availableCount} saved reset${account.availableCount === 1 ? "" : "s"}`;
const countText = redeemable ? theme.fg("success", countLabel) : theme.fg("dim", countLabel);
const countLabel = account.error
? account.error
: `${account.availableCount} saved reset${account.availableCount === 1 ? "" : "s"}`;
const countText = account.error
? theme.fg("error", countLabel)
: redeemable
? theme.fg("success", countLabel)
: theme.fg("dim", countLabel);
const activeTag = account.active ? theme.fg("muted", " (active)") : "";
if (isSelected) {
const name = redeemable ? theme.fg("accent", account.label) : theme.fg("dim", account.label);
@@ -1,5 +1,4 @@
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { UsageReport } from "@oh-my-pi/pi-ai";
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
@@ -32,10 +31,9 @@ import type { ResetCreditRedeemOutcome } from "../../session/auth-storage";
import { type SessionInfo, SessionManager } from "../../session/session-manager";
import { FileSessionStorage } from "../../session/session-storage";
import {
buildResetUsageAccounts,
CODEX_PROVIDER_ID,
describeRedeemOutcome,
type ResetUsageAccount,
toResetUsageAccounts,
} from "../../slash-commands/helpers/reset-usage";
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
import {
@@ -1102,21 +1100,25 @@ export class SelectorController {
async showResetUsageSelector(): Promise<void> {
const session = this.ctx.session;
let reports: UsageReport[] | null = null;
this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
let statuses: Awaited<ReturnType<typeof session.listResetCredits>>;
try {
reports = await session.fetchUsageReports();
} catch {
this.ctx.showError("Could not load usage data to find saved resets.");
statuses = await session.listResetCredits();
} catch (error) {
this.ctx.showError(`Could not load saved resets: ${error instanceof Error ? error.message : String(error)}`);
return;
}
const active = session.modelRegistry.authStorage.getOAuthAccountIdentity(CODEX_PROVIDER_ID, session.sessionId);
const accounts = buildResetUsageAccounts(reports, active);
const accounts = toResetUsageAccounts(statuses);
if (accounts.length === 0) {
this.ctx.showStatus("No Codex accounts found. Use /login to add one.");
return;
}
if (!accounts.some(account => account.availableCount > 0)) {
this.ctx.showStatus("No saved rate-limit resets available to spend right now.");
this.ctx.showStatus(
accounts.some(account => account.error)
? "No saved resets available — some accounts couldn't be reached (try /login)."
: "No saved rate-limit resets available to spend right now.",
);
return;
}
this.showSelector(done => {
@@ -73,6 +73,7 @@ import type {
Model,
ProviderResponseMetadata,
ProviderSessionState,
ResetCreditAccountStatus,
ResetCreditRedeemOutcome,
ResetCreditTarget,
ServiceTier,
@@ -10153,6 +10154,19 @@ export class AgentSession {
});
}
/**
* List saved Codex rate-limit resets per stored account, fetched live from
* the dedicated credits endpoint (bypasses the usage cache). Powers the
* `/reset-usage` account selector.
*/
async listResetCredits(signal?: AbortSignal): Promise<ResetCreditAccountStatus[]> {
return this.#modelRegistry.authStorage.listResetCredits({
sessionId: this.sessionId,
baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
signal,
});
}
/**
* Estimate context tokens from messages, using the last assistant usage when available.
*/
@@ -14,6 +14,7 @@ export type {
CredentialOriginKind,
OAuthAccountIdentity,
OAuthCredential,
ResetCreditAccountStatus,
ResetCreditRedeemOutcome,
ResetCreditTarget,
SerializedAuthStorage,
@@ -2,7 +2,6 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
import type { UsageReport } from "@oh-my-pi/pi-ai";
import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
import { Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
import { $ } from "bun";
@@ -31,11 +30,11 @@ import { formatDuration } from "./helpers/format";
import { createMarketplaceManager } from "./helpers/marketplace-manager";
import { handleMcpAcp } from "./helpers/mcp";
import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
import { describeRedeemOutcome, toResetUsageAccounts } from "./helpers/reset-usage";
import { handleSshAcp } from "./helpers/ssh";
import { launchStatsDashboard, parseStatsDashboardArgs } from "./helpers/stats-dashboard";
import { handleTodoAcp } from "./helpers/todo";
import { buildUsageReportText } from "./helpers/usage-report";
import { buildResetUsageAccounts, describeRedeemOutcome } from "./helpers/reset-usage";
import { parseMarketplaceInstallArgs, parsePluginScopeArgs } from "./marketplace-install-parser";
import type {
BuiltinSlashCommand,
@@ -611,15 +610,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
allowArgs: true,
handle: async (command, runtime) => {
const { session } = runtime;
let reports: UsageReport[] | null = null;
let accounts: ReturnType<typeof toResetUsageAccounts>;
try {
reports = await session.fetchUsageReports();
} catch {
await runtime.output("Could not load usage data to find saved resets.");
accounts = toResetUsageAccounts(await session.listResetCredits());
} catch (error) {
await runtime.output(
`Could not load saved resets: ${error instanceof Error ? error.message : String(error)}`,
);
return commandConsumed();
}
const active = session.modelRegistry.authStorage.getOAuthAccountIdentity("openai-codex", session.sessionId);
const accounts = buildResetUsageAccounts(reports, active);
if (accounts.length === 0) {
await runtime.output("No Codex accounts found. Use /login to add one.");
return commandConsumed();
@@ -628,7 +627,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
if (!arg) {
const lines = ["Saved Codex rate-limit resets:"];
for (const account of accounts) {
lines.push(`- ${account.label}: ${account.availableCount} available${account.active ? " (active)" : ""}`);
const detail = account.error ? `unavailable (${account.error})` : `${account.availableCount} available`;
lines.push(`- ${account.label}: ${detail}${account.active ? " (active)" : ""}`);
}
lines.push("", "Spend one with `/reset-usage <account email>` or `/reset-usage active`.");
await runtime.output(lines.join("\n"));
@@ -1,10 +1,9 @@
/**
* Shared helpers for the `/reset-usage` command (TUI selector + ACP) and
* auto-redeem: turn usage reports into a per-account list of redeemable saved
* rate-limit resets, and map a redeem outcome code to a human message.
* Shared helpers for the `/reset-usage` command (TUI selector + ACP): turn the
* live per-account reset-credit status into selector rows, and map a redeem
* outcome code to a human message.
*/
import type { UsageReport } from "@oh-my-pi/pi-ai";
import type { OAuthAccountIdentity, ResetCreditRedeemOutcome, ResetCreditTarget } from "../../session/auth-storage";
import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget } from "../../session/auth-storage";
export const CODEX_PROVIDER_ID = "openai-codex";
@@ -18,36 +17,32 @@ export interface ResetUsageAccount {
target: ResetCreditTarget;
/** Whether this is the session's active Codex account. */
active: boolean;
/** Set when this account could not be reached (token/list failure). */
error?: string;
}
/**
* Build the per-account reset list from usage reports (Codex only). Sorted with
* the active account first, then most-credits, then label.
* Map live per-account reset status to selector rows. Sorted with the active
* account first, then most-credits, then label.
*/
export function buildResetUsageAccounts(
reports: UsageReport[] | null | undefined,
active?: OAuthAccountIdentity,
): ResetUsageAccount[] {
const accounts = (reports ?? [])
.filter(report => report.provider === CODEX_PROVIDER_ID)
.map(report => {
const accountId = typeof report.metadata?.accountId === "string" ? report.metadata.accountId : undefined;
const email = typeof report.metadata?.email === "string" ? report.metadata.email : undefined;
const isActive =
!!active &&
((!!active.accountId && active.accountId === accountId) || (!!active.email && active.email === email));
return {
label: email ?? accountId ?? "account",
availableCount: report.resetCredits?.availableCount ?? 0,
target: { accountId, email } satisfies ResetCreditTarget,
active: isActive,
};
export function toResetUsageAccounts(statuses: ResetCreditAccountStatus[]): ResetUsageAccount[] {
return statuses
.map(status => ({
label: status.email ?? status.accountId ?? "account",
availableCount: status.availableCount,
target: {
credentialId: status.credentialId,
accountId: status.accountId,
email: status.email,
} satisfies ResetCreditTarget,
active: status.active,
error: status.error,
}))
.sort((a, b) => {
if (a.active !== b.active) return a.active ? -1 : 1;
if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
return a.label.localeCompare(b.label);
});
return accounts.sort((a, b) => {
if (a.active !== b.active) return a.active ? -1 : 1;
if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
return a.label.localeCompare(b.label);
});
}
/** Human-facing summary of a redeem outcome for status lines and ACP output. */