Skip to content

feat(codex): surface subscription expiry metadata#3844

Open
drwpls wants to merge 10 commits into
router-for-me:devfrom
drwpls:pr/codex-subscription-expiry
Open

feat(codex): surface subscription expiry metadata#3844
drwpls wants to merge 10 commits into
router-for-me:devfrom
drwpls:pr/codex-subscription-expiry

Conversation

@drwpls

@drwpls drwpls commented Jun 14, 2026

Copy link
Copy Markdown

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.go enriches an account's metadata with subscription_active_until and a derived subscription_expired flag. It reads from the id_token JWT claims first, and only falls back to the ChatGPT backend (accounts/check, then subscriptions) 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

  • JWT claims (chatgpt_account_id, chatgpt_plan_type, chatgpt_subscription_active_until) populate metadata without any network call when present.
  • If the current expiry is still valid, the backend lookup is skipped.
  • Otherwise FetchSubscriptionStatus queries accounts/check and falls back to subscriptions for plan/expiry, selecting the preferred account id when multiple are returned.
  • subscription_expired is 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.
  • Unit tests for time/scalar parsing, the expired-flag computation, accounts/check snapshot selection, the subscriptions fallback (HTTP mocked), and the EnrichSubscriptionMetadata* 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/... — pass
  • gofmt/go vet clean

drwpls and others added 2 commits June 14, 2026 11:02
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).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread sdk/auth/filestore.go
Comment thread internal/api/handlers/management/auth_files.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/auth/codex/subscription.go
Comment thread internal/api/handlers/management/auth_files.go Outdated
- 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/auth/codex/subscription.go Outdated
Comment thread sdk/auth/filestore.go Outdated
- 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread sdk/auth/filestore.go
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/api/handlers/management/auth_files.go
Comment thread sdk/auth/filestore.go Outdated
@drwpls

drwpls commented Jun 14, 2026

Copy link
Copy Markdown
Author

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Attributes: map[string]string{
"plan_type": planType,
},

P2 Badge Mirror the enriched Codex plan into SDK auth attributes

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".

Comment thread internal/api/handlers/management/auth_files.go Outdated
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread sdk/auth/codex_device.go
Comment thread sdk/auth/filestore.go Outdated
- 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/api/handlers/management/auth_files.go Outdated
drwpls added 2 commits June 14, 2026 15:35
…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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread sdk/auth/filestore.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant