diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index dca9f03e75..00c7b0d0c7 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -532,6 +532,7 @@ server_notification_definitions! { McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification), McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification), AccountUpdated => "account/updated" (v2::AccountUpdatedNotification), + ModelPresetsUpdated => "model/presets/updated" (v2::ModelPresetsUpdatedNotification), AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification), ReasoningSummaryTextDelta => "item/reasoning/summaryTextDelta" (v2::ReasoningSummaryTextDeltaNotification), ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 05362b204e..7140a4b257 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1074,6 +1074,13 @@ pub struct AccountUpdatedNotification { pub auth_mode: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ModelPresetsUpdatedNotification { + pub models: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d017d22943..a6ceff06f7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -61,6 +61,7 @@ use codex_app_server_protocol::McpServerOauthLoginParams; use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::ModelPresetsUpdatedNotification; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; use codex_app_server_protocol::RemoveConversationListenerParams; @@ -231,6 +232,17 @@ pub(crate) enum ApiVersion { V2, } +async fn emit_model_presets_notification( + outgoing: Arc, + conversation_manager: Arc, + config: Arc, +) { + let models = supported_models(conversation_manager, &config).await; + let notification = + ServerNotification::ModelPresetsUpdated(ModelPresetsUpdatedNotification { models }); + outgoing.send_server_notification(notification).await; +} + impl CodexMessageProcessor { async fn conversation_from_thread_id( &self, @@ -281,6 +293,15 @@ impl CodexMessageProcessor { } } + pub(crate) async fn send_model_presets_notification(&self) { + emit_model_presets_notification( + self.outgoing.clone(), + self.conversation_manager.clone(), + self.config.clone(), + ) + .await; + } + async fn load_latest_config(&self) -> Result { Config::load_with_cli_overrides(self.cli_overrides.clone(), ConfigOverrides::default()) .await @@ -573,6 +594,7 @@ impl CodexMessageProcessor { self.outgoing .send_server_notification(ServerNotification::AuthStatusChange(payload)) .await; + self.send_model_presets_notification().await; } Err(error) => { self.outgoing.send_error(request_id, error).await; @@ -603,6 +625,7 @@ impl CodexMessageProcessor { self.outgoing .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) .await; + self.send_model_presets_notification().await; } Err(error) => { self.outgoing.send_error(request_id, error).await; @@ -659,6 +682,8 @@ impl CodexMessageProcessor { let outgoing_clone = self.outgoing.clone(); let active_login = self.active_login.clone(); let auth_manager = self.auth_manager.clone(); + let conversation_manager = self.conversation_manager.clone(); + let config = self.config.clone(); let auth_url = server.auth_url.clone(); tokio::spawn(async move { let (success, error_msg) = match tokio::time::timeout( @@ -699,6 +724,12 @@ impl CodexMessageProcessor { payload, )) .await; + emit_model_presets_notification( + outgoing_clone, + conversation_manager, + config, + ) + .await; } // Clear the active login if it matches this attempt. It may have been replaced or cancelled. @@ -749,6 +780,8 @@ impl CodexMessageProcessor { let outgoing_clone = self.outgoing.clone(); let active_login = self.active_login.clone(); let auth_manager = self.auth_manager.clone(); + let conversation_manager = self.conversation_manager.clone(); + let config = self.config.clone(); let auth_url = server.auth_url.clone(); tokio::spawn(async move { let (success, error_msg) = match tokio::time::timeout( @@ -789,6 +822,12 @@ impl CodexMessageProcessor { payload_v2, )) .await; + emit_model_presets_notification( + outgoing_clone, + conversation_manager, + config, + ) + .await; } // Clear the active login if it matches this attempt. It may have been replaced or cancelled. @@ -908,6 +947,7 @@ impl CodexMessageProcessor { self.outgoing .send_server_notification(ServerNotification::AuthStatusChange(payload)) .await; + self.send_model_presets_notification().await; } Err(error) => { self.outgoing.send_error(request_id, error).await; @@ -928,6 +968,7 @@ impl CodexMessageProcessor { self.outgoing .send_server_notification(ServerNotification::AccountUpdated(payload_v2)) .await; + self.send_model_presets_notification().await; } Err(error) => { self.outgoing.send_error(request_id, error).await; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 6a6cf5edb2..d754eb666d 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -128,6 +128,9 @@ impl MessageProcessor { self.outgoing.send_response(request_id, response).await; self.initialized = true; + self.codex_message_processor + .send_model_presets_notification() + .await; return; } diff --git a/codex-rs/docs/codex_mcp_interface.md b/codex-rs/docs/codex_mcp_interface.md index a7236e363e..1ae0e99876 100644 --- a/codex-rs/docs/codex_mcp_interface.md +++ b/codex-rs/docs/codex_mcp_interface.md @@ -20,6 +20,7 @@ At a glance: - Configuration and info - `getUserSavedConfig`, `setDefaultModel`, `getUserAgent`, `userInfo` - `model/list` → enumerate available models and reasoning options + - notifications: `model/presets/updated` - Auth - `account/read`, `account/login/start`, `account/login/cancel`, `account/logout`, `account/rateLimits/read` - notifications: `account/login/completed`, `account/updated`, `account/rateLimits/updated` @@ -94,12 +95,17 @@ Each response yields: - `isDefault` – whether the model is recommended for most users - `nextCursor` – pass into the next request to continue paging (optional) +The server also emits `model/presets/updated` notifications after initialization and after auth state changes (login/logout). The payload is: + +- `models` – the full list of available models, with the same shape as `model/list` items. + ## Event stream While a conversation runs, the server sends notifications: - `codex/event` with the serialized Codex event payload. The shape matches `core/src/protocol.rs`’s `Event` and `EventMsg` types. Some notifications include a `_meta.requestId` to correlate with the originating request. - Auth notifications via method names `loginChatGptComplete` and `authStatusChange`. +- Model catalog notifications via method name `model/presets/updated`. Clients should render events and, when present, surface approval requests (see next section).