feat(codex): surface subscription expiry metadata#3844
Conversation
Expose subscription_active_until and a subscription_expired flag for Codex auth. Remaining days are derived live from the expiry date in the dashboard rather than cached, so no static subscription_remaining_days field is stored or exposed (it would otherwise go stale). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add unit tests for the subscription metadata helpers and entry points: time/scalar parsing, expired-flag computation, accounts/check snapshot selection, the subscriptions fallback, and EnrichSubscriptionMetadata* (existing-expiry short-circuit, backend fallback, no-token no-op).
There was a problem hiding this comment.
Code Review
This pull request introduces Codex subscription metadata enrichment, allowing the application to fetch, parse, and cache ChatGPT subscription details (such as plan type and active duration) using JWT claims and backend APIs. It also updates the auth file storage to persist this enriched metadata. The review feedback highlights two key improvements: ensuring that writing updated metadata to the auth file is done atomically to prevent file corruption, and wrapping the metadata enrichment call in the API handler with a context timeout to prevent the handler from hanging indefinitely if the backend is unresponsive.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b8d6b5f3d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Bound the management Codex OAuth subscription lookup with a 20s context so a slow/unresponsive ChatGPT backend cannot block the token save and session completion, matching the SDK device-flow path. - Match object-keyed accounts/check records by their map key, not only by fields inside the value, so the requested account's plan/expiry is selected for keyed responses; fall back to the map key for the account id too. - Write the subscription metadata file atomically (temp file + rename) to avoid a truncated/empty credential file on crash or concurrent read. Tests: object-keyed account selection, atomic metadata write leaves no temp.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e53849af29
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Normalize subscription plan values before storing under plan_type, so backend web values (e.g. "chatgptplusplan", "chatgptfreeplan") become the canonical tokens the rest of the app expects (the free-plan check and model registration read Attributes["plan_type"]). Prevents misclassification of Codex auths that need the backend fallback. - Thread the List context into readAuthFile and derive the subscription enrichment timeout from it, so a cancelled startup/shutdown aborts promptly instead of blocking up to 20s per Codex file. Tests: plan normalization table + backend-plan normalization through enrichment; List returns promptly under a cancelled context.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e33eb86bd0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
FileTokenStore.readAuthFile populated metadata["plan_type"] via enrichment but returned the auth with only path/email attributes. The runtime selects the Codex model catalog from Attributes["plan_type"] (sdk/cliproxy/service.go), so Plus/Free/Team accounts loaded through the file store were treated as the default plan. Mirror plan_type (and subscription_active_until) from metadata into attributes, matching the watcher synthesizer. Test: List copies a codex plan_type/expiry from metadata into attributes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b388a44681
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
I think your AI was overthinking 🤦 |
…ibutes - Persist List-path subscription enrichment under the store mutex with a re-read+merge of only the subscription keys, so a token refresh/login Save racing the enrichment can no longer have its fresh access/refresh tokens rolled back by a stale-read rename. - In the management Codex OAuth flow, mirror the enriched plan_type and expiry into record.Attributes before saving, so a Free/Plus/Team account added via the management flow is registered with the correct model catalog instead of defaulting to Pro until a later file rescan. Test: persist merges subscription fields without clobbering on-disk tokens.
There was a problem hiding this comment.
💡 Codex Review
CLIProxyAPI/sdk/auth/codex_device.go
Lines 305 to 307 in 6c958bb
When the SDK Codex login path has to rely on EnrichSubscriptionMetadata to discover or normalize the plan, the metadata map now contains the correct plan_type, but the returned auth record still uses only the raw JWT planType. Callers that register the returned record immediately (the model catalog switch in sdk/cliproxy/service.go reads Attributes["plan_type"]) will default missing/raw values to Pro until a later file reload, so Free/Plus/Team accounts can get the wrong model set right after login.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The /auth-files response copied the cached subscription_expired boolean, which goes stale once the stored subscription_active_until passes without a reload or enrichment. Derive the flag from the expiry timestamp at response time (via the new codex.IsSubscriptionExpired helper), falling back to the cached value only when no expiry is available. Test: a past expiry with a stale cached false is reported as expired; a future expiry as not expired.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbb28ba5c2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Read the disabled flag before the List-path subscription enrichment and skip the lookup for disabled Codex credentials, so a disabled account with a missing/expired expiry no longer delays List by up to 20s per file. - In the SDK device-login return, prefer the enriched/normalized plan_type (and expiry) from metadata over the pre-enrichment JWT claim when populating Attributes, so SDK callers select the correct Codex catalog before a reload. Test: List returns promptly for a disabled codex auth without contacting the backend.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea67990638
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…expired A Codex subscription_active_until stored as a JSON number was stringified via valueAsString before the expired check, producing scientific notation (e.g. 1.893553445e+09) the parser rejects, so /auth-files reported expired for still-active subscriptions. Pass the raw metadata value to codex.IsSubscriptionExpired, which applies the same scalar normalization (Unix seconds/millis) as the enrichment path. Test: a numeric future expiry is reported as not expired.
Every code path that builds a Codex Auth was independently copying plan_type and subscription_active_until from metadata into attributes, which is why the same "defaults to Pro" bug kept resurfacing at each new site. Extract a single ApplyCodexSubscriptionAttributes helper (alongside ApplyCustomHeadersFromMetadata) and route all four sites through it: file-store load, management OAuth, SDK device-login, and the watcher synthesizer (which keeps its JWT-claim fallback). No behavior change; consolidates the duplicated blocks so future call sites stay consistent by construction. Test: helper copies/overrides plan and expiry and is a safe no-op without metadata.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d80abb88a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Summary
Surfaces ChatGPT subscription expiry for Codex accounts so the management API can report whether a Codex subscription is still active.
A new
internal/auth/codex/subscription.goenriches an account's metadata withsubscription_active_untiland a derivedsubscription_expiredflag. It reads from theid_tokenJWT claims first, and only falls back to the ChatGPT backend (accounts/check, thensubscriptions) when the cached expiry is missing or already expired.Remaining time is intentionally not stored as a static duration — it is derived live from the expiry timestamp by the consumer — so it never goes stale between refreshes.
Behavior
chatgpt_account_id,chatgpt_plan_type,chatgpt_subscription_active_until) populate metadata without any network call when present.FetchSubscriptionStatusqueriesaccounts/checkand falls back tosubscriptionsfor plan/expiry, selecting the preferred account id when multiple are returned.subscription_expiredis recomputed from the expiry on each enrichment; only metadata that actually changes is written.Changes
internal/auth/codex/subscription.go— new: subscription enrichment, ChatGPT backend lookups, and metadata helpers.internal/api/handlers/management/auth_files.go— expose the Codex subscription metadata on auth-file responses.internal/watcher/synthesizer/file.go,sdk/auth/codex.go,sdk/auth/codex_device.go,sdk/auth/filestore.go— wire enrichment into the auth load/save path.accounts/checksnapshot selection, thesubscriptionsfallback (HTTP mocked), and theEnrichSubscriptionMetadata*entry points.Testing
go build ./...go test ./internal/auth/codex/...— all pass (subscription parsing/enrichment functions covered, most at 90-100%)go test ./internal/api/handlers/management/... ./internal/watcher/...— passgofmt/go vetclean