mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Use input items for Responses Lite tools
This commit is contained in:
@@ -28,7 +28,8 @@ pub struct CompactionInput<'a> {
|
||||
pub input: &'a [ResponseItem],
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
pub instructions: &'a str,
|
||||
pub tools: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Value>>,
|
||||
pub parallel_tool_calls: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -192,7 +193,8 @@ pub struct ResponsesApiRequest {
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub instructions: String,
|
||||
pub input: Vec<ResponseItem>,
|
||||
pub tools: Vec<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<serde_json::Value>>,
|
||||
pub tool_choice: String,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -240,7 +242,8 @@ pub struct ResponseCreateWsRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previous_response_id: Option<String>,
|
||||
pub input: Vec<ResponseItem>,
|
||||
pub tools: Vec<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Value>>,
|
||||
pub tool_choice: String,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<Reasoning>,
|
||||
|
||||
@@ -401,7 +401,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
|
||||
model: "gpt-test".into(),
|
||||
instructions: "Say hi".into(),
|
||||
input: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
@@ -520,7 +520,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
|
||||
phase: None,
|
||||
metadata: None,
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
|
||||
@@ -41,7 +41,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
||||
_ => false,
|
||||
},
|
||||
RolloutItem::ResponseItem(
|
||||
ResponseItem::AgentMessage { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -73,6 +73,7 @@ use codex_otel::current_span_w3c_trace_context;
|
||||
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;
|
||||
@@ -778,12 +779,31 @@ impl ModelClient {
|
||||
service_tier: Option<String>,
|
||||
responses_metadata: &CodexResponsesMetadata,
|
||||
) -> Result<ResponsesApiRequest> {
|
||||
let instructions = &prompt.base_instructions.text;
|
||||
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
|
||||
if !self.state.provider.info().is_openai() {
|
||||
input.iter_mut().for_each(ResponseItem::clear_metadata);
|
||||
}
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let (instructions, tools) = if model_info.use_responses_lite {
|
||||
let mut prefix = vec![ResponseItem::AdditionalTools {
|
||||
role: "developer".to_string(),
|
||||
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,
|
||||
});
|
||||
}
|
||||
input.splice(0..0, prefix);
|
||||
(String::new(), None)
|
||||
} else {
|
||||
(prompt.base_instructions.text.clone(), Some(tools))
|
||||
};
|
||||
let reasoning = Self::build_reasoning(model_info, effort, summary);
|
||||
let include = if reasoning.is_some() {
|
||||
vec!["reasoning.encrypted_content".to_string()]
|
||||
@@ -810,7 +830,7 @@ impl ModelClient {
|
||||
let service_tier = model_info.service_tier_for_request(service_tier);
|
||||
let request = ResponsesApiRequest {
|
||||
model: model_info.slug.clone(),
|
||||
instructions: instructions.clone(),
|
||||
instructions,
|
||||
input,
|
||||
tools,
|
||||
tool_choice: "auto".to_string(),
|
||||
|
||||
@@ -83,7 +83,8 @@ fn strip_image_details(items: &mut [ResponseItem]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -104,13 +104,11 @@ fn responses_lite_request_copies_strip_image_details() {
|
||||
|
||||
#[test]
|
||||
fn serializes_text_verbosity_when_set() {
|
||||
let input: Vec<ResponseItem> = vec![];
|
||||
let tools: Vec<serde_json::Value> = vec![];
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
input: vec![],
|
||||
tools: Some(vec![]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -137,8 +135,6 @@ fn serializes_text_verbosity_when_set() {
|
||||
|
||||
#[test]
|
||||
fn serializes_text_schema_with_strict_format() {
|
||||
let input: Vec<ResponseItem> = vec![];
|
||||
let tools: Vec<serde_json::Value> = vec![];
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -156,8 +152,8 @@ fn serializes_text_schema_with_strict_format() {
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
input: vec![],
|
||||
tools: Some(vec![]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -212,13 +208,11 @@ fn serializes_text_schema_with_non_strict_format() {
|
||||
|
||||
#[test]
|
||||
fn omits_text_when_not_set() {
|
||||
let input: Vec<ResponseItem> = vec![];
|
||||
let tools: Vec<serde_json::Value> = vec![];
|
||||
let req = ResponsesApiRequest {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input,
|
||||
tools,
|
||||
input: vec![],
|
||||
tools: Some(vec![]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
@@ -241,7 +235,7 @@ fn serializes_flex_service_tier_when_set() {
|
||||
model: "gpt-5.4".to_string(),
|
||||
instructions: "i".to_string(),
|
||||
input: vec![],
|
||||
tools: vec![],
|
||||
tools: Some(vec![]),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
|
||||
@@ -352,7 +352,8 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
||||
ResponseItem::AgentMessage { .. } => true,
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger { .. } => false,
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
|
||||
@@ -362,7 +362,8 @@ impl ContextManager {
|
||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||
metadata: metadata.clone(),
|
||||
},
|
||||
ResponseItem::Message { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Message { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
@@ -452,7 +453,8 @@ pub(crate) fn truncate_function_output_payload(
|
||||
fn is_api_message(message: &ResponseItem) -> bool {
|
||||
match message {
|
||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||
ResponseItem::AgentMessage { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
@@ -698,7 +700,8 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
| ResponseItem::Compaction { .. }
|
||||
| ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger { .. } => false,
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
|
||||
@@ -52,7 +52,8 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
|
||||
prepare_tool_output_content(content);
|
||||
}
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -2072,7 +2072,8 @@ async fn try_run_sampling_request(
|
||||
}
|
||||
ResponseItem::Reasoning { .. } => true,
|
||||
ResponseItem::AgentMessage { .. } => false,
|
||||
ResponseItem::LocalShellCall { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
|
||||
@@ -380,7 +380,8 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool {
|
||||
| ResponseItem::Compaction { .. }
|
||||
| ResponseItem::ContextCompaction { .. } => true,
|
||||
ResponseItem::CompactionTrigger { .. } => false,
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::Other => false,
|
||||
|
||||
@@ -54,6 +54,70 @@ fn has_hosted_tool(tools: &[Value], tool_type: &str) -> bool {
|
||||
.any(|tool| tool.get("type").and_then(Value::as_str) == Some(tool_type))
|
||||
}
|
||||
|
||||
fn additional_tools(body: &Value) -> Result<&[Value]> {
|
||||
body["input"]
|
||||
.as_array()
|
||||
.context("Responses request input should be an array")?
|
||||
.iter()
|
||||
.find(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools"))
|
||||
.context("Responses request should contain additional_tools")?["tools"]
|
||||
.as_array()
|
||||
.map(Vec::as_slice)
|
||||
.context("additional_tools tools should be an array")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_model_info_override("gpt-5.4", |model_info| {
|
||||
model_info.use_responses_lite = true;
|
||||
})
|
||||
.with_config(|config| {
|
||||
config.base_instructions = Some("test instructions".to_string());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let body = response_mock.single_request().body_json();
|
||||
assert!(body.get("instructions").is_none());
|
||||
assert!(body.get("tools").is_none());
|
||||
|
||||
let input = body["input"]
|
||||
.as_array()
|
||||
.context("Responses request input should be an array")?;
|
||||
assert_eq!(input[0]["type"], "additional_tools");
|
||||
assert_eq!(input[0]["role"], "developer");
|
||||
assert_eq!(
|
||||
input[1],
|
||||
serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "developer",
|
||||
"content": [{
|
||||
"type": "input_text",
|
||||
"text": "test instructions",
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!tools.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_strips_data_image_detail_without_resize_all_images() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -144,17 +208,10 @@ async fn responses_lite_uses_standalone_web_search_and_image_generation() -> Res
|
||||
request.header(RESPONSES_LITE_HEADER).as_deref(),
|
||||
Some("true")
|
||||
);
|
||||
request
|
||||
.tool_by_name("web", "run")
|
||||
.context("Responses Lite should expose standalone web search")?;
|
||||
request
|
||||
.tool_by_name("image_gen", "imagegen")
|
||||
.context("Responses Lite should expose standalone image generation")?;
|
||||
|
||||
let body = request.body_json();
|
||||
let tools = body["tools"]
|
||||
.as_array()
|
||||
.context("Responses request tools should be an array")?;
|
||||
assert!(body.get("tools").is_none());
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!tools.is_empty());
|
||||
assert!(!has_hosted_tool(tools, "web_search"));
|
||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||
|
||||
@@ -242,9 +299,8 @@ async fn responses_lite_omits_hosted_tools_without_standalone_extensions() -> Re
|
||||
test.submit_turn("Do not use hosted tools").await?;
|
||||
|
||||
let body = response_mock.single_request().body_json();
|
||||
let tools = body["tools"]
|
||||
.as_array()
|
||||
.context("Responses request tools should be an array")?;
|
||||
assert!(body.get("tools").is_none());
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!has_hosted_tool(tools, "web_search"));
|
||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||
|
||||
|
||||
@@ -256,7 +256,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
|
||||
ResponseItem::CustomToolCall { call_id, .. } => {
|
||||
custom_tool_call_ids.insert(call_id.as_str());
|
||||
}
|
||||
ResponseItem::Message { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Message { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
@@ -296,7 +297,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
|
||||
ResponseItem::ImageGenerationCall { result, .. } if !result.is_empty() => {
|
||||
image_urls.push(format!("data:image/png;base64,{result}"));
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
|
||||
@@ -1218,6 +1218,7 @@ impl SessionTelemetry {
|
||||
|
||||
fn responses_item_type(item: &ResponseItem) -> String {
|
||||
match item {
|
||||
ResponseItem::AdditionalTools { .. } => "additional_tools".into(),
|
||||
ResponseItem::Message { role, .. } => format!("message_from_{role}"),
|
||||
ResponseItem::AgentMessage { .. } => "agent_message".into(),
|
||||
ResponseItem::Reasoning { .. } => "reasoning".into(),
|
||||
|
||||
@@ -917,6 +917,12 @@ pub struct ResponseItemMetadata {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResponseItem {
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
AdditionalTools {
|
||||
role: String,
|
||||
tools: Vec<serde_json::Value>,
|
||||
},
|
||||
Message {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
|
||||
@@ -30,7 +30,8 @@ pub fn persisted_rollout_items(items: &[RolloutItem]) -> Vec<RolloutItem> {
|
||||
#[inline]
|
||||
pub fn should_persist_response_item(item: &ResponseItem) -> bool {
|
||||
match item {
|
||||
ResponseItem::Message { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::Message { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
@@ -62,7 +63,8 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool {
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::WebSearchCall { .. } => true,
|
||||
ResponseItem::AgentMessage { .. }
|
||||
ResponseItem::AdditionalTools { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::ImageGenerationCall { .. }
|
||||
| ResponseItem::Compaction { .. }
|
||||
|
||||
Reference in New Issue
Block a user