-
Notifications
You must be signed in to change notification settings - Fork 53
feat(cli): expose machine-readable quota limits #688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import { formatAccountLabel } from "../../accounts.js"; | ||
| import { findQuotaCacheEntryForAccount } from "../../quota-readiness.js"; | ||
| import type { QuotaCacheData, QuotaCacheEntry } from "../../quota-cache.js"; | ||
| import type { AccountStorageV3 } from "../../storage.js"; | ||
|
|
||
| const LIMITS_SCHEMA_VERSION = 1; | ||
| const LIMITS_REFRESH_MAX_AGE_MS = 5 * 60_000; | ||
| const LIMITS_USAGE = "Usage: codex-multi-auth limits --json [--refresh]"; | ||
|
|
||
| export interface LimitsCommandDeps { | ||
| setStoragePath: (path: string | null) => void; | ||
| loadAccounts: () => Promise<AccountStorageV3 | null>; | ||
| loadQuotaCache: () => Promise<QuotaCacheData>; | ||
| refreshQuotaCache: ( | ||
| storage: AccountStorageV3, | ||
| cache: QuotaCacheData, | ||
| maxAgeMs: number, | ||
| ) => Promise<QuotaCacheData>; | ||
| resolveActiveIndex: (storage: AccountStorageV3, family?: "codex") => number; | ||
| getNow?: () => number; | ||
| logInfo?: (message: string) => void; | ||
| logError?: (message: string) => void; | ||
| } | ||
|
|
||
| interface ParsedLimitsOptions { | ||
| json: boolean; | ||
| refresh: boolean; | ||
| help: boolean; | ||
| } | ||
|
|
||
| /** Parse the intentionally small, JSON-only limits command surface. */ | ||
| function parseLimitsOptions(args: string[]): | ||
| | { ok: true; options: ParsedLimitsOptions } | ||
| | { ok: false; message: string } { | ||
| const options: ParsedLimitsOptions = { json: false, refresh: false, help: false }; | ||
| for (const arg of args) { | ||
| if (arg === "--json" || arg === "-j") { | ||
| options.json = true; | ||
| continue; | ||
| } | ||
| if (arg === "--refresh") { | ||
| options.refresh = true; | ||
| continue; | ||
| } | ||
| if (arg === "--help" || arg === "-h") { | ||
| options.help = true; | ||
| continue; | ||
| } | ||
| return { ok: false, message: `Unknown limits option: ${arg}` }; | ||
| } | ||
| if (!options.json && !options.help) { | ||
| return { ok: false, message: LIMITS_USAGE }; | ||
| } | ||
| return { ok: true, options }; | ||
| } | ||
|
|
||
| /** Convert a cached quota window to the explicit-null public JSON contract. */ | ||
| function publicWindow(window: QuotaCacheEntry["primary"]) { | ||
| return { | ||
| usedPercent: window.usedPercent ?? null, | ||
| windowMinutes: window.windowMinutes ?? null, | ||
| resetAtMs: window.resetAtMs ?? null, | ||
| }; | ||
| } | ||
|
|
||
| /** Remove internal probe metadata and stabilize optional quota fields. */ | ||
| function publicQuotaEntry(entry: QuotaCacheEntry) { | ||
| return { | ||
| updatedAt: entry.updatedAt, | ||
| status: entry.status, | ||
| planType: entry.planType ?? null, | ||
| primary: publicWindow(entry.primary), | ||
| secondary: publicWindow(entry.secondary), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Emit configured accounts joined to safe cached quota records. | ||
| * | ||
| * Cached mode performs no provider requests. Refresh mode delegates to the | ||
| * existing sequential, age-gated refresh path before serializing the snapshot. | ||
| */ | ||
| export async function runLimitsCommand( | ||
| args: string[], | ||
| deps: LimitsCommandDeps, | ||
| ): Promise<number> { | ||
| const parsed = parseLimitsOptions(args); | ||
| const logInfo = deps.logInfo ?? console.log; | ||
| const logError = deps.logError ?? console.error; | ||
| if (!parsed.ok) { | ||
| logError(parsed.message); | ||
| return 1; | ||
| } | ||
| if (parsed.options.help) { | ||
| logInfo(LIMITS_USAGE); | ||
| return 0; | ||
| } | ||
|
|
||
| deps.setStoragePath(null); | ||
| const storage = await deps.loadAccounts(); | ||
| if (!storage || storage.accounts.length === 0) { | ||
| const generatedAt = deps.getNow?.() ?? Date.now(); | ||
| logInfo( | ||
| JSON.stringify( | ||
| { | ||
| schemaVersion: LIMITS_SCHEMA_VERSION, | ||
| generatedAt, | ||
| mode: parsed.options.refresh ? "refresh" : "cached", | ||
| accounts: [], | ||
| }, | ||
| null, | ||
| 2, | ||
| ), | ||
| ); | ||
| return 0; | ||
| } | ||
|
|
||
| let cache = await deps.loadQuotaCache(); | ||
| if (parsed.options.refresh) { | ||
| cache = await deps.refreshQuotaCache( | ||
| storage, | ||
| cache, | ||
| LIMITS_REFRESH_MAX_AGE_MS, | ||
| ); | ||
|
Comment on lines
+120
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Knowledge Base Used: Quota and refresh orchestration Prompt To Fix With AIThis is a comment left during a code review.
Path: lib/codex-manager/commands/limits.ts
Line: 111-115
Comment:
**concurrent refreshes can overwrite**
`--refresh` adds another concurrent caller to a cache update that performs an unlocked cross-process read, modify, and replacement. overlapping cli or dashboard refreshes can probe the same accounts twice and allow an older snapshot to overwrite newer cache data. add cross-process coordination or freshness-aware merging, with a vitest that runs two refreshes concurrently.
**Knowledge Base Used:** [Quota and refresh orchestration](https://app.greptile.com/zeian/-/custom-context/knowledge-base/ndycode/codex-multi-auth/-/docs/quota-and-refresh-orchestration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
|
|
||
| const generatedAt = deps.getNow?.() ?? Date.now(); | ||
| const activeIndex = deps.resolveActiveIndex(storage, "codex"); | ||
| const accounts = storage.accounts.map((account, index) => { | ||
| const quota = findQuotaCacheEntryForAccount( | ||
| cache, | ||
| account, | ||
| storage.accounts, | ||
| ); | ||
| return { | ||
| index, | ||
| label: formatAccountLabel(account, index), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Knowledge Base Used: Prompt To Fix With AIThis is a comment left during a code review.
Path: lib/codex-manager/commands/limits.ts
Line: 128
Comment:
**raw emails enter json**
`formatAccountLabel` includes the account's unredacted email, so `limits --json` can send pii to logs and integrations through the generic `label` field. use a redacted or explicitly public label instead. the vitest coverage checks that access tokens are excluded, but does not check email redaction.
**Knowledge Base Used:**
- [CLI commands and operations](https://app.greptile.com/zeian/-/custom-context/knowledge-base/ndycode/codex-multi-auth/-/docs/cli-commands-and-operations.md)
- [CLI and interactive experience](https://app.greptile.com/zeian/-/custom-context/knowledge-base/ndycode/codex-multi-auth/-/docs/cli-and-interactive-experience.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| enabled: account.enabled !== false, | ||
| current: index === activeIndex, | ||
| quota: quota ? publicQuotaEntry(quota) : null, | ||
| }; | ||
| }); | ||
|
|
||
| logInfo( | ||
| JSON.stringify( | ||
| { | ||
| schemaVersion: LIMITS_SCHEMA_VERSION, | ||
| generatedAt, | ||
| mode: parsed.options.refresh ? "refresh" : "cached", | ||
| accounts, | ||
| }, | ||
| null, | ||
| 2, | ||
| ), | ||
| ); | ||
| return 0; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.