(app-server and Desktop) thread/list treats omitted modelProviders as current-provider filter instead of all providers
What issue are you seeing?
The app-server thread/list API currently treats an omitted/null modelProviders field as an implicit filter for the currently configured model provider. If a third party app doesn't include the modelProviders field, then not all threads are shown. This issue is especially annoying when trying to continue a thread with a different model provider, as many apps, including Codex Desktop, don't see the thread with the new model provider. While this issue could be fixed both on the desktop and app server side, I think this is more of an app server issue than a desktop one, since an omitted filter shouldn't be filtering by default.
What steps can reproduce the bug?
App server
- Create local threads under at least two model providers, for example openai and lcpp.
- Configure Codex so the active provider is one of them, for example:
model_provider = "lcpp"
[model_providers.lcpp]
name = "Llama.cpp"
base_url = "http://localhost:8080/v1"
wire_api = "responses"
- Call app-server thread/list with modelProviders omitted or set to null.
- Observe that only threads for the active provider are returned.
- Call thread/list again with:
{
"modelProviders": []
}
- Observe that threads from all providers are returned.
Desktop
- Open Codex with default OpenAI config
- Send message in a folder
- Quit Codex Desktop
- Change config to use another model and provider
- Reopen Codex
- See that threads previously used are missing, only threads that used that provider previously are visible.
This is also inconsistent with the CLI since the CLI still lists threads that were started with other model providers.
Here is a screenshot with my OpenAI login, using the default OpenAI provider and model=gpt-5.5 in the config:
<img width="449" height="352" alt="Image" src="https://github.com/user-attachments/assets/e93d1f14-3a51-45a0-b2d2-851ac21a71fd" />
After quitting Codex, changing my config to the following, this is what I see:
model_provider = "lcpp"
model = "qwen3.6-27b-mtp"
<img width="433" height="500" alt="Image" src="https://github.com/user-attachments/assets/71e9a06e-df12-4ecd-bdc7-3f4aec71db40" />
What is the expected behavior?
Current behavior appears to be:
- modelProviders: ["openai"] returns only openai threads.
- modelProviders: [] returns all providers.
- modelProviders: null / omitted returns only threads matching the current configured provider.
What it should be:
- modelProviders: ["lcpp"] means only lcpp.
- modelProviders: [] means all providers.
- modelProviders: null / omitted means also all providers.
On the desktop side, I expect that my threads will still show up after switching providers.
Additional information
I pointed Codex to the desktop app.asar, and it confirmed that the desktop app currently sends listAllThreads({ modelProviders: null }) which is why threads go missing in the desktop app after switching model providers. If the app server is behaving as intended and you would rather fix it on the desktop app side, then switch model providers to be modelProviders: [] and it will include all threads from all model providers. Again this is inconsistent with the CLI since in the CLI you can resume across different model providers. This issue is the root cause of
Relevant code is here in main: https://github.com/openai/codex/blob/3da89d4831251b704ac40d1357d26ad518abaf1d/codex-rs/app-server/src/request_processors/thread_processor.rs#L3457
I also had drafted a fix but can't open a PR since I don't have permission: https://github.com/joshrad-dev/codex/tree/fix-thread-list-null-model-provider.
This code is the cause and fix for
the following issues:
https://github.com/openai/codex/issues/20184
https://github.com/openai/codex/issues/20004
https://github.com/openai/codex/issues/19088
After getting Codex to patch the ASAR to use [] instead of null for the modelProviders send, I'm now able to see all my threads across providers.
<img width="709" height="589" alt="Image" src="https://github.com/user-attachments/assets/098b82fd-9b07-44b0-8500-280ce29872bf" />
9 Comments
I checked current
origin/main, and the behavior described here matches the app-server boundary conversion.ThreadListFilters.model_providersis anOption<Vec<String>>. Inlist_threads_common, a non-empty list stays filtered,[]is converted to no provider filter, butnull/ omitted is converted toSome(vec![self.config.model_provider_id.clone()])before callingthread_store.list_threads(...). So the server is not merely passing the caller's omitted filter through; it is rewriting it into an active-provider filter.That narrows the fix to the contract decision: if the app-server protocol should treat omitted/null as all providers, the conversion in
list_threads_commonis the right boundary to change. If omitted/null is intended to mean current provider, then Desktop should send[]when it wants cross-provider history. A focused regression test should cover two stored threads with differentmodel_provider_idvalues and assert the chosen behavior for omitted/null,[], and a non-empty provider list.This also makes the current README misleading.
codex-rs/app-server/README.mddocumentsthread/list.modelProvidersas:“unset, null, or an empty array will include all providers.”
But current behavior appears to be:
So either the README should distinguish ordinary
thread/listfrom parentThreadId-filtered requests, or the app-server behavior should be changed to match the documented contract.I reproduced this on Codex Desktop for macOS while testing an OpenAI-compatible local proxy.
Observed behavior:
state_5.sqliterows and rollout JSONL files remain intact.The behavior appears consistent with the current conversion in
list_threads_common:modelProviders: []becomes no provider filter.modelProvidersbecomesSome(vec![self.config.model_provider_id.clone()]).Because Desktop omits this field when loading history, the app server implicitly filters the catalog to the currently configured provider.
A possible fix is to treat the optional provider filter as a filter only when it is non-empty:
That would establish these semantics:
modelProviders: all providersmodelProviders: []: all providersmodelProviders: only the requested providersSuggested regression coverage:
openaithread and oneother_providerthread.The protocol documentation should also state that omitted and empty provider filters include all providers.
Additional macOS reproduction after upgrading from the standalone Codex app to the unified ChatGPT desktop app.
Environment:
openai-httpsopenaiAfter the upgrade/restart, the task list shows only sessions whose stored provider matches the currently configured
openai-httpsprovider.This matches the reported
thread/listbehavior: the desktop history request sendsmodelProviders: null, which App Server converts into a filter for the currently configured provider. PassingmodelProviders: []would return threads from all providers.The upgrade appears to have triggered a cold history reload; it did not delete or migrate the sessions.
Expected behavior: the default local history view should remain visible across providers, or Desktop should explicitly pass
modelProviders: []when loading the general task history.Fresh macOS reproduction confirming this remains present in the current unified ChatGPT desktop app.
Environment:
customObserved:
model_provideromitted/commented, the effective provider isopenai; the project shows no previous tasks.model_provider = "custom"makes the previous tasks appear immediately.~/.codex/state_5.sqlitecontains 72 rows for that project cwd, all withmodel_provider = "custom"; the rollout data is intact.Directly querying the bundled app-server with the same cwd reproduces the boundary behavior:
The installed desktop renderer still sends
modelProviders: nullfrom its recent-thread listing path. Currentmainalso still converts omitted/null providers toSome(vec![self.config.model_provider_id.clone()]), while an empty array removes the provider filter.This confirms the bug is not data loss or project-assignment corruption. It is provider-based history filtering, and it persists in the latest app build available to me. Desktop sending
modelProviders: []for general local history would resolve this case.issue still exists
I can confirm this issue still affects the current unified ChatGPT desktop
app on macOS.
Environment:
openaicustomandmodeldock_httpObserved behavior for one affected project containing 7 valid, unarchived
threads:
modelProviders: null-> 0 threadsmodelProvidersomitted -> 0 threadsmodelProviders: ["custom"]-> all 7 threadsmodelProviders: []-> all 7 threadsthread/readby ID succeeds for every affected threadThe corresponding rows remain present in
state_5.sqlite, and all rolloutJSONL files remain intact. Across the local database, 32 top-level CLI/VS
Code threads were hidden by the same provider filter.
As a reversible diagnostic confirmation, changing only the SQLite listing
metadata from the historical provider ID to
openaimade the threadsimmediately appear through the default
modelProviders: nullrequest. Theoriginal JSONL provider metadata remained unchanged and
thread/readcontinued to report the original provider. I do not recommend this database
workaround as the product fix, but it confirms that the problem is limited to
provider-based listing.
This matches the documented contract that unset, null, and an empty array
should include all providers.
A focused fix appears to be changing the app-server boundary so that only a
non-empty provider list applies a filter, with regression coverage for:
If the maintainers agree with this approach, I would be happy to implement
the fix and regression tests. Please let me know if you would like to invite
a PR.
I prepared a focused fix for this issue, but the repository currently limits opening pull requests to collaborators.
Patch: https://github.com/hobbescalvin414-tech/codex/commit/d25e00c88091e1beebd45e2998ee639a420e3e3a
Branch: https://github.com/hobbescalvin414-tech/codex/tree/fix/thread-list-null-model-providers
The change makes omitted/null and empty
modelProvidersdisable provider filtering, while preserving explicit filtering for non-empty arrays. It also extends the app-server test to cover omitted/null and empty-array behavior across multiple providers.Verification:
just fmtandgit diff --checkpassed. The app-server test suite was not completed because dependency downloading was stopped before compilation.A collaborator is welcome to cherry-pick commit
d25e00c88091e1beebd45e2998ee639a420e3e3a.Confirming this from working on a small local provider-manager tool for custom [model_providers.*] setups.
Omitted modelProviders behaving as a current-provider filter matches what we see in practice and what was reported in the closed #15494, plus #31625, #31627, and #17316. Sessions remain intact in rollouts and SQLite; only discovery narrows to the active provider bucket.
Defaulting local discovery to all providers with an explicit filter opt-in (as proposed here) would remove the main footgun of switching between the built-in OpenAI provider and custom ones. Happy to help validate once the default changes.