openai-account
Tier 0. Reuses anthropic-account secret redaction. TP-3363 read-side slice.
npm install @titan-design/openai-accountThe problem it solves
Read token-free Codex login state and quota observations without copying account credentials. The pure root handles shapes and decisions; ./node handles the bounded status probe and read-only rollout scan.
When to reach for it
Use this package for account admission and usage displays. Use agent to execute a turn, session-read to decode transcripts and anthropic-account for Claude accounts. Nothing here depends on the new Codex runtime adapter exports.
Example
import { quotaAdmission } from "@titan-design/openai-account";
import { readLoginState, readUsage } from "@titan-design/openai-account/node";
const profile = { label: "reporter", codeHome: "/tmp/example-codex-home" };
const login = await readLoginState(profile);
const usage = await readUsage(profile);
const admission = quotaAdmission(usage, Date.now() / 1000);
// A supervisor must require both login.status === "present" and admission.allowed.Tests use synthetic rollout fixtures and fake status executables, not installed Codex or real account sessions. The status grammar and rollout shape target 0.162.1, but live CLI compatibility remains unverified under this seat's rules.
Login and account selection
Every node read requires a safe label and explicit absolute codeHome. There is no fallback to a shared account. accountLabel(codeHome) uses the final path segment (.codex becomes default); labels accept only short non-secret identifiers and cannot inject statusline delimiters.
readLoginState(profile, options?) runs only codex login status, with that CODEX_HOME and API credential environment variables removed. It recognizes exact ChatGPT success and not-logged-in output on either stdout or stderr. API-key login and unknown formats refuse. It returns no raw stdout, stderr, path, token, error or cause. Default timeout is five seconds; allowed override is 1–60000 ms. An AbortSignal cancels the native probe. runStatus is a dependency seam for tests and supervised transports, which must preserve those bounds.
Neither entry reads auth.json directly. This is not a login-health network check: present means the CLI reports cached ChatGPT login, not that a model turn works.
Rollout readings and age
usageFromRolloutEvents(events, account) recognizes event_msg / payload.type=token_count with payload.rate_limits. It requires both numeric windows: used_percent in [0,100], window_minutes 300 and 10080, plus future epoch-second resets_at. Window order may be reversed. An explicit limit_id must be codex; optional credits and other payload content are not returned.
The result is { status: "unknown", reason } for native rollout evidence. Codex can emit cached rate_limits on local token updates without fetching quota. Even a single event, a changed value, or an identical re-emission cannot prove a server observation time. Valid evidence therefore returns reason: "freshness-unverified", with no numbers, written_at or known age. Displays must show unknown; admission refuses. No value-equality heuristic, file mtime, read time or invented payload timestamp establishes freshness.
Validation still selects the latest evidence by event time across files. A newer bad event invalidates older evidence; conflicting equal timestamps, missing timestamps and unexpected shapes return inconsistent. No quota events returns missing. Unknown fields never enter returned state.
The public UsageResult also supports a caller-supplied { status: "known", reading } for sources that independently establish quota observation time. That reading contains account, written_at and rate_limits.five_hour / seven_day windows with used_percentage and resets_at. All timestamps use epoch seconds. The caller must establish the provenance; no native rollout reader in this package currently produces known readings. Additional fields in raw rollouts are not treated as a trusted timestamp seam.
readUsage(profile, options?) scans only rollout-*.jsonl in sessions and archived_sessions beneath the selected home. No auth files, default-home scan or network requests. Symlinks, hard links, nonregular rollouts, parse errors, partial lines and scan failures return unknown/unreadable. The scan defaults to 64 MiB total, 4096 rollouts, 16384 directory entries and depth eight; bounds refuse the entire scan rather than silently dropping newer files. maxBytes and maxFiles can override their bounds. Large or concurrently appended transcripts may therefore temporarily give unknown. Returned state never contains transcript text or underlying errors.
Quota admission
quotaAdmission(result, now) refuses automatic runs when weekly usage is >86 (less than 14 points remain), five-hour usage is >=100, the reading is missing or inconsistent, freshness is unverified, or its age is >900 seconds. Exactly 86 weekly used and 900 seconds old are admitted if the other checks pass. Future observations and already expired resets are inconsistent. No missing quota becomes zero. This check does not include login state; supervisors check both.
For caller-established observations, age is now - written_at. Native event emission time cannot supply it, and reading a file never refreshes it. The local cache is an observation, not a live quota endpoint or account-wide spend reservation. Additional model-specific pools are not admitted by this API.
Scope and redaction
No CLI, statusline, credential provisioning, auth copying, login writes, token refresh, permission adapter, runtime launch or account-wide reservation in this slice. redactSecrets reuses Titan's redactor and adds OpenAI key prefixes; Error copies contain only scrubbed message/name/stack and no cause. Structural allow-listing keeps transcript and status output out of account state altogether.
Where it came from
New package for TP-3363, following anthropic-account's pure root / node subpath layout. CLI and statusline output are a separate follow-up slice.