From 84c989acf9af93f35c2f3c36b297cd4dc0f830b3 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Sun, 23 Aug 2026 23:46:39 +0000 Subject: [PATCH] Annotate Responses Lite base instructions (#40296) ## What changed - Build Responses Lite base instructions through a dedicated context fragment so the developer message carries the `model.base_instructions` content kind. - Apply provider metadata cleanup after adding the base-instructions message, preserving the existing behavior for non-OpenAI providers. ## Testing - Extend Responses Lite request coverage to verify the annotation is present for OpenAI requests and omitted for a third-party provider. GitOrigin-RevId: b0317f92dfb9570860688b49e3a7668da516ee8c --- codex-rs/core/src/client.rs | 39 ++++++++----------- .../core/src/context/base_instructions.rs | 31 +++++++++++++++ codex-rs/core/src/context/mod.rs | 2 + codex-rs/core/tests/suite/responses_lite.rs | 6 +++ 4 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 codex-rs/core/src/context/base_instructions.rs diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 5c95a4871e..dc0323cb9d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -78,7 +78,6 @@ use codex_protocol::auth::AuthMode; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::Verbosity as VerbosityConfig; -use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -115,6 +114,8 @@ use crate::attestation::X_OAI_ATTESTATION_HEADER; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; +use crate::context::BaseInstructionsFragment; +use crate::context::ContextualUserFragment; use crate::feedback_tags; use crate::responses_metadata::CodexResponsesMetadata; use crate::responses_metadata::subagent_header_value; @@ -875,18 +876,6 @@ impl ModelClient { ) -> Result { let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); let is_openai = self.state.provider.info().is_openai(); - if !is_openai { - for item in &mut input { - item.clear_internal_chat_message_metadata_passthrough(); - if let ResponseItem::FunctionCall { - encrypted_function_args, - .. - } = item - { - *encrypted_function_args = None; - } - } - } let (instructions, tools) = if model_info.use_responses_lite { let tools = if self.state.provider.capabilities().namespace_tools { create_tools_json_for_responses_lite(&prompt.tools)? @@ -899,15 +888,9 @@ impl ModelClient { tools, }]; if !prompt.base_instructions.text.is_empty() { - prefix.push(ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: prompt.base_instructions.text.clone(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }); + prefix.push(ContextualUserFragment::into(BaseInstructionsFragment( + prompt.base_instructions.text.clone(), + ))); } input.splice(0..0, prefix); (String::new(), None) @@ -917,6 +900,18 @@ impl ModelClient { Some(create_tools_raw_json_for_responses_api(&prompt.tools)?.into()), ) }; + if !is_openai { + for item in &mut input { + item.clear_internal_chat_message_metadata_passthrough(); + if let ResponseItem::FunctionCall { + encrypted_function_args, + .. + } = item + { + *encrypted_function_args = None; + } + } + } let reasoning = Self::build_reasoning(model_info, effort, summary); let stream_options = (self.state.concurrent_reasoning_summaries_enabled && is_openai diff --git a/codex-rs/core/src/context/base_instructions.rs b/codex-rs/core/src/context/base_instructions.rs new file mode 100644 index 0000000000..b4dc3677ce --- /dev/null +++ b/codex-rs/core/src/context/base_instructions.rs @@ -0,0 +1,31 @@ +use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct BaseInstructionsFragment(pub(crate) String); + +impl ContextualUserFragment for BaseInstructionsFragment { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("model.base_instructions".to_string()) + } + + fn role(&self) -> &'static str { + "developer" + } + + fn requires_separate_message(&self) -> bool { + true + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ("", "") + } + + fn body(&self) -> String { + self.0.clone() + } +} diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index 378fe0cb08..44628cc08f 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -3,6 +3,7 @@ mod approved_command_prefix_saved; mod apps_instructions; mod available_plugins_instructions; +mod base_instructions; mod compaction_summary; mod contextual_user_message; mod current_time_reminder; @@ -47,6 +48,7 @@ pub(crate) use approved_command_prefix_saved::APPROVED_COMMAND_PREFIX_SAVED_MESS pub(crate) use approved_command_prefix_saved::ApprovedCommandPrefixSaved; pub(crate) use apps_instructions::AppsInstructions; pub(crate) use available_plugins_instructions::AvailablePluginsInstructions; +pub(crate) use base_instructions::BaseInstructionsFragment; pub(crate) use codex_context_fragments::AdditionalContextDeveloperFragment; pub(crate) use codex_context_fragments::AdditionalContextUserFragment; pub use codex_context_fragments::ContextualUserFragment; diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index a0bc5ed71c..2636c24b1a 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -109,6 +109,7 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< }) .with_config(|config| { config.base_instructions = Some("test instructions".to_string()); + config.code_mode.disable_in_process_fallback = true; }); let test = builder.build(&server).await?; @@ -132,6 +133,9 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result< "type": "input_text", "text": "test instructions", }], + "internal_chat_message_metadata_passthrough": { + "content_item_kinds": ["model.base_instructions"], + }, }) ); @@ -185,6 +189,7 @@ async fn responses_lite_includes_tool_namespaces_info_when_enabled() -> Result<( model_info.supports_search_tool = false; }) .with_config(|config| { + config.code_mode.disable_in_process_fallback = true; config.tool_registry.turn_metadata_includes_tool_info = true; }); let test = builder.build_with_auto_env(&server).await?; @@ -456,6 +461,7 @@ async fn responses_lite_does_not_expose_standalone_web_search_for_bedrock_provid ); let body = request.body_json(); assert!(body.get("tools").is_none()); + assert!(!request.has_content_kinds(&["model.base_instructions"])); let tools = additional_tools(&body)?; assert!(!has_namespaced_tool(tools, "web", "run")); assert!(!has_hosted_tool(tools, "web_search"));