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
This commit is contained in:
pakrym-oai
2026-08-23 23:52:07 +00:00
committed by copyberry
parent 068847f76d
commit 84c989acf9
4 changed files with 56 additions and 22 deletions
+17 -22
View File
@@ -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<ResponsesApiRequest> {
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
@@ -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()
}
}
+2
View File
@@ -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;
@@ -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"));