mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Use input items for Responses Lite tools (#27946)
When using Responses Lite, we should all use `additional_tools` and a developer item instead of the top level tools array & instructions field. This keeps things 1-to-1. Forced namespacing for _all_ tools will land in a following PR after some coordination & fixes in Responses API (around collisions & return items). The goal is to eventually expand the scope of this to _all_ requests from codex, but that will require larger coordination across providers & slower rollout.
This commit is contained in:
1 parent
a22e3d0b82
commit
33cc928d33
22 files changed
+298
-45
No files matched your search
@@ -28,7 +28,8 @@ pub struct CompactionInput<'a> {
|
|||||||
pub input: &'a [ResponseItem],
|
pub input: &'a [ResponseItem],
|
||||||
#[serde(skip_serializing_if = "str::is_empty")]
|
#[serde(skip_serializing_if = "str::is_empty")]
|
||||||
pub instructions: &'a str,
|
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,
|
pub parallel_tool_calls: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub reasoning: Option<Reasoning>,
|
pub reasoning: Option<Reasoning>,
|
||||||
@@ -210,7 +211,8 @@ pub struct ResponsesApiRequest {
|
|||||||
#[serde(skip_serializing_if = "String::is_empty")]
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
pub instructions: String,
|
pub instructions: String,
|
||||||
pub input: Vec<ResponseItem>,
|
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 tool_choice: String,
|
||||||
pub parallel_tool_calls: bool,
|
pub parallel_tool_calls: bool,
|
||||||
pub reasoning: Option<Reasoning>,
|
pub reasoning: Option<Reasoning>,
|
||||||
@@ -258,7 +260,8 @@ pub struct ResponseCreateWsRequest {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub previous_response_id: Option<String>,
|
pub previous_response_id: Option<String>,
|
||||||
pub input: Vec<ResponseItem>,
|
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 tool_choice: String,
|
||||||
pub parallel_tool_calls: bool,
|
pub parallel_tool_calls: bool,
|
||||||
pub reasoning: Option<Reasoning>,
|
pub reasoning: Option<Reasoning>,
|
||||||
|
|||||||
@@ -838,11 +838,11 @@ mod tests {
|
|||||||
phase: None,
|
phase: None,
|
||||||
internal_chat_message_metadata_passthrough: None,
|
internal_chat_message_metadata_passthrough: None,
|
||||||
}],
|
}],
|
||||||
tools: vec![json!({
|
tools: Some(vec![json!({
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"name": "lookup",
|
"name": "lookup",
|
||||||
"parameters": {"type": "object"}
|
"parameters": {"type": "object"}
|
||||||
})],
|
})]),
|
||||||
tool_choice: "auto".to_string(),
|
tool_choice: "auto".to_string(),
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
|
|||||||
phase: None,
|
phase: None,
|
||||||
internal_chat_message_metadata_passthrough: None,
|
internal_chat_message_metadata_passthrough: None,
|
||||||
}],
|
}],
|
||||||
tools: Vec::new(),
|
tools: Some(Vec::new()),
|
||||||
tool_choice: "auto".into(),
|
tool_choice: "auto".into(),
|
||||||
parallel_tool_calls: false,
|
parallel_tool_calls: false,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
@@ -401,7 +401,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
|
|||||||
model: "gpt-test".into(),
|
model: "gpt-test".into(),
|
||||||
instructions: "Say hi".into(),
|
instructions: "Say hi".into(),
|
||||||
input: Vec::new(),
|
input: Vec::new(),
|
||||||
tools: Vec::new(),
|
tools: Some(Vec::new()),
|
||||||
tool_choice: "auto".into(),
|
tool_choice: "auto".into(),
|
||||||
parallel_tool_calls: false,
|
parallel_tool_calls: false,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
@@ -520,7 +520,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
|
|||||||
phase: None,
|
phase: None,
|
||||||
internal_chat_message_metadata_passthrough: None,
|
internal_chat_message_metadata_passthrough: None,
|
||||||
}],
|
}],
|
||||||
tools: Vec::new(),
|
tools: Some(Vec::new()),
|
||||||
tool_choice: "auto".into(),
|
tool_choice: "auto".into(),
|
||||||
parallel_tool_calls: false,
|
parallel_tool_calls: false,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
|||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
RolloutItem::ResponseItem(
|
RolloutItem::ResponseItem(
|
||||||
ResponseItem::AgentMessage { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ use codex_otel::current_span_w3c_trace_context;
|
|||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||||
use codex_protocol::config_types::Verbosity as VerbosityConfig;
|
use codex_protocol::config_types::Verbosity as VerbosityConfig;
|
||||||
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||||
@@ -790,7 +791,6 @@ impl ModelClient {
|
|||||||
service_tier: Option<String>,
|
service_tier: Option<String>,
|
||||||
responses_metadata: &CodexResponsesMetadata,
|
responses_metadata: &CodexResponsesMetadata,
|
||||||
) -> Result<ResponsesApiRequest> {
|
) -> Result<ResponsesApiRequest> {
|
||||||
let instructions = &prompt.base_instructions.text;
|
|
||||||
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
|
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
|
||||||
if !self.state.provider.info().is_openai() {
|
if !self.state.provider.info().is_openai() {
|
||||||
input
|
input
|
||||||
@@ -798,6 +798,28 @@ impl ModelClient {
|
|||||||
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
|
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
|
||||||
}
|
}
|
||||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
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 {
|
||||||
|
id: None,
|
||||||
|
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,
|
||||||
|
internal_chat_message_metadata_passthrough: 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 reasoning = Self::build_reasoning(model_info, effort, summary);
|
||||||
let include = if reasoning.is_some() {
|
let include = if reasoning.is_some() {
|
||||||
vec!["reasoning.encrypted_content".to_string()]
|
vec!["reasoning.encrypted_content".to_string()]
|
||||||
@@ -824,7 +846,7 @@ impl ModelClient {
|
|||||||
let service_tier = model_info.service_tier_for_request(service_tier);
|
let service_tier = model_info.service_tier_for_request(service_tier);
|
||||||
let request = ResponsesApiRequest {
|
let request = ResponsesApiRequest {
|
||||||
model: model_info.slug.clone(),
|
model: model_info.slug.clone(),
|
||||||
instructions: instructions.clone(),
|
instructions,
|
||||||
input,
|
input,
|
||||||
tools,
|
tools,
|
||||||
tool_choice: "auto".to_string(),
|
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::AgentMessage { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ fn serializes_text_verbosity_when_set() {
|
|||||||
model: "gpt-5.4".to_string(),
|
model: "gpt-5.4".to_string(),
|
||||||
instructions: "i".to_string(),
|
instructions: "i".to_string(),
|
||||||
input,
|
input,
|
||||||
tools,
|
tools: Some(tools),
|
||||||
tool_choice: "auto".to_string(),
|
tool_choice: "auto".to_string(),
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
@@ -157,7 +157,7 @@ fn serializes_text_schema_with_strict_format() {
|
|||||||
model: "gpt-5.4".to_string(),
|
model: "gpt-5.4".to_string(),
|
||||||
instructions: "i".to_string(),
|
instructions: "i".to_string(),
|
||||||
input,
|
input,
|
||||||
tools,
|
tools: Some(tools),
|
||||||
tool_choice: "auto".to_string(),
|
tool_choice: "auto".to_string(),
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
@@ -218,7 +218,7 @@ fn omits_text_when_not_set() {
|
|||||||
model: "gpt-5.4".to_string(),
|
model: "gpt-5.4".to_string(),
|
||||||
instructions: "i".to_string(),
|
instructions: "i".to_string(),
|
||||||
input,
|
input,
|
||||||
tools,
|
tools: Some(tools),
|
||||||
tool_choice: "auto".to_string(),
|
tool_choice: "auto".to_string(),
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
@@ -241,7 +241,7 @@ fn serializes_flex_service_tier_when_set() {
|
|||||||
model: "gpt-5.4".to_string(),
|
model: "gpt-5.4".to_string(),
|
||||||
instructions: "i".to_string(),
|
instructions: "i".to_string(),
|
||||||
input: vec![],
|
input: vec![],
|
||||||
tools: vec![],
|
tools: Some(vec![]),
|
||||||
tool_choice: "auto".to_string(),
|
tool_choice: "auto".to_string(),
|
||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
reasoning: None,
|
reasoning: None,
|
||||||
|
|||||||
@@ -352,7 +352,8 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
|
|||||||
ResponseItem::AgentMessage { .. } => true,
|
ResponseItem::AgentMessage { .. } => true,
|
||||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
|
||||||
ResponseItem::CompactionTrigger { .. } => false,
|
ResponseItem::CompactionTrigger { .. } => false,
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
|
|||||||
@@ -386,7 +386,8 @@ impl ContextManager {
|
|||||||
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
output: truncate_function_output_payload(output, policy_with_serialization_budget),
|
||||||
internal_chat_message_metadata_passthrough: metadata.clone(),
|
internal_chat_message_metadata_passthrough: metadata.clone(),
|
||||||
},
|
},
|
||||||
ResponseItem::Message { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::Message { .. }
|
||||||
| ResponseItem::AgentMessage { .. }
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
@@ -476,7 +477,8 @@ pub(crate) fn truncate_function_output_payload(
|
|||||||
fn is_api_message(message: &ResponseItem) -> bool {
|
fn is_api_message(message: &ResponseItem) -> bool {
|
||||||
match message {
|
match message {
|
||||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||||
ResponseItem::AgentMessage { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::FunctionCallOutput { .. }
|
| ResponseItem::FunctionCallOutput { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
@@ -722,7 +724,8 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
|||||||
| ResponseItem::Compaction { .. }
|
| ResponseItem::Compaction { .. }
|
||||||
| ResponseItem::ContextCompaction { .. } => true,
|
| ResponseItem::ContextCompaction { .. } => true,
|
||||||
ResponseItem::CompactionTrigger { .. } => false,
|
ResponseItem::CompactionTrigger { .. } => false,
|
||||||
ResponseItem::FunctionCallOutput { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::FunctionCallOutput { .. }
|
||||||
| ResponseItem::ToolSearchOutput { .. }
|
| ResponseItem::ToolSearchOutput { .. }
|
||||||
| ResponseItem::CustomToolCallOutput { .. }
|
| ResponseItem::CustomToolCallOutput { .. }
|
||||||
| ResponseItem::AgentMessage { .. }
|
| ResponseItem::AgentMessage { .. }
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
|
|||||||
prepare_tool_output_content(content);
|
prepare_tool_output_content(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::AgentMessage { .. }
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
|
|||||||
@@ -2715,6 +2715,7 @@ impl Session {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let prefix = match item {
|
let prefix = match item {
|
||||||
|
ResponseItem::AdditionalTools { .. } => "at",
|
||||||
ResponseItem::Message { .. } => "msg",
|
ResponseItem::Message { .. } => "msg",
|
||||||
ResponseItem::Reasoning { .. } => "rs",
|
ResponseItem::Reasoning { .. } => "rs",
|
||||||
ResponseItem::LocalShellCall { .. } => "lsh",
|
ResponseItem::LocalShellCall { .. } => "lsh",
|
||||||
|
|||||||
@@ -222,6 +222,19 @@ fn assign_missing_response_item_ids_skips_agent_messages() {
|
|||||||
assert!(items[1].id().is_some_and(|id| id.starts_with("msg_")));
|
assert!(items[1].id().is_some_and(|id| id.starts_with("msg_")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_missing_response_item_ids_assigns_additional_tools_ids() {
|
||||||
|
let items = Cow::Owned(vec![ResponseItem::AdditionalTools {
|
||||||
|
id: None,
|
||||||
|
role: "developer".to_string(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
}]);
|
||||||
|
|
||||||
|
let items = Session::assign_missing_response_item_ids(items);
|
||||||
|
|
||||||
|
assert!(items[0].id().is_some_and(|id| id.starts_with("at_")));
|
||||||
|
}
|
||||||
|
|
||||||
fn assistant_message(text: &str) -> ResponseItem {
|
fn assistant_message(text: &str) -> ResponseItem {
|
||||||
ResponseItem::Message {
|
ResponseItem::Message {
|
||||||
id: None,
|
id: None,
|
||||||
|
|||||||
@@ -2091,7 +2091,8 @@ async fn try_run_sampling_request(
|
|||||||
}
|
}
|
||||||
ResponseItem::Reasoning { .. } => true,
|
ResponseItem::Reasoning { .. } => true,
|
||||||
ResponseItem::AgentMessage { .. } => false,
|
ResponseItem::AgentMessage { .. } => false,
|
||||||
ResponseItem::LocalShellCall { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
| ResponseItem::ToolSearchCall { .. }
|
| ResponseItem::ToolSearchCall { .. }
|
||||||
| ResponseItem::FunctionCallOutput { .. }
|
| ResponseItem::FunctionCallOutput { .. }
|
||||||
|
|||||||
@@ -380,7 +380,8 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool {
|
|||||||
| ResponseItem::Compaction { .. }
|
| ResponseItem::Compaction { .. }
|
||||||
| ResponseItem::ContextCompaction { .. } => true,
|
| ResponseItem::ContextCompaction { .. } => true,
|
||||||
ResponseItem::CompactionTrigger { .. } => false,
|
ResponseItem::CompactionTrigger { .. } => false,
|
||||||
ResponseItem::FunctionCallOutput { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::FunctionCallOutput { .. }
|
||||||
| ResponseItem::CustomToolCallOutput { .. }
|
| ResponseItem::CustomToolCallOutput { .. }
|
||||||
| ResponseItem::ToolSearchOutput { .. }
|
| ResponseItem::ToolSearchOutput { .. }
|
||||||
| ResponseItem::Other => false,
|
| ResponseItem::Other => false,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
use codex_protocol::config_types::ServiceTier;
|
use codex_protocol::config_types::ServiceTier;
|
||||||
|
use codex_protocol::protocol::EventMsg;
|
||||||
|
use codex_protocol::protocol::Op;
|
||||||
|
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||||
|
use codex_protocol::user_input::UserInput;
|
||||||
use core_test_support::responses::WebSocketConnectionConfig;
|
use core_test_support::responses::WebSocketConnectionConfig;
|
||||||
use core_test_support::responses::ev_assistant_message;
|
use core_test_support::responses::ev_assistant_message;
|
||||||
use core_test_support::responses::ev_completed;
|
use core_test_support::responses::ev_completed;
|
||||||
@@ -10,12 +14,89 @@ use core_test_support::responses::start_websocket_server;
|
|||||||
use core_test_support::responses::start_websocket_server_with_headers;
|
use core_test_support::responses::start_websocket_server_with_headers;
|
||||||
use core_test_support::skip_if_no_network;
|
use core_test_support::skip_if_no_network;
|
||||||
use core_test_support::test_codex::test_codex;
|
use core_test_support::test_codex::test_codex;
|
||||||
|
use core_test_support::wait_for_event;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06";
|
const WS_V2_BETA_HEADER_VALUE: &str = "responses_websockets=2026-02-06";
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn websocket_model_switch_to_responses_lite_omits_top_level_tools() -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_websocket_server(vec![vec![
|
||||||
|
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
|
||||||
|
vec![ev_response_created("resp-1"), ev_completed("resp-1")],
|
||||||
|
vec![ev_response_created("resp-2"), ev_completed("resp-2")],
|
||||||
|
]])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut builder = test_codex()
|
||||||
|
.with_model_info_override("gpt-5.4", |model_info| {
|
||||||
|
model_info.use_responses_lite = true;
|
||||||
|
})
|
||||||
|
.with_model("gpt-5.3-codex");
|
||||||
|
let test = builder.build_with_websocket_server(&server).await?;
|
||||||
|
|
||||||
|
test.submit_turn("non-lite turn").await?;
|
||||||
|
test.codex
|
||||||
|
.submit(Op::UserInput {
|
||||||
|
items: vec![UserInput::Text {
|
||||||
|
text: "lite turn".into(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
final_output_json_schema: None,
|
||||||
|
responsesapi_client_metadata: None,
|
||||||
|
additional_context: Default::default(),
|
||||||
|
thread_settings: ThreadSettingsOverrides {
|
||||||
|
model: Some("gpt-5.4".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(event, EventMsg::TurnComplete(_))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(server.handshakes().len(), 1);
|
||||||
|
let connection = server.single_connection();
|
||||||
|
assert_eq!(connection.len(), 3);
|
||||||
|
let non_lite_turn = connection
|
||||||
|
.get(1)
|
||||||
|
.expect("missing non-lite turn request")
|
||||||
|
.body_json();
|
||||||
|
let lite_turn = connection
|
||||||
|
.get(2)
|
||||||
|
.expect("missing lite turn request")
|
||||||
|
.body_json();
|
||||||
|
|
||||||
|
assert_eq!(non_lite_turn["model"].as_str(), Some("gpt-5.3-codex"));
|
||||||
|
assert_eq!(lite_turn["model"].as_str(), Some("gpt-5.4"));
|
||||||
|
assert!(
|
||||||
|
non_lite_turn
|
||||||
|
.get("tools")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.is_some_and(|tools| !tools.is_empty())
|
||||||
|
);
|
||||||
|
assert_eq!(lite_turn.get("previous_response_id"), None);
|
||||||
|
assert_eq!(lite_turn.get("tools"), None);
|
||||||
|
assert_eq!(lite_turn.get("instructions"), None);
|
||||||
|
let additional_tools = lite_turn
|
||||||
|
.get("input")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.and_then(|input| input.first())
|
||||||
|
.filter(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools"))
|
||||||
|
.and_then(|item| item.get("tools"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.expect("lite turn should start with an additional_tools item");
|
||||||
|
assert!(!additional_tools.is_empty());
|
||||||
|
|
||||||
|
server.shutdown().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn websocket_test_codex_shell_chain() -> Result<()> {
|
async fn websocket_test_codex_shell_chain() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
@@ -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))
|
.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")?
|
||||||
|
.first()
|
||||||
|
.filter(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools"))
|
||||||
|
.context("Responses request should start with 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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn responses_lite_prepares_images() -> Result<()> {
|
async fn responses_lite_prepares_images() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
@@ -158,17 +222,10 @@ async fn responses_lite_uses_standalone_web_search_and_image_generation() -> Res
|
|||||||
request.header(RESPONSES_LITE_HEADER).as_deref(),
|
request.header(RESPONSES_LITE_HEADER).as_deref(),
|
||||||
Some("true")
|
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 body = request.body_json();
|
||||||
let tools = body["tools"]
|
assert!(body.get("tools").is_none());
|
||||||
.as_array()
|
let tools = additional_tools(&body)?;
|
||||||
.context("Responses request tools should be an array")?;
|
assert!(!tools.is_empty());
|
||||||
assert!(!has_hosted_tool(tools, "web_search"));
|
assert!(!has_hosted_tool(tools, "web_search"));
|
||||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||||
|
|
||||||
@@ -256,9 +313,8 @@ async fn responses_lite_omits_hosted_tools_without_standalone_extensions() -> Re
|
|||||||
test.submit_turn("Do not use hosted tools").await?;
|
test.submit_turn("Do not use hosted tools").await?;
|
||||||
|
|
||||||
let body = response_mock.single_request().body_json();
|
let body = response_mock.single_request().body_json();
|
||||||
let tools = body["tools"]
|
assert!(body.get("tools").is_none());
|
||||||
.as_array()
|
let tools = additional_tools(&body)?;
|
||||||
.context("Responses request tools should be an array")?;
|
|
||||||
assert!(!has_hosted_tool(tools, "web_search"));
|
assert!(!has_hosted_tool(tools, "web_search"));
|
||||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||||
|
|
||||||
|
|||||||
@@ -256,7 +256,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
|
|||||||
ResponseItem::CustomToolCall { call_id, .. } => {
|
ResponseItem::CustomToolCall { call_id, .. } => {
|
||||||
custom_tool_call_ids.insert(call_id.as_str());
|
custom_tool_call_ids.insert(call_id.as_str());
|
||||||
}
|
}
|
||||||
ResponseItem::Message { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::Message { .. }
|
||||||
| ResponseItem::AgentMessage { .. }
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
@@ -296,7 +297,8 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
|
|||||||
ResponseItem::ImageGenerationCall { result, .. } if !result.is_empty() => {
|
ResponseItem::ImageGenerationCall { result, .. } if !result.is_empty() => {
|
||||||
image_urls.push(format!("data:image/png;base64,{result}"));
|
image_urls.push(format!("data:image/png;base64,{result}"));
|
||||||
}
|
}
|
||||||
ResponseItem::Reasoning { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::AgentMessage { .. }
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::LocalShellCall { .. }
|
| ResponseItem::LocalShellCall { .. }
|
||||||
| ResponseItem::FunctionCall { .. }
|
| ResponseItem::FunctionCall { .. }
|
||||||
|
|||||||
@@ -1198,6 +1198,7 @@ impl SessionTelemetry {
|
|||||||
|
|
||||||
fn responses_item_type(item: &ResponseItem) -> String {
|
fn responses_item_type(item: &ResponseItem) -> String {
|
||||||
match item {
|
match item {
|
||||||
|
ResponseItem::AdditionalTools { .. } => "additional_tools".into(),
|
||||||
ResponseItem::Message { role, .. } => format!("message_from_{role}"),
|
ResponseItem::Message { role, .. } => format!("message_from_{role}"),
|
||||||
ResponseItem::AgentMessage { .. } => "agent_message".into(),
|
ResponseItem::AgentMessage { .. } => "agent_message".into(),
|
||||||
ResponseItem::Reasoning { .. } => "reasoning".into(),
|
ResponseItem::Reasoning { .. } => "reasoning".into(),
|
||||||
|
|||||||
@@ -932,6 +932,14 @@ impl InternalChatMessageMetadataPassthrough {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum ResponseItem {
|
pub enum ResponseItem {
|
||||||
|
#[schemars(skip)]
|
||||||
|
#[ts(skip)]
|
||||||
|
AdditionalTools {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
id: Option<String>,
|
||||||
|
role: String,
|
||||||
|
tools: Vec<serde_json::Value>,
|
||||||
|
},
|
||||||
Message {
|
Message {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
@@ -1160,7 +1168,8 @@ impl ResponseItem {
|
|||||||
/// Returns the non-empty Responses API item ID, if present.
|
/// Returns the non-empty Responses API item ID, if present.
|
||||||
pub fn id(&self) -> Option<&str> {
|
pub fn id(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::Message { id, .. }
|
Self::AdditionalTools { id, .. }
|
||||||
|
| Self::Message { id, .. }
|
||||||
| Self::AgentMessage { id, .. }
|
| Self::AgentMessage { id, .. }
|
||||||
| Self::LocalShellCall { id, .. }
|
| Self::LocalShellCall { id, .. }
|
||||||
| Self::FunctionCall { id, .. }
|
| Self::FunctionCall { id, .. }
|
||||||
@@ -1181,7 +1190,8 @@ impl ResponseItem {
|
|||||||
/// Sets or clears the Responses API item ID for variants that carry one.
|
/// Sets or clears the Responses API item ID for variants that carry one.
|
||||||
pub fn set_id(&mut self, new_id: Option<String>) {
|
pub fn set_id(&mut self, new_id: Option<String>) {
|
||||||
match self {
|
match self {
|
||||||
Self::Message { id, .. }
|
Self::AdditionalTools { id, .. }
|
||||||
|
| Self::Message { id, .. }
|
||||||
| Self::AgentMessage { id, .. }
|
| Self::AgentMessage { id, .. }
|
||||||
| Self::LocalShellCall { id, .. }
|
| Self::LocalShellCall { id, .. }
|
||||||
| Self::FunctionCall { id, .. }
|
| Self::FunctionCall { id, .. }
|
||||||
@@ -1282,7 +1292,7 @@ impl ResponseItem {
|
|||||||
internal_chat_message_metadata_passthrough: metadata,
|
internal_chat_message_metadata_passthrough: metadata,
|
||||||
..
|
..
|
||||||
} => metadata.as_ref(),
|
} => metadata.as_ref(),
|
||||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1346,7 +1356,7 @@ impl ResponseItem {
|
|||||||
internal_chat_message_metadata_passthrough: metadata,
|
internal_chat_message_metadata_passthrough: metadata,
|
||||||
..
|
..
|
||||||
} => Some(metadata),
|
} => Some(metadata),
|
||||||
Self::CompactionTrigger { .. } | Self::Other => None,
|
Self::CompactionTrigger { .. } | Self::AdditionalTools { .. } | Self::Other => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2237,6 +2247,14 @@ mod tests {
|
|||||||
item.set_id(/*new_id*/ None);
|
item.set_id(/*new_id*/ None);
|
||||||
|
|
||||||
assert_eq!(item.id(), None);
|
assert_eq!(item.id(), None);
|
||||||
|
|
||||||
|
let mut additional_tools = ResponseItem::AdditionalTools {
|
||||||
|
id: None,
|
||||||
|
role: "developer".to_string(),
|
||||||
|
tools: Vec::new(),
|
||||||
|
};
|
||||||
|
additional_tools.set_id(Some("at_test".to_string()));
|
||||||
|
assert_eq!(additional_tools.id(), Some("at_test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn response_item_with_passthrough_metadata(
|
fn response_item_with_passthrough_metadata(
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ pub(super) fn normalize_model_items(
|
|||||||
) -> Result<Vec<NormalizedConversationItem>> {
|
) -> Result<Vec<NormalizedConversationItem>> {
|
||||||
let mut normalized_items = Vec::new();
|
let mut normalized_items = Vec::new();
|
||||||
for item in items {
|
for item in items {
|
||||||
|
if item.get("type").and_then(Value::as_str) == Some("additional_tools") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
normalized_items.push(normalize_model_item(item, raw_payload)?);
|
normalized_items.push(normalize_model_item(item, raw_payload)?);
|
||||||
}
|
}
|
||||||
Ok(normalized_items)
|
Ok(normalized_items)
|
||||||
|
|||||||
@@ -735,6 +735,48 @@ fn unsupported_model_item_is_reducer_error() -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn additional_tools_are_excluded_from_request_conversation() -> anyhow::Result<()> {
|
||||||
|
let temp = TempDir::new()?;
|
||||||
|
let writer = create_started_writer(&temp)?;
|
||||||
|
start_turn(&writer, "turn-1")?;
|
||||||
|
|
||||||
|
let request = writer.write_json_payload(
|
||||||
|
RawPayloadKind::InferenceRequest,
|
||||||
|
&json!({
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"type": "additional_tools",
|
||||||
|
"role": "developer",
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"name": "lookup",
|
||||||
|
"parameters": {"type": "object", "properties": {}}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
message("user", "find it")
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
append_inference_start(&writer, "inference-1", "turn-1", request)?;
|
||||||
|
|
||||||
|
let rollout = replay_bundle(temp.path())?;
|
||||||
|
let request_item_ids = &rollout.inference_calls["inference-1"].request_item_ids;
|
||||||
|
|
||||||
|
assert_eq!(request_item_ids.len(), 1);
|
||||||
|
assert_eq!(rollout.conversation_items.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
rollout.conversation_items[&request_item_ids[0]].body,
|
||||||
|
ConversationBody {
|
||||||
|
parts: vec![ConversationPart::Text {
|
||||||
|
text: "find it".to_string(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_request_input_is_reducer_error() -> anyhow::Result<()> {
|
fn missing_request_input_is_reducer_error() -> anyhow::Result<()> {
|
||||||
let temp = TempDir::new()?;
|
let temp = TempDir::new()?;
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ pub fn should_persist_response_item(item: &ResponseItem) -> bool {
|
|||||||
| ResponseItem::ImageGenerationCall { .. }
|
| ResponseItem::ImageGenerationCall { .. }
|
||||||
| ResponseItem::Compaction { .. }
|
| ResponseItem::Compaction { .. }
|
||||||
| ResponseItem::ContextCompaction { .. } => true,
|
| ResponseItem::ContextCompaction { .. } => true,
|
||||||
ResponseItem::CompactionTrigger { .. } => false,
|
ResponseItem::AdditionalTools { .. }
|
||||||
ResponseItem::Other => false,
|
| ResponseItem::CompactionTrigger { .. }
|
||||||
|
| ResponseItem::Other => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,8 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool {
|
|||||||
| ResponseItem::CustomToolCall { .. }
|
| ResponseItem::CustomToolCall { .. }
|
||||||
| ResponseItem::CustomToolCallOutput { .. }
|
| ResponseItem::CustomToolCallOutput { .. }
|
||||||
| ResponseItem::WebSearchCall { .. } => true,
|
| ResponseItem::WebSearchCall { .. } => true,
|
||||||
ResponseItem::AgentMessage { .. }
|
ResponseItem::AdditionalTools { .. }
|
||||||
|
| ResponseItem::AgentMessage { .. }
|
||||||
| ResponseItem::Reasoning { .. }
|
| ResponseItem::Reasoning { .. }
|
||||||
| ResponseItem::ImageGenerationCall { .. }
|
| ResponseItem::ImageGenerationCall { .. }
|
||||||
| ResponseItem::Compaction { .. }
|
| ResponseItem::Compaction { .. }
|
||||||
|
|||||||
Reference in new issue
Block a user