diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 1d30253303..582692b76c 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -31,6 +31,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { available_access_programs: preset.available_access_programs.clone(), upgrade: preset.upgrade.as_ref().map(Into::into), model_messages: Some(ModelMessages { + apps: None, content_filter_guidance: None, persistent_instructions: None, tools: None, diff --git a/codex-rs/core/tests/suite/catalog_permission_messages.rs b/codex-rs/core/tests/suite/catalog_permission_messages.rs index 7e6d8adb66..67c3cbd804 100644 --- a/codex-rs/core/tests/suite/catalog_permission_messages.rs +++ b/codex-rs/core/tests/suite/catalog_permission_messages.rs @@ -40,6 +40,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent( let model_slug = "remote-catalog-permissions-model"; let mut model = model_info_from_slug(model_slug); model.model_messages = Some(ModelMessages { + apps: None, content_filter_guidance: None, persistent_instructions: None, tools: None, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index deb15f4cd4..3cf3874144 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -74,6 +74,7 @@ fn model_with_collaboration_messages( ) -> codex_protocol::openai_models::ModelInfo { let mut model = model_info_from_slug(slug); let model_messages = model.model_messages.get_or_insert(ModelMessages { + apps: None, content_filter_guidance: None, persistent_instructions: None, tools: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 842e923427..a160daf644 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -643,6 +643,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { available_access_programs: None, upgrade: None, model_messages: Some(ModelMessages { + apps: None, content_filter_guidance: None, persistent_instructions: None, tools: None, diff --git a/codex-rs/models-manager/src/model_info_tests.rs b/codex-rs/models-manager/src/model_info_tests.rs index bec7bf88fd..dc061ccffe 100644 --- a/codex-rs/models-manager/src/model_info_tests.rs +++ b/codex-rs/models-manager/src/model_info_tests.rs @@ -3,6 +3,7 @@ use crate::ModelsManagerConfig; use codex_prompts::render_model_instructions; use codex_protocol::config_types::Personality; use codex_protocol::openai_models::ApprovalMessages; +use codex_protocol::openai_models::AppsMessages; use codex_protocol::openai_models::AutoReviewMessages; use codex_protocol::openai_models::CollaborationModeMessages; use codex_protocol::openai_models::ConfirmationPolicies; @@ -90,6 +91,9 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() { computer_use: Some(" # Native policy\r\n\n${native_markdown}\n".to_string()), }; let mut messages = ModelMessages { + apps: Some(AppsMessages { + usage_instructions: Some("Catalog Apps instructions.".to_string()), + }), content_filter_guidance: None, persistent_instructions: Some(persistent_instructions.to_string()), tools: Some(ToolMessages { diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 4e36332bdc..ade3654968 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -580,6 +580,9 @@ impl ModelInfo { /// retained for model catalog compatibility. #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelMessages { + /// Optional model-owned Apps wording, transported without runtime interpretation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub apps: Option, /// Developer guidance after a content-filter block. Missing, null, blank, or values over /// 512 UTF-8 bytes use the bundled guidance. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -605,6 +608,15 @@ pub struct ModelMessages { pub confirmation_policies: Option, } +/// Optional Apps wording from the model catalog, preserved for future runtime consumption. +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] +pub struct AppsMessages { + /// Missing and null deserialize as `None`; all strings preserve their exact bytes. + /// Rendering, size limits, and empty-string behavior are defined by consumers. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub usage_instructions: Option, +} + /// Model-owned confirmation-policy Markdown, forwarded unchanged to actor tools. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ConfirmationPolicies { @@ -1225,6 +1237,32 @@ mod tests { assert_eq!(messages, ModelMessages::default()); } + #[test] + fn apps_messages_preserve_nullable_and_literal_wire_values() { + for apps in [ + serde_json::Value::Null, + serde_json::json!({}), + serde_json::json!({"usage_instructions": null}), + serde_json::json!({"usage_instructions": ""}), + serde_json::json!({"usage_instructions": " \r\n\t"}), + serde_json::json!({"usage_instructions": " {{literal}}\r\n"}), + serde_json::json!({"usage_instructions": "é".repeat(501)}), + ] { + let messages: ModelMessages = + serde_json::from_value(serde_json::json!({"apps": apps})).unwrap(); + let serialized = serde_json::to_value(messages).unwrap(); + let mut expected = serde_json::to_value(ModelMessages::default()).unwrap(); + if !apps.is_null() { + expected["apps"] = if apps["usage_instructions"].is_null() { + serde_json::json!({}) + } else { + apps + }; + } + assert_eq!(serialized, expected); + } + } + #[test] fn send_user_message_async_description_preserves_missing_null_and_empty_values() { for (value, expected) in [ @@ -1723,6 +1761,9 @@ mod tests { #[test] fn models_response_prefers_template_and_preserves_message_siblings() { let messages = ModelMessages { + apps: Some(AppsMessages { + usage_instructions: Some("Literal Apps {{guidance}}".to_string()), + }), content_filter_guidance: Some("Offer a permitted alternative.".to_string()), persistent_instructions: Some("Persistent catalog instructions".to_string()), tools: Some(ToolMessages {