Allow injecting the models manager into ThreadManager (#32911)

## Why

Embedding callers need to control whether model catalogs are persisted to disk.

## What changed

- Accept a shared models manager when constructing `ThreadManager` instead of always creating a cached manager internally.
- Add provider APIs for creating uncached model managers. OpenAI-compatible providers fetch on each `OnlineIfUncached` refresh without reading or writing `models_cache.json`.
- Keep existing app server, MCP server, sample, and test callers on the standard cached manager.

## Testing

- Verify an uncached manager fetches on every refresh.
- Verify an injected uncached manager controls thread refresh behavior and does not create `models_cache.json`.

GitOrigin-RevId: 8bb646054ecec4ccb865b2bf56249384916ea9db
This commit is contained in:
richardopenai
2026-07-14 01:06:11 +00:00
committed by copyberry
parent c39520f3d1
commit 80c6cd3014
17 changed files with 224 additions and 21 deletions
+3 -1
View File
@@ -2393,9 +2393,11 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
async fn resume_thread_subagent_restores_stored_metadata() {
let (home, config) = test_config().await;
let thread_store = Arc::new(InMemoryThreadStore::default());
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
+1
View File
@@ -44,6 +44,7 @@ pub async fn build_prompt_input(
let thread_manager = ThreadManager::new(
&config,
Arc::clone(&auth_manager),
crate::thread_manager::build_models_manager(&config, Arc::clone(&auth_manager)),
SessionSource::Exec,
Arc::new(
EnvironmentManager::from_codex_home(
+2 -1
View File
@@ -305,6 +305,7 @@ impl ThreadManager {
pub fn new(
config: &Config,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
extensions: Arc<ExtensionRegistry<Config>>,
@@ -337,7 +338,7 @@ impl ThreadManager {
state: Arc::new(ThreadManagerState {
threads: Arc::new(RwLock::new(HashMap::new())),
thread_created_tx,
models_manager: build_models_manager(config, auth_manager.clone()),
models_manager,
environment_manager,
skills_service,
plugins_manager,
+73 -4
View File
@@ -596,9 +596,12 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
extensions.thread_lifecycle_contributor(recorder.clone());
extensions.mcp_server_contributor(recorder);
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
Arc::new(extensions.build()),
@@ -818,6 +821,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -943,7 +947,8 @@ async fn explicit_installation_id_skips_codex_home_file() {
let thread_store = thread_store_from_config(&config, state_db.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -985,6 +990,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1045,6 +1051,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1112,6 +1119,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1203,7 +1211,8 @@ async fn subtree_listing_uses_injected_graph_store_without_state_db() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1249,6 +1258,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1353,7 +1363,8 @@ async fn new_uses_active_provider_for_model_refresh() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1375,6 +1386,61 @@ async fn new_uses_active_provider_for_model_refresh() {
assert_eq!(models_mock.requests().len(), 1);
}
#[tokio::test]
async fn injected_models_manager_controls_refresh_policy() {
let server = MockServer::start().await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
config.model_catalog = None;
config.model_provider.base_url = Some(server.uri());
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = create_model_provider(
config.model_provider.clone(),
Some(Arc::clone(&auth_manager)),
);
let models_manager = provider.models_manager_without_cache(config.model_catalog.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
models_manager,
SessionSource::Custom("test-embedder".to_string()),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
/*analytics_events_client*/ None,
thread_store_from_config(&config, /*state_db*/ None),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);
let http_client_factory = crate::test_support::default_http_client_factory();
let _ = manager
.list_models(
RefreshStrategy::OnlineIfUncached,
http_client_factory.clone(),
)
.await;
let _ = manager
.list_models(RefreshStrategy::OnlineIfUncached, http_client_factory)
.await;
assert_eq!(
server.received_requests().await.unwrap_or_default().len(),
2
);
assert!(!config.codex_home.join("models_cache.json").exists());
}
#[test]
fn interrupted_fork_snapshot_appends_interrupt_boundary() {
let committed_history =
@@ -1593,6 +1659,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1703,6 +1770,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -1804,6 +1872,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
@@ -4293,9 +4293,11 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.enable(Feature::Sqlite)
.expect("test config should allow sqlite");
let state_db = init_state_db(&config).await;
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),