diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6b85049d2..65721a698 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1862,8 +1862,7 @@ impl CodexMessageProcessor { async fn list_models(&self, request_id: RequestId, params: ModelListParams) { let ModelListParams { limit, cursor } = params; - let auth_mode = self.auth_manager.auth().map(|auth| auth.mode); - let models = supported_models(auth_mode); + let models = supported_models(self.conversation_manager.clone()).await; let total = models.len(); if total == 0 { diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 78f6fd585..3ac71e85b 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -1,12 +1,15 @@ -use codex_app_server_protocol::AuthMode; +use std::sync::Arc; + use codex_app_server_protocol::Model; use codex_app_server_protocol::ReasoningEffortOption; -use codex_core::openai_models::model_presets::builtin_model_presets; +use codex_core::ConversationManager; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; -pub fn supported_models(auth_mode: Option) -> Vec { - builtin_model_presets(auth_mode) +pub async fn supported_models(conversation_manager: Arc) -> Vec { + conversation_manager + .list_models() + .await .into_iter() .map(model_from_preset) .collect() diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index d874435e8..a5c9add53 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -1201,4 +1201,8 @@ impl AuthManager { self.reload(); Ok(removed) } + + pub fn get_auth_mode(&self) -> Option { + self.auth().map(|a| a.mode) + } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 13be377e9..885a4cdf7 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -23,7 +23,6 @@ use crate::user_notification::UserNotifier; use crate::util::error_or_panic; use async_channel::Receiver; use async_channel::Sender; -use codex_app_server_protocol::AuthMode; use codex_protocol::ConversationId; use codex_protocol::items::TurnItem; use codex_protocol::protocol::FileChange; @@ -643,14 +642,6 @@ impl Session { Ok(sess) } - pub(crate) fn get_auth_mode(&self) -> AuthMode { - self.services - .auth_manager - .auth() - .map(|a| a.mode) - .unwrap_or(AuthMode::ApiKey) - } - pub(crate) fn get_tx_event(&self) -> Sender { self.tx_event.clone() } @@ -1486,9 +1477,6 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::Review { review_request } => { handlers::review(&sess, &config, sub.id.clone(), review_request).await; } - Op::ListModels => { - handlers::list_models(&sess, sub.id.clone(), Some(sess.get_auth_mode())).await; - } _ => {} // Ignore unknown ops; enum is non_exhaustive to allow extensions. } } @@ -1505,15 +1493,12 @@ mod handlers { use crate::config::Config; use crate::mcp::auth::compute_auth_statuses; use crate::mcp::collect_mcp_snapshot_from_manager; - use crate::openai_models::model_presets::builtin_model_presets; use crate::review_prompts::resolve_review_request; use crate::tasks::CompactTask; use crate::tasks::RegularTask; use crate::tasks::UndoTask; use crate::tasks::UserShellCommandTask; - use codex_app_server_protocol::AuthMode; use codex_protocol::custom_prompts::CustomPrompt; - use codex_protocol::openai_models::AvailableModelsEvent; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::Event; @@ -1828,15 +1813,6 @@ mod handlers { } } } - - pub async fn list_models(sess: &Arc, sub_id: String, auth_mode: Option) { - let models = builtin_model_presets(auth_mode); - let event = Event { - id: sub_id, - msg: EventMsg::ListModelsResponse(AvailableModelsEvent { models }), - }; - sess.send_event_raw(event).await; - } } /// Spawn a review thread using the given prompt. diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/conversation_manager.rs index 0f4577bf1..3ac09ec78 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/conversation_manager.rs @@ -7,6 +7,7 @@ use crate::codex_conversation::CodexConversation; use crate::config::Config; use crate::error::CodexErr; use crate::error::Result as CodexResult; +use crate::openai_models::models_manager::ModelsManager; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::protocol::SessionConfiguredEvent; @@ -14,6 +15,7 @@ use crate::rollout::RolloutRecorder; use codex_protocol::ConversationId; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ModelPreset; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; @@ -35,6 +37,7 @@ pub struct NewConversation { pub struct ConversationManager { conversations: Arc>>>, auth_manager: Arc, + models_manager: Arc, session_source: SessionSource, } @@ -42,8 +45,9 @@ impl ConversationManager { pub fn new(auth_manager: Arc, session_source: SessionSource) -> Self { Self { conversations: Arc::new(RwLock::new(HashMap::new())), - auth_manager, + auth_manager: auth_manager.clone(), session_source, + models_manager: Arc::new(ModelsManager::new(auth_manager.get_auth_mode())), } } @@ -193,6 +197,10 @@ impl ConversationManager { self.finalize_spawn(codex, conversation_id).await } + + pub async fn list_models(&self) -> Vec { + self.models_manager.available_models.read().await.clone() + } } /// Return a prefix of `items` obtained by cutting strictly before the nth user message diff --git a/codex-rs/core/src/openai_models/mod.rs b/codex-rs/core/src/openai_models/mod.rs index 7df68c4ab..13ee2e060 100644 --- a/codex-rs/core/src/openai_models/mod.rs +++ b/codex-rs/core/src/openai_models/mod.rs @@ -1 +1,2 @@ pub mod model_presets; +pub mod models_manager; diff --git a/codex-rs/core/src/openai_models/models_manager.rs b/codex-rs/core/src/openai_models/models_manager.rs new file mode 100644 index 000000000..1d57f1e69 --- /dev/null +++ b/codex-rs/core/src/openai_models/models_manager.rs @@ -0,0 +1,26 @@ +use codex_app_server_protocol::AuthMode; +use codex_protocol::openai_models::ModelPreset; +use tokio::sync::RwLock; + +use crate::openai_models::model_presets::builtin_model_presets; + +pub struct ModelsManager { + pub available_models: RwLock>, + pub etag: String, + pub auth_mode: Option, +} + +impl ModelsManager { + pub fn new(auth_mode: Option) -> Self { + Self { + available_models: RwLock::new(builtin_model_presets(auth_mode)), + etag: String::new(), + auth_mode, + } + } + + pub async fn refresh_available_models(&self) { + let models = builtin_model_presets(self.auth_mode); + *self.available_models.write().await = models; + } +} diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index 4b7d7d306..58072f933 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -51,7 +51,6 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::Warning(_) | EventMsg::TaskStarted(_) | EventMsg::TaskComplete(_) - | EventMsg::ListModelsResponse(_) | EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) | EventMsg::AgentReasoningRawContentDelta(_) diff --git a/codex-rs/core/tests/suite/list_models.rs b/codex-rs/core/tests/suite/list_models.rs index ecfe9138e..930382016 100644 --- a/codex-rs/core/tests/suite/list_models.rs +++ b/codex-rs/core/tests/suite/list_models.rs @@ -1,51 +1,30 @@ use anyhow::Result; use codex_core::CodexAuth; -use codex_core::protocol::EventMsg; -use codex_core::protocol::Op; +use codex_core::ConversationManager; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffortPreset; -use core_test_support::responses::start_mock_server; -use core_test_support::test_codex::test_codex; -use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_models_returns_api_key_models() -> Result<()> { - let server = start_mock_server().await; - let mut builder = test_codex().with_auth(CodexAuth::from_api_key("sk-test")); - let test = builder.build(&server).await?; - - test.codex.submit(Op::ListModels).await?; - - let event = wait_for_event_match(&test.codex, |event| match event { - EventMsg::ListModelsResponse(models) => Some(models.clone()), - _ => None, - }) - .await; + let manager = ConversationManager::with_auth(CodexAuth::from_api_key("sk-test")); + let models = manager.list_models().await; let expected_models = expected_models_for_api_key(); - assert_eq!(expected_models, event.models); + assert_eq!(expected_models, models); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_models_returns_chatgpt_models() -> Result<()> { - let server = start_mock_server().await; - let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); - let test = builder.build(&server).await?; - - test.codex.submit(Op::ListModels).await?; - - let event = wait_for_event_match(&test.codex, |event| match event { - EventMsg::ListModelsResponse(models) => Some(models.clone()), - _ => None, - }) - .await; + let manager = + ConversationManager::with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + let models = manager.list_models().await; let expected_models = expected_models_for_chatgpt(); - assert_eq!(expected_models, event.models); + assert_eq!(expected_models, models); Ok(()) } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 17a167585..64a5358f3 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -583,7 +583,6 @@ impl EventProcessor for EventProcessorWithHumanOutput { | EventMsg::ReasoningContentDelta(_) | EventMsg::ReasoningRawContentDelta(_) | EventMsg::UndoCompleted(_) - | EventMsg::ListModelsResponse(_) | EventMsg::UndoStarted(_) => {} } CodexStatus::Running diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 2b3fd0770..55808f17c 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -307,7 +307,6 @@ async fn run_codex_tool_session_inner( | EventMsg::UndoCompleted(_) | EventMsg::ExitedReviewMode(_) | EventMsg::ContextCompacted(_) - | EventMsg::ListModelsResponse(_) | EventMsg::DeprecationNotice(_) => { // For now, we do not do anything extra for these // events. Note that diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index f9a05f9ff..b99c3bbde 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -35,11 +35,6 @@ pub enum ReasoningEffort { XHigh, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] -pub struct AvailableModelsEvent { - pub models: Vec, -} - /// A reasoning effort option that can be surfaced for a model. #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ReasoningEffortPreset { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6b787afe1..99d2ec70d 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -19,7 +19,6 @@ use crate::message_history::HistoryEntry; use crate::models::ContentItem; use crate::models::ResponseItem; use crate::num_format::format_with_separators; -use crate::openai_models::AvailableModelsEvent; use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; use crate::parse_command::ParsedCommand; use crate::plan_tool::UpdatePlanArgs; @@ -582,7 +581,6 @@ pub enum EventMsg { AgentMessageContentDelta(AgentMessageContentDeltaEvent), ReasoningContentDelta(ReasoningContentDeltaEvent), ReasoningRawContentDelta(ReasoningRawContentDeltaEvent), - ListModelsResponse(AvailableModelsEvent), } /// Codex errors that we expose to clients. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2ae53bc0c..c4059dc85 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1842,7 +1842,6 @@ impl ChatWidget { | EventMsg::ItemCompleted(_) | EventMsg::AgentMessageContentDelta(_) | EventMsg::ReasoningContentDelta(_) - | EventMsg::ListModelsResponse(_) | EventMsg::ReasoningRawContentDelta(_) => {} } }