Skip to content

Commit f50d255

Browse files
committed
fix(codex-limits): deduplicate accounts by refreshToken to avoid duplicate display
1 parent 417fa71 commit f50d255

2 files changed

Lines changed: 435 additions & 3 deletions

File tree

index.ts

Lines changed: 293 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4156,9 +4156,299 @@ while (attempted.size < Math.max(1, accountCount)) {
41564156
})}`,
41574157
);
41584158

4159-
return lines.join("\n");
4160-
},
4161-
}),
4159+
return lines.join("\n");
4160+
},
4161+
}),
4162+
"codex-limits": tool({
4163+
description: "Show live 5-hour and weekly Codex usage limits for all accounts.",
4164+
args: {},
4165+
async execute() {
4166+
const ui = resolveUiRuntime();
4167+
const storage = await loadAccounts();
4168+
if (!storage || storage.accounts.length === 0) {
4169+
if (ui.v2Enabled) {
4170+
return [
4171+
...formatUiHeader(ui, "Codex limits"),
4172+
"",
4173+
formatUiItem(ui, "No accounts configured.", "warning"),
4174+
formatUiItem(ui, "Run: opencode auth login", "accent"),
4175+
].join("\n");
4176+
}
4177+
return "No Codex accounts configured. Run: opencode auth login";
4178+
}
4179+
4180+
type UsageWindow = {
4181+
used_percent?: number;
4182+
limit_window_seconds?: number;
4183+
reset_at?: number;
4184+
reset_after_seconds?: number;
4185+
} | null;
4186+
4187+
type LimitWindow = {
4188+
usedPercent?: number;
4189+
windowMinutes?: number;
4190+
resetAtMs?: number;
4191+
};
4192+
4193+
type UsageRateLimit = {
4194+
primary_window?: UsageWindow;
4195+
secondary_window?: UsageWindow;
4196+
} | null;
4197+
4198+
type UsageCredits = {
4199+
has_credits?: boolean;
4200+
unlimited?: boolean;
4201+
balance?: string | null;
4202+
} | null;
4203+
4204+
type UsagePayload = {
4205+
plan_type?: string;
4206+
rate_limit?: UsageRateLimit;
4207+
code_review_rate_limit?: UsageRateLimit;
4208+
additional_rate_limits?: Array<{
4209+
limit_name?: string;
4210+
metered_feature?: string;
4211+
rate_limit?: UsageRateLimit;
4212+
}> | null;
4213+
credits?: UsageCredits;
4214+
};
4215+
4216+
const formatWindowLabel = (windowMinutes: number | undefined): string => {
4217+
if (!windowMinutes || !Number.isFinite(windowMinutes) || windowMinutes <= 0) {
4218+
return "quota";
4219+
}
4220+
if (windowMinutes % 1440 === 0) return `${windowMinutes / 1440}d`;
4221+
if (windowMinutes % 60 === 0) return `${windowMinutes / 60}h`;
4222+
return `${windowMinutes}m`;
4223+
};
4224+
4225+
const formatReset = (resetAtMs: number | undefined): string | undefined => {
4226+
if (!resetAtMs || !Number.isFinite(resetAtMs) || resetAtMs <= 0) return undefined;
4227+
const date = new Date(resetAtMs);
4228+
if (!Number.isFinite(date.getTime())) return undefined;
4229+
4230+
const now = new Date();
4231+
const sameDay =
4232+
now.getFullYear() === date.getFullYear() &&
4233+
now.getMonth() === date.getMonth() &&
4234+
now.getDate() === date.getDate();
4235+
const time = date.toLocaleTimeString(undefined, {
4236+
hour: "2-digit",
4237+
minute: "2-digit",
4238+
hour12: false,
4239+
});
4240+
if (sameDay) return time;
4241+
const day = date.toLocaleDateString(undefined, { month: "short", day: "2-digit" });
4242+
return `${time} on ${day}`;
4243+
};
4244+
4245+
const mapWindow = (window: UsageWindow): LimitWindow => {
4246+
if (!window) return {};
4247+
return {
4248+
usedPercent:
4249+
typeof window.used_percent === "number" && Number.isFinite(window.used_percent)
4250+
? window.used_percent
4251+
: undefined,
4252+
windowMinutes:
4253+
typeof window.limit_window_seconds === "number" && Number.isFinite(window.limit_window_seconds)
4254+
? Math.max(1, Math.ceil(window.limit_window_seconds / 60))
4255+
: undefined,
4256+
resetAtMs:
4257+
typeof window.reset_at === "number" && window.reset_at > 0
4258+
? window.reset_at * 1000
4259+
: typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0
4260+
? Date.now() + window.reset_after_seconds * 1000
4261+
: undefined,
4262+
};
4263+
};
4264+
4265+
const formatLimitTitle = (windowMinutes: number | undefined, fallback = "quota"): string => {
4266+
if (windowMinutes === 300) return "5h limit";
4267+
if (windowMinutes === 10080) return "Weekly limit";
4268+
if (fallback !== "quota") return fallback;
4269+
return `${formatWindowLabel(windowMinutes)} limit`;
4270+
};
4271+
4272+
const formatLimitSummary = (window: LimitWindow): string => {
4273+
const used = window.usedPercent;
4274+
const left =
4275+
typeof used === "number" && Number.isFinite(used)
4276+
? Math.max(0, Math.min(100, Math.round(100 - used)))
4277+
: undefined;
4278+
const reset = formatReset(window.resetAtMs);
4279+
if (left !== undefined && reset) return `${left}% left (resets ${reset})`;
4280+
if (left !== undefined) return `${left}% left`;
4281+
if (reset) return `resets ${reset}`;
4282+
return "unavailable";
4283+
};
4284+
4285+
const formatCredits = (credits: UsageCredits): string | undefined => {
4286+
if (!credits) return undefined;
4287+
if (credits.unlimited) return "unlimited";
4288+
if (typeof credits.balance === "string" && credits.balance.trim()) {
4289+
return credits.balance.trim();
4290+
}
4291+
if (credits.has_credits) return "available";
4292+
return undefined;
4293+
};
4294+
4295+
const formatExtraName = (name: string | undefined): string => {
4296+
if (!name) return "Additional limit";
4297+
if (name === "code_review_rate_limit") return "Code review";
4298+
return name.replace(/[_-]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
4299+
};
4300+
4301+
const fetchUsage = async (params: {
4302+
accountId: string;
4303+
accessToken: string;
4304+
organizationId: string | undefined;
4305+
}): Promise<UsagePayload> => {
4306+
const headers = createCodexHeaders(undefined, params.accountId, params.accessToken, {
4307+
organizationId: params.organizationId,
4308+
});
4309+
headers.set("accept", "application/json");
4310+
4311+
const response = await fetch(`${CODEX_BASE_URL}/wham/usage`, {
4312+
method: "GET",
4313+
headers,
4314+
});
4315+
if (!response.ok) {
4316+
const bodyText = await response.text().catch(() => "");
4317+
throw new Error(bodyText || `HTTP ${response.status}`);
4318+
}
4319+
return (await response.json()) as UsagePayload;
4320+
};
4321+
4322+
// Deduplicate accounts by refreshToken (same credential = same limits)
4323+
const seenTokens = new Set<string>();
4324+
const uniqueIndices: number[] = [];
4325+
for (let i = 0; i < storage.accounts.length; i++) {
4326+
const acct = storage.accounts[i];
4327+
if (!acct) continue;
4328+
if (seenTokens.has(acct.refreshToken)) continue;
4329+
seenTokens.add(acct.refreshToken);
4330+
uniqueIndices.push(i);
4331+
}
4332+
4333+
const lines: string[] = ui.v2Enabled
4334+
? [...formatUiHeader(ui, "Codex limits"), ""]
4335+
: [`Codex limits (${uniqueIndices.length} account${uniqueIndices.length === 1 ? "" : "s"}):`, ""];
4336+
const activeIndex = resolveActiveIndex(storage, "codex");
4337+
let storageChanged = false;
4338+
4339+
for (const i of uniqueIndices) {
4340+
const account = storage.accounts[i];
4341+
if (!account) continue;
4342+
const label = formatCommandAccountLabel(account, i);
4343+
const activeSuffix = i === activeIndex ? (ui.v2Enabled ? ` ${formatUiBadge(ui, "active", "accent")}` : " [active]") : "";
4344+
4345+
try {
4346+
let accessToken = account.accessToken;
4347+
if (
4348+
typeof accessToken !== "string" ||
4349+
!accessToken ||
4350+
typeof account.expiresAt !== "number" ||
4351+
account.expiresAt <= Date.now() + 30_000
4352+
) {
4353+
const refreshResult = await queuedRefresh(account.refreshToken);
4354+
if (refreshResult.type !== "success") {
4355+
throw new Error(refreshResult.message ?? refreshResult.reason);
4356+
}
4357+
account.refreshToken = refreshResult.refresh;
4358+
account.accessToken = refreshResult.access;
4359+
account.expiresAt = refreshResult.expires;
4360+
accessToken = refreshResult.access;
4361+
storageChanged = true;
4362+
}
4363+
4364+
const accountId = account.accountId ?? extractAccountId(accessToken);
4365+
if (!accountId) {
4366+
throw new Error("Missing account id");
4367+
}
4368+
4369+
const payload = await fetchUsage({
4370+
accountId,
4371+
accessToken,
4372+
organizationId: account.organizationId,
4373+
});
4374+
4375+
const primary = mapWindow(payload.rate_limit?.primary_window ?? null);
4376+
const secondary = mapWindow(payload.rate_limit?.secondary_window ?? null);
4377+
const codeReviewRateLimit =
4378+
payload.code_review_rate_limit ??
4379+
payload.additional_rate_limits?.find((entry) => entry.limit_name === "code_review_rate_limit")?.rate_limit ??
4380+
null;
4381+
const codeReview = mapWindow(codeReviewRateLimit?.primary_window ?? null);
4382+
const credits = formatCredits(payload.credits ?? null);
4383+
const additionalLimits = (payload.additional_rate_limits ?? []).filter(
4384+
(entry) => entry.limit_name !== "code_review_rate_limit",
4385+
);
4386+
4387+
if (ui.v2Enabled) {
4388+
lines.push(formatUiItem(ui, `${label}${activeSuffix}`));
4389+
lines.push(` ${formatUiKeyValue(ui, formatLimitTitle(primary.windowMinutes), formatLimitSummary(primary), "muted")}`);
4390+
lines.push(` ${formatUiKeyValue(ui, formatLimitTitle(secondary.windowMinutes), formatLimitSummary(secondary), "muted")}`);
4391+
if (codeReview.windowMinutes || typeof codeReview.usedPercent === "number" || codeReview.resetAtMs) {
4392+
lines.push(` ${formatUiKeyValue(ui, "Code review", formatLimitSummary(codeReview), "muted")}`);
4393+
}
4394+
for (const limit of additionalLimits) {
4395+
const extraWindow = mapWindow(limit.rate_limit?.primary_window ?? null);
4396+
lines.push(` ${formatUiKeyValue(ui, formatExtraName(limit.limit_name ?? limit.metered_feature), formatLimitSummary(extraWindow), "muted")}`);
4397+
}
4398+
if (payload.plan_type) {
4399+
lines.push(` ${formatUiKeyValue(ui, "Plan", payload.plan_type, "muted")}`);
4400+
}
4401+
if (credits) {
4402+
lines.push(` ${formatUiKeyValue(ui, "Credits", credits, "muted")}`);
4403+
}
4404+
} else {
4405+
lines.push(`${label}${activeSuffix}:`);
4406+
lines.push(` ${formatLimitTitle(primary.windowMinutes)}: ${formatLimitSummary(primary)}`);
4407+
lines.push(` ${formatLimitTitle(secondary.windowMinutes)}: ${formatLimitSummary(secondary)}`);
4408+
if (codeReview.windowMinutes || typeof codeReview.usedPercent === "number" || codeReview.resetAtMs) {
4409+
lines.push(` Code review: ${formatLimitSummary(codeReview)}`);
4410+
}
4411+
for (const limit of additionalLimits) {
4412+
const extraWindow = mapWindow(limit.rate_limit?.primary_window ?? null);
4413+
lines.push(` ${formatExtraName(limit.limit_name ?? limit.metered_feature)}: ${formatLimitSummary(extraWindow)}`);
4414+
}
4415+
if (payload.plan_type) {
4416+
lines.push(` Plan: ${payload.plan_type}`);
4417+
}
4418+
if (credits) {
4419+
lines.push(` Credits: ${credits}`);
4420+
}
4421+
}
4422+
} catch (error) {
4423+
const message = error instanceof Error ? error.message : String(error);
4424+
if (ui.v2Enabled) {
4425+
lines.push(formatUiItem(ui, `${label}${activeSuffix}`));
4426+
lines.push(` ${formatUiKeyValue(ui, "Error", message.slice(0, 160), "danger")}`);
4427+
} else {
4428+
lines.push(`${label}${activeSuffix}:`);
4429+
lines.push(` Error: ${message.slice(0, 160)}`);
4430+
}
4431+
}
4432+
4433+
lines.push("");
4434+
}
4435+
4436+
if (storageChanged) {
4437+
await saveAccounts(storage);
4438+
if (cachedAccountManager) {
4439+
const reloadedManager = await AccountManager.loadFromDisk();
4440+
cachedAccountManager = reloadedManager;
4441+
accountManagerPromise = Promise.resolve(reloadedManager);
4442+
}
4443+
}
4444+
4445+
while (lines.length > 0 && lines[lines.length - 1] === "") {
4446+
lines.pop();
4447+
}
4448+
4449+
return lines.join("\n");
4450+
},
4451+
}),
41624452
"codex-metrics": tool({
41634453
description: "Show runtime request metrics for this plugin process.",
41644454
args: {},

0 commit comments

Comments
 (0)