feat: include user_id in rate limit data persisted to session rollout files
feat: include user_id in rate limit data persisted to session rollout files
Problem
Session rollout files (~/.codex/sessions/) contain token_count events with rate_limits data (used percentage, window info, plan type), but there is no indication of which user the quota belongs to.
This matters because quotas are per-user, not per-account. Multiple users on the same ChatGPT Team account each have their own independent rate limits, but they share a single account_id. For example, three Team members sharing a machine all have account_id: "f81d2d74-..." but completely separate quota buckets tied to their individual chatgpt_user_id values.
Today, the only way to determine which user a session belongs to is to inspect ~/.codex/auth.json — but this is a shared mutable file that gets overwritten whenever any Codex session logs in (#6360). If you switch users and resume a session, or have multiple sessions open, auth.json reflects whichever login happened last, not the user of the session you're looking at.
The result: it's impossible to reliably attribute token consumption and quota usage to a specific user by parsing session files alone.
Ideal Solution: Server-side user_id in rate limit responses
The API server already knows exactly which user is making requests — the chatgpt_user_id is present in the JWT access token that the client sends with every request. The server currently returns rate limit data via:
- HTTP response headers:
x-codex-primary-used-percent,x-codex-secondary-used-percent,x-codex-primary-window-minutes, etc. - SSE events:
codex.rate_limitsevents containingrate_limits,plan_type,credits, andmetered_limit_name
Proposed: Add the user identity to both paths:
- Header:
x-codex-user-id(parsed alongside the existingx-codex-*family incodex-api/src/rate_limits.rs) - SSE event:
user_idfield in thecodex.rate_limitsJSON payload
Since the server is already extracting and validating the JWT to process the request, echoing back the chatgpt_user_id is trivial and authoritative. No client-side guessing required.
On the client side, RateLimitSnapshot gains one optional field:
pub struct RateLimitSnapshot {
pub limit_id: Option<String>,
pub limit_name: Option<String>,
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<PlanType>,
pub user_id: Option<String>, // NEW — e.g. "user-XQL1tOSLLvHXGVA5EtOkaEHa"
}
This flows automatically into rollout JSONL via the existing TokenCountEvent → EventMsg::TokenCount → RolloutItem::EventMsg serialization pipeline.
Client-side workaround (until server support lands)
The client already has the user identity available at runtime. The JWT stored in auth.json (and cached in-memory by AuthManager per session) contains the chatgpt_user_id in its https://api.openai.com/auth claim. Two options:
Option A — Inject user_id into RateLimitSnapshot client-side
When SessionState::set_rate_limits() merges a new snapshot in codex-rs/core/src/state/session.rs, inject the user_id from the in-memory auth state if the snapshot doesn't already carry one from the server. The AuthManager is session-scoped, so it reflects the user who actually started that session, even if auth.json on disk has since been overwritten by another session.
Option B — Emit SessionMeta on resume with user_id
The RolloutItem::SessionMeta variant is already handled at any position in the rollout stream (not just line 1). Emitting a fresh SessionMeta with user_id whenever a session is started or resumed would let tooling attribute subsequent turns to the correct user. This is a lighter touch than modifying every rate limit snapshot, though it requires tooling to correlate turns with the nearest preceding SessionMeta.
Example rollout output (after server-side change)
{
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": 12065,
"output_tokens": 3253,
"total_tokens": 15318
}
},
"rate_limits": {
"limit_id": "codex",
"primary": { "used_percent": 42.0, "window_minutes": 299, "resets_at": 1711900000 },
"secondary": { "used_percent": 15.0, "window_minutes": 10079, "resets_at": 1712400000 },
"plan_type": "team",
"user_id": "user-XQL1tOSLLvHXGVA5EtOkaEHa"
}
}
}
Backward compatibility
Using Option<String> with #[serde(default, skip_serializing_if = "Option::is_none")] ensures:
- Older rollout files without
user_idstill parse correctly - Older clients ignore the new header/field gracefully
Use Cases
- Multi-user quota tracking: Tooling like codex-quota can accurately attribute usage per user on shared machines
- Team usage auditing: Organizations can see which team member consumed how much of their individual quota
- Multi-user dashboards: Per-user burn rate displays become reliable without fragile timestamp correlation
- Debugging rate limits: Users can verify which identity was actually used when they hit unexpected limits
Related
- #6360 —
/statusshows the wrong user's info when multiple sessions are open (same root cause: identity not tracked per-session)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗