VulCode
Blog

Zed: a slow ChatGPT model catalog no longer blocks sign-in

The ChatGPT subscription model catalog can take 30 seconds to answer, while Zed waited 5 and hid the account's models. Sign-in now waits 5 seconds and the catalog finishes loading in the background.

DualFroz - VulCode CEODualFroz - VulCode CEO1 min read
Zed: a slow ChatGPT model catalog no longer blocks sign-in - DualFroz - VulCode CEO
Title in the repository
openai_subscribed: Keep loading slow model catalogs in the background
Merged
1 Oct 2026
Written up here
6 Oct 2026

The note was written 5 days after the change was merged.

Zed
Technologies:Rustasync

What was broken

Zed, a code editor written in Rust, can use models from a ChatGPT subscription. After sign-in it fetches the catalog of models available to the account from /backend-api/codex/models. For some accounts that response takes around 30 seconds, and Zed aborted the request after 5. It then fell back to the built-in model list, and the account-specific models disappeared from the model selector (issue #64173).

Simply raising the timeout was not an option. Loading the credentials and signing in both wait for this request, so the user would be staring at a stuck screen for half a minute.

What we changed

We split the timeout in two. Sign-in still waits at most 5 seconds, but the request is no longer aborted: it keeps running in the background for up to 60 seconds and replaces the model list once it finally returns.

rust
futures::select! {
    result = refresh => log_model_catalog_error(result),
    () = wait_timeout => {
        cx.spawn(async move |_| log_model_catalog_error(refresh.await))
            .detach();
    }
}

If the catalog arrives within 5 seconds, nothing changes. If not, the detached task finishes later, and any error only goes to the log instead of blocking the interface.

How we tested it

We added test_slow_model_catalog_does_not_block_initial_load, with a fake HTTP client that holds the response back. The initial load finishes after 5 seconds with the built-in models, and a response arriving 30 seconds later replaces them with the account's models. cargo test and clippy passed for the crate. The change was not tested against the live endpoint, and the pull request says so plainly.

The result

The change was merged into main on 1 October 2026. Its release note entry describes it as a fix for ChatGPT subscription models disappearing when the catalog takes longer than 5 seconds to load.

A fast code editor in Rust from the creators of Atom and Tree-sitter, with live multiplayer editing.

Language
Rust
GitHub stars
91,587

The change in Zed's code

Fragments of pull request #64925 as git shows them, with notes in our words: what was there, what is there now and why. Beside them, the test that checks it.

zed-industries/zed#64925
Pull request
merged 1 Oct 2026crates/openai_subscribed/src/openai_subscribed.rs+123-10 in 1 file
@@ -46,2 +46,8 @@ const TOKEN_REFRESH_BUFFER_MS: u64 = Duration::from_mins(5).as_millis() as u64;
46-// Codex applies the same bound because model discovery is a startup-critical request.
47-const MODEL_CATALOG_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
46+/// How long credential loading and sign-in wait for model discovery before
47+/// continuing with the models known so far.
48+///
49+/// Model discovery is startup-critical, so this matches the 5s bound Codex uses.
50+const MODEL_CATALOG_WAIT_TIMEOUT: Duration = Duration::from_secs(5);
51+/// The authenticated catalog can take tens of seconds to respond, so the request
52+/// keeps running in the background well past [`MODEL_CATALOG_WAIT_TIMEOUT`].
53+const MODEL_CATALOG_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
@@ -146,6 +152,4 @@ impl State {
146152})?;
147-if let Some(refresh_models_task) = refresh_models_task
148- && let Err(error) = refresh_models_task.await
149-{
150- log::warn!("Failed to refresh ChatGPT models: {error:#}");
153+if let Some(refresh_models_task) = refresh_models_task {
154+ wait_for_model_catalog(refresh_models_task, cx).await;
151155}
@@ -1164,0 +1171,14 @@ async fn exchange_code(
above it, 4 added lines of the function's doc comment
1171+async fn wait_for_model_catalog(refresh: Task<Result<(), Arc<anyhow::Error>>>, cx: &mut AsyncApp) {
1172+ let mut refresh = refresh.fuse();
1173+ let mut wait_timeout = cx
1174+ .background_executor()
1175+ .timer(MODEL_CATALOG_WAIT_TIMEOUT)
1176+ .fuse();
1177+ futures::select! {
1178+ result = refresh => log_model_catalog_error(result),
1179+ () = wait_timeout => {
1180+ cx.spawn(async move |_| log_model_catalog_error(refresh.await))
1181+ .detach();
1182+ }
1183+ }
1184+}
then 7 added lines: log_model_catalog_error writes an error from the background to the log

Notes on the change

  1. Two limits instead of onelines 50-53Sign-in still waits at most 5 seconds for the catalog, as Codex does. The request itself gets 60 seconds, because for some accounts the catalog answers after about 30.
  2. This is where sign-in waitedlines 147-150, before the changeLoading the credentials waited for the catalog request to finish. After 5 seconds Zed aborted it, fell back to the built-in list, and the account's own models vanished from the selector.
  3. Whichever finishes first winslines 1177-1179The catalog or 5 seconds. If the catalog makes it in time, everything works as before the change.
  4. The rest happens in the backgroundlines 1180-1181After 5 seconds the request is not aborted. A detached task waits up to 60 seconds for the catalog and replaces the model list, and any error only goes to the log.

How we tested it

The pull request adds a test with a fake HTTP client that holds back the catalog response. cargo test and clippy for the crate passed too.

test_slow_model_catalog_does_not_block_initial_load
  1. 0 sSign-in startsThe HTTP client holds the model catalog response.
  2. 5 sSign-in finishesThe account is signed in and the selector has the built-in models.
  3. 30 sThe catalog arrivesThe response replaces the list with the account's model, with no catalog error.

The change was not tested against the live endpoint, and the pull request description says so.

What it taught us

A slow network request must not block signing in. What is not needed right away gets loaded in the background.

Questions about change #64925

Where the change is, how we tested it and who merged it.