Skip to content

feat(provider): list the models each ChatGPT account serves - #1579

Open
bwachman wants to merge 6 commits into
mainfrom
cl-11727-restore-the-live-codex-model-catalog-fetch-for-chatgpt
Open

bwachman wants to merge 6 commits into
mainfrom
cl-11727-restore-the-live-codex-model-catalog-fetch-for-chatgpt

Conversation

@bwachman

@bwachman bwachman commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A ChatGPT (Codex OAuth) profile lists the models its account serves: the TUI fetches GET /codex/models after startup and after /connect, keeps the listed entries in the backend's priority order, and makes the first one the profile's default. Requests send client_version 0.165.0; the backend returns no models to the previous 0.50.0.
  • Connecting a ChatGPT account shows that account's live models in the model step and preselects the first one, unless the operator has moved to another model or the type-a-model row. Confirming before the fetch returns waits for it (at most its 10 second timeout), so the saved model is picked from the account's list. That list is kept once the profile is saved.
  • Each list is kept per sign-in, by profile name and when it signed in, which a token refresh keeps and a reconnect changes. When a fetch fails or comes back empty, the profile keeps its own last live list or the built-in list, never another sign-in's list. An older fetch never replaces a newer list.

Verification

  • bun run check passes.
  • Against a live ChatGPT Plus account, a profile lists gpt-6.1-sol, gpt-6-astra, gpt-6.1-terra, gpt-6-sol, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra and gpt-5.6-luna, with gpt-6.1-sol as the default.
  • A headless turn on codex/plus with gpt-6.1-sol completes.

Fixes CL-11727

@linear-code

linear-code Bot commented Oct 29, 2026

Copy link
Copy Markdown

CL-11727

@bwachman

Copy link
Copy Markdown
Contributor Author

Self-Review (code-review skill)

The branch carries the following intentional exceptions:

  • src/tui/runner/settings.ts (post-connect picker): after a ChatGPT connect, the reopened picker focuses the picked model only when that model is in the built-in list. A model only the account's live list offers appears once the post-connect refresh lands, without moving focus, and stays out of the picker until restart if that refresh fails; the choice is persisted, so the next start recovers. Today the live list matches the built-in list. Authorized by Greybeard on grounds of cost against impact: the fix is seeding the stored profile's catalog from the connect-time fetch, which needs a new write path on the shared bounded catalog for a transient double-fault case.
  • src/tui/provider/discovery.ts (connect model step): the sign-in result type does not let the compiler prove its tokens are ChatGPT tokens; the call type-checks structurally, as the existing sign-in scope check does. Accepted by the operator.
  • src/provider/codex-model-catalog.ts (discoverStagedCodexModels): if staged sign-in tokens expire during a long idle and the operator re-enters the model step and submits at once, the catalog fetch and the submit-time scope check can both refresh the same staged token. Accepted by the operator.

The ChatGPT picker offered a built-in list that went stale as OpenAI
added models and retired others for ChatGPT sign-in, so a fresh login
could default to a model its account no longer serves. The backend
returns no models to client_version 0.50.0, so requests now send the
current Codex CLI version, including the sign-in scope check.
@bwachman
bwachman force-pushed the cl-11727-restore-the-live-codex-model-catalog-fetch-for-chatgpt branch from 9ca662d to a5f74c3 Compare October 29, 2026 18:49
@bwachman
bwachman changed the base branch from main to cl-11727-prepare-bounded-catalogs-for-the-live-chatgpt-catalog October 29, 2026 18:49
@bwachman

Copy link
Copy Markdown
Contributor Author

Stack merge order: #1580 → #1581 → #1579. This repo keeps merged branches, so GitHub does not retarget the next PR on its own:

  1. After fix(auth): drop the models retired for ChatGPT sign-in #1580 merges: gh pr edit 1581 --base main
  2. After refactor(provider): prepare live model catalogs for per-account lists #1581 merges: gh pr edit 1579 --base main

TheGreatAxios

This comment was marked as duplicate.

@TheGreatAxios TheGreatAxios left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review · Request changes

A ChatGPT profile fetches the models that account can use and shows those in the picker. The first one is the default. A failed fetch is supposed to keep the last good list, or the built-in list if there has never been one.

  • The fetched list is saved under the account id from the first token read (src/provider/codex-model-catalog.ts:119). If that read fails, the list is saved under "no account." The fetch then reads the token again, succeeds, and learns the real account id. The picker looks the list up under that new id, finds nothing, and shows the built-in models instead of the ones just fetched. The comment on that function says the second read fails the same way. It does not. A temporary test of this sequence failed and was removed. Save the list under the account id the successful fetch used. Startup only fetches once, so the picker stays on the built-in list until something fetches again.

  • Two logins that have no account id share one saved list. Switch accounts and have the new fetch fail or come back empty, and the picker still shows the previous account's models, while the request uses the new account's token. Logins that both have account ids do not share a list.

  • The models shown while connecting are not saved. That fetch builds a throwaway list (src/provider/codex-model-catalog.ts:140). If the fetch after connect then fails, the fallback is the built-in list, not the models just shown.

The fetched list was saved under the account from a token read made before
the request. When that read failed, the request's own read could still
succeed and name the account, so the picker looked under that account, found
nothing, and showed the built-in models. The token that makes the request now
picks where its list is kept, and a failed read gets one more try.

Two logins with no account id shared one saved list, so a reconnect whose
fetch failed or came back empty showed the previous login's models while
requests used the new token. Such a login is now told apart by its creation
time, which a token refresh keeps and a new sign-in changes.

The list shown while connecting was thrown away, so a failed fetch after
connecting fell back to the built-in models. It is now kept once the submit
saves the profile, before the picker reads it.
@bwachman

bwachman commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

All three findings fixed in 75655fe. Each had a test that failed first.

List saved under the wrong account. The token that makes the request now decides where its list is kept, so there is no separate earlier read to disagree with it. Your sequence (first read fails, next read succeeds and names the account) now ends with the fetched list in the picker. A failed read gets one more try, which matches the two reads the old code made. If both fail, the fetch counts as failed and logs why, and the picker stays on its current list until the next fetch.

Two logins with no account id sharing a list. A login with no account id is now told apart by its createdAt: updateTokens keeps it across a refresh, and a new sign-in writes a new one. After a reconnect, a failed or empty fetch falls back to the built-in list, not the previous login's models. Tests cover both of those cases, plus a refresh keeping the list.

Models shown while connecting. The list is now kept once the submit saves the profile, before setup returns and the picker reads it. A failed fetch after connecting leaves those models, and a later successful fetch replaces them. A failed submit keeps nothing, and neither does a step that only showed the built-in list.

Two things to look at:

  • ChatGPT profiles now fetch through discoverModels and keep their own lists per login, instead of one bounded catalog per key. That is what lets the key come from the request's token. bounded-model-catalog.ts and the Go and Zen catalogs are untouched. Overlapping fetches for a profile no longer merge into one, which only happens if a connect lands during the startup fetch, and each result is still kept under its own login.
  • My self-review listed an exception: a model only the live list offers stays out of the picker until restart if the post-connect refresh fails. Keeping the connect list should close it. The catalog tests cover the list itself, but no test drives the reopened picker and I have not checked it by hand.

Stack note: this still conflicts with #1580's d2c486f3, only in the comment above CODEX_DEFAULT_MODELS; the lists are identical. I'll resolve it when this retargets to main, so this diff stays limited to #1579's changes for now.

bun run check passes: 9620 tests, 0 failures.

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A ChatGPT sign-in fetches the models that account serves, keeps the list with that login, and uses the first entry as the default. A failed fetch is supposed to keep that login's own last list.

  • should-fix — The fetch stores the list under the account id on the token that served it, but the picker and the save after connect read the id on the profile. A refresh can learn an id the profile did not have yet. A later fetch that fails does not move the earlier entry, so the profile shows the built-in list while the real one is still stored under the old key. The save after connect has the same split: the scope check can refresh that token before the list is stored, and the models just shown are kept under an id that did not serve them. src/provider/codex-model-catalog.ts:102-173
  • should-fix — Overlapping fetches for one login are unordered. A startup prefetch that returns after connect has already stored the list just shown replaces it with the older response, so a model picked from that list can disappear from the picker. src/provider/codex-model-catalog.ts:119-136
  • should-fix — Confirming the model step before the fetch returns saves the built-in default and drops the live list when it arrives. That first live model never becomes the saved default, and the catalog refresh that follows does not rewrite the saved id. src/tui/provider/discovery.ts:227-238
  • informational — A blank account id counts as an identity. Two sign-ins of the same profile name with an empty id share one list, while a missing id is separated by sign-in time. src/provider/codex-model-catalog.ts:102-107

Base automatically changed from cl-11727-prepare-bounded-catalogs-for-the-live-chatgpt-catalog to main November 3, 2026 00:47

@corbits-triage corbits-triage 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.

Triage requests changes:

  • Merge conflicts: merge conflicts with base

…live-codex-model-catalog-fetch-for-chatgpt

The comment above CODEX_DEFAULT_MODELS conflicted with #1580; this keeps both:
the live-catalog fallback role and the Plus-catalog provenance. The list is
unchanged.
@corbits-triage

corbits-triage Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Triage: Fail

Asking a maintainer about:

  • changes sensitive paths: auth: src/auth/codex/constants.ts, auth: src/auth/oauth-scope-check.ts

Notes:

  • added code matches a code pattern: src/provider/codex-model-catalog.ts:143-145 promise-chain: Promise chain; use async/await with try/catch., src/tui/provider/discovery.ts:229-292 promise-chain: Promise chain; use async/await with try/catch., src/tui/provider/discovery.ts:229-289 promise-chain: Promise chain; use async/await with try/catch., src/tui/provider/discovery.ts:229-284 promise-chain: Promise chain; use async/await with try/catch., src/tui/provider/setup.ts:349-352 promise-chain: Promise chain; use async/await with try/catch., src/tui/provider/setup.ts:387-415 promise-chain: Promise chain; use async/await with try/catch., src/tui/runner/settings.ts:291-397 iife: Immediately invoked function; name the function., src/tui/runner/settings.ts:291-397 promise-chain: Promise chain; use async/await with try/catch.

@corbits-triage
corbits-triage Bot dismissed their stale review November 3, 2026 01:13

Nothing triage asked to change still fails, so this request no longer stands.

A fetch stored its list under the account id on the token that served it,
while the picker and the save after connecting read the id on the stored
profile. A refresh that taught the profile its id, followed by a failed
fetch, left the live list under the old key and showed the built-in one.
Lists are now keyed by profile name and creation time, which a token
refresh keeps and a reconnect changes, so every reader and writer agrees
and a blank account id no longer counts as an identity.

Overlapping fetches could answer in any order, so a startup fetch that
returned after the list shown at connect was kept replaced it with an
older view. Each request now takes a ticket when it starts, and an older
answer never replaces a newer list. The connect list is newer than every
fetch started before it was kept.

Confirming the connect model step before the fetch returned saved the
built-in default and dropped the live list, so the account's first model
never became the saved one. Confirming now waits for the fetch in flight,
bounded by its own timeout, then saves the pick made against the list.
@bwachman

bwachman commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

All four items from the review of 75655fe are fixed in 75fa109, each with a test that failed first. This also includes the merge of main (e1df050), which cleared the conflict with #1580.

Key mismatch (should-fix). The fetch stored the list under the account id on the token that served it, while the picker and the save after connecting read the id on the profile. Lists are now keyed by profile name and creation time. A refresh keeps the creation time even when it teaches the profile its account id, and a reconnect changes it, so every reader and writer uses the same key. A test covers a refresh that learns the id followed by a failed fetch, which now keeps the live list.

Blank account id (informational). The account id is no longer part of the key, so a blank id is not an identity. Two sign-ins of one profile name with blank ids no longer share a list, and a test pins that.

Overlapping fetches (should-fix). Each request now takes a ticket when it starts, and an older answer never replaces a newer list. The list kept at connect counts as newer than every fetch that started before it, so a startup fetch returning late cannot replace the list the operator picked from. Tests cover that case, an earlier request answering after a later one, and a later answer replacing an earlier one.

Confirming before the fetch returns (should-fix). Confirming the model step now waits for the fetch in flight, bounded by its own 10 second timeout, then saves the pick made against the account's list. A model the operator moved to is kept when the list has it. A failed fetch saves the built-in default, and cancelling while it waits saves nothing. I chose waiting over saving at once and rewriting later, because that would change a model the operator had already confirmed. Say so if you would rather not block on the network there.

bun run check passes: 9656 tests, 0 failures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants