mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Reduce cloning when building Responses requests (#34825)
## Why Preparing Responses API requests rebuilt tool definitions as a generic JSON tree, and incremental WebSocket requests cloned their full item prefix for comparison. ## What changed - Serialize tool definitions into shared raw JSON that can be embedded directly in HTTP and WebSocket requests. - Compare incremental request prefixes in place while still ignoring internal message metadata. ## Testing - Verify raw tool JSON matches the existing value encoding. - Preserve the serialized WebSocket request payload. GitOrigin-RevId: 66e2921792c332e8904954dafa597f6d39abcf29
This commit is contained in:
@@ -19,7 +19,7 @@ http = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "stream"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "macros", "net", "rt", "sync", "time"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
|
||||
@@ -12,8 +12,10 @@ use futures::Stream;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::value::RawValue;
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Context;
|
||||
use std::task::Poll;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -29,7 +31,7 @@ pub struct CompactionInput<'a> {
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
pub instructions: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<Value>>,
|
||||
pub tools: Option<ResponsesApiTools>,
|
||||
pub parallel_tool_calls: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -212,6 +214,40 @@ impl From<VerbosityConfig> for OpenAiVerbosity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialized tool definitions for Responses API requests.
|
||||
///
|
||||
/// Keeping the tool list as raw JSON avoids rebuilding a generic JSON value
|
||||
/// tree, while the shared allocation keeps request clones cheap.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResponsesApiTools(Arc<RawValue>);
|
||||
|
||||
impl ResponsesApiTools {
|
||||
pub(crate) fn as_raw_value(&self) -> &RawValue {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<RawValue>> for ResponsesApiTools {
|
||||
fn from(value: Arc<RawValue>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ResponsesApiTools {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0) || self.0.get() == other.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ResponsesApiTools {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, PartialEq)]
|
||||
pub struct ResponsesApiRequest {
|
||||
pub model: String,
|
||||
@@ -219,7 +255,7 @@ pub struct ResponsesApiRequest {
|
||||
pub instructions: String,
|
||||
pub input: Vec<ResponseItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<serde_json::Value>>,
|
||||
pub tools: Option<ResponsesApiTools>,
|
||||
pub tool_choice: String,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<Reasoning>,
|
||||
@@ -245,7 +281,7 @@ impl<'a> From<&'a ResponsesApiRequest> for ResponseCreateWsRequest<'a> {
|
||||
instructions: &request.instructions,
|
||||
previous_response_id: None,
|
||||
input: &request.input,
|
||||
tools: request.tools.as_deref(),
|
||||
tools: request.tools.as_ref().map(ResponsesApiTools::as_raw_value),
|
||||
tool_choice: &request.tool_choice,
|
||||
parallel_tool_calls: request.parallel_tool_calls,
|
||||
reasoning: request.reasoning.as_ref(),
|
||||
@@ -271,7 +307,7 @@ pub struct ResponseCreateWsRequest<'a> {
|
||||
pub previous_response_id: Option<String>,
|
||||
pub input: &'a [ResponseItem],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<&'a [Value]>,
|
||||
pub tools: Option<&'a RawValue>,
|
||||
pub tool_choice: &'a str,
|
||||
pub parallel_tool_calls: bool,
|
||||
pub reasoning: Option<&'a Reasoning>,
|
||||
|
||||
@@ -907,7 +907,10 @@ mod tests {
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::value::to_raw_value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn direct_serialization_preserves_websocket_request_payload() {
|
||||
@@ -923,11 +926,17 @@ mod tests {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: Some(vec![json!({
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"parameters": {"type": "object"}
|
||||
})]),
|
||||
tools: Some(
|
||||
Arc::<RawValue>::from(
|
||||
to_raw_value(&vec![json!({
|
||||
"type": "function",
|
||||
"name": "lookup",
|
||||
"parameters": {"type": "object"}
|
||||
})])
|
||||
.expect("serialize tools"),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
tool_choice: "auto".to_string(),
|
||||
parallel_tool_calls: true,
|
||||
reasoning: None,
|
||||
|
||||
@@ -39,6 +39,7 @@ pub use crate::common::ResponseCreateWsRequest;
|
||||
pub use crate::common::ResponseEvent;
|
||||
pub use crate::common::ResponseStream;
|
||||
pub use crate::common::ResponsesApiRequest;
|
||||
pub use crate::common::ResponsesApiTools;
|
||||
pub use crate::common::ResponsesWsRequest;
|
||||
pub use crate::common::StreamOptions;
|
||||
pub use crate::common::TextControls;
|
||||
|
||||
@@ -28,6 +28,7 @@ use http::HeaderMap;
|
||||
use http::HeaderValue;
|
||||
use http::StatusCode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
fn assert_path_ends_with(requests: &[Request], suffix: &str) {
|
||||
assert_eq!(requests.len(), 1);
|
||||
@@ -38,6 +39,10 @@ fn assert_path_ends_with(requests: &[Request], suffix: &str) {
|
||||
);
|
||||
}
|
||||
|
||||
fn empty_tools() -> Arc<RawValue> {
|
||||
Arc::from(RawValue::from_string("[]".to_string()).expect("valid tool JSON"))
|
||||
}
|
||||
|
||||
fn request_body_bytes(request: &Request) -> &[u8] {
|
||||
let Some(RequestBody::EncodedJson(body)) = request.body.as_ref() else {
|
||||
panic!("expected a prepared request body");
|
||||
@@ -316,7 +321,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: Some(Vec::new()),
|
||||
tools: Some(empty_tools().into()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
@@ -403,7 +408,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
|
||||
model: "gpt-test".into(),
|
||||
instructions: "Say hi".into(),
|
||||
input: Vec::new(),
|
||||
tools: Some(Vec::new()),
|
||||
tools: Some(empty_tools().into()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
@@ -523,7 +528,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> {
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
}],
|
||||
tools: Some(Vec::new()),
|
||||
tools: Some(empty_tools().into()),
|
||||
tool_choice: "auto".into(),
|
||||
parallel_tool_calls: false,
|
||||
reasoning: None,
|
||||
|
||||
Reference in New Issue
Block a user