Use responses request helpers for compact requests

Make /responses/compact reuse the shared responses request construction and serialization path, then adapt only the fields rejected by the compact endpoint. Preserve compact-specific headers and add remote compact parity coverage across auth modes.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-05-06 00:23:01 +03:00
co-authored by Codex
parent 8c88f9a304
commit e3626db649
14 changed files with 759 additions and 190 deletions
+17 -24
View File
@@ -20,21 +20,6 @@ use tokio::sync::mpsc;
pub const WS_REQUEST_HEADER_TRACEPARENT_CLIENT_METADATA_KEY: &str = "ws_request_header_traceparent";
pub const WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY: &str = "ws_request_header_tracestate";
/// Canonical input payload for the compaction endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct CompactionInput<'a> {
pub model: &'a str,
pub input: &'a [ResponseItem],
#[serde(skip_serializing_if = "str::is_empty")]
pub instructions: &'a str,
pub tools: Vec<Value>,
pub parallel_tool_calls: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextControls>,
}
/// Canonical input payload for the memory summarize endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct MemorySummarizeInput {
@@ -162,6 +147,11 @@ impl From<VerbosityConfig> for OpenAiVerbosity {
}
}
/// Shared request body for `/responses`-style calls.
///
/// Normal sampling sends every populated field. `/responses/compact` starts from this same shape
/// and then clears the fields that endpoint does not accept, so new request-body fields naturally
/// stay visible to both paths unless compact explicitly opts out.
#[derive(Debug, Serialize, Clone, PartialEq)]
pub struct ResponsesApiRequest {
pub model: String,
@@ -169,12 +159,14 @@ pub struct ResponsesApiRequest {
pub instructions: String,
pub input: Vec<ResponseItem>,
pub tools: Vec<serde_json::Value>,
pub tool_choice: String,
pub parallel_tool_calls: bool,
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -193,7 +185,6 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest {
previous_response_id: None,
input: request.input.clone(),
tools: request.tools.clone(),
tool_choice: request.tool_choice.clone(),
parallel_tool_calls: request.parallel_tool_calls,
reasoning: request.reasoning.clone(),
store: request.store,
@@ -217,12 +208,14 @@ pub struct ResponseCreateWsRequest {
pub previous_response_id: Option<String>,
pub input: Vec<ResponseItem>,
pub tools: Vec<Value>,
pub tool_choice: String,
pub parallel_tool_calls: bool,
pub reasoning: Option<Reasoning>,
pub store: bool,
pub stream: bool,
pub include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
+17 -8
View File
@@ -1,15 +1,17 @@
use crate::auth::SharedAuthProvider;
use crate::common::CompactionInput;
use crate::common::ResponsesApiRequest;
use crate::endpoint::ResponsesOptions;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::requests::responses::build_responses_request_body;
use crate::requests::responses::build_responses_request_headers;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use codex_protocol::models::ResponseItem;
use http::HeaderMap;
use http::Method;
use serde::Deserialize;
use serde_json::to_value;
use std::sync::Arc;
pub struct CompactClient<T: HttpTransport> {
@@ -47,14 +49,21 @@ impl<T: HttpTransport> CompactClient<T> {
Ok(parsed.output)
}
pub async fn compact_input(
pub async fn compact_request(
&self,
input: &CompactionInput<'_>,
extra_headers: HeaderMap,
request: ResponsesApiRequest,
options: ResponsesOptions,
) -> Result<Vec<ResponseItem>, ApiError> {
let body = to_value(input)
.map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
self.compact(body, extra_headers).await
let ResponsesOptions {
conversation_id,
session_source,
extra_headers,
..
} = options;
let body = build_responses_request_body(&request, self.session.provider())?;
let headers =
build_responses_request_headers(extra_headers, conversation_id, session_source);
self.compact(body, headers).await
}
}
+6 -19
View File
@@ -5,10 +5,8 @@ use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::requests::Compression;
use crate::requests::attach_item_ids;
use crate::requests::headers::build_conversation_headers;
use crate::requests::headers::insert_header;
use crate::requests::headers::subagent_header;
use crate::requests::responses::build_responses_request_body;
use crate::requests::responses::build_responses_request_headers;
use crate::sse::spawn_response_stream;
use crate::telemetry::SseTelemetry;
use codex_client::HttpTransport;
@@ -28,7 +26,7 @@ pub struct ResponsesClient<T: HttpTransport> {
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
}
#[derive(Default)]
#[derive(Clone, Default)]
pub struct ResponsesOptions {
pub conversation_id: Option<String>,
pub session_source: Option<SessionSource>,
@@ -79,20 +77,9 @@ impl<T: HttpTransport> ResponsesClient<T> {
turn_state,
} = options;
let mut body = serde_json::to_value(&request)
.map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
if request.store && self.session.provider().is_azure_responses_endpoint() {
attach_item_ids(&mut body, &request.input);
}
let mut headers = extra_headers;
if let Some(ref conv_id) = conversation_id {
insert_header(&mut headers, "x-client-request-id", conv_id);
}
headers.extend(build_conversation_headers(conversation_id));
if let Some(subagent) = subagent_header(&session_source) {
insert_header(&mut headers, "x-openai-subagent", &subagent);
}
let body = build_responses_request_body(&request, self.session.provider())?;
let headers =
build_responses_request_headers(extra_headers, conversation_id, session_source);
self.stream(body, headers, compression, turn_state).await
}
-1
View File
@@ -21,7 +21,6 @@ pub use crate::auth::AuthHeaderTelemetry;
pub use crate::auth::AuthProvider;
pub use crate::auth::SharedAuthProvider;
pub use crate::auth::auth_header_telemetry;
pub use crate::common::CompactionInput;
pub use crate::common::MemorySummarizeInput;
pub use crate::common::MemorySummarizeOutput;
pub use crate::common::OpenAiVerbosity;
-1
View File
@@ -2,4 +2,3 @@ pub(crate) mod headers;
pub(crate) mod responses;
pub use responses::Compression;
pub(crate) use responses::attach_item_ids;
@@ -1,4 +1,12 @@
use crate::common::ResponsesApiRequest;
use crate::error::ApiError;
use crate::provider::Provider;
use crate::requests::headers::build_conversation_headers;
use crate::requests::headers::insert_header;
use crate::requests::headers::subagent_header;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::SessionSource;
use http::HeaderMap;
use serde_json::Value;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -8,6 +16,33 @@ pub enum Compression {
Zstd,
}
pub(crate) fn build_responses_request_body(
request: &ResponsesApiRequest,
provider: &Provider,
) -> Result<Value, ApiError> {
let mut body = serde_json::to_value(request)
.map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
if request.store == Some(true) && provider.is_azure_responses_endpoint() {
attach_item_ids(&mut body, &request.input);
}
Ok(body)
}
pub(crate) fn build_responses_request_headers(
mut extra_headers: HeaderMap,
conversation_id: Option<String>,
session_source: Option<SessionSource>,
) -> HeaderMap {
if let Some(ref conv_id) = conversation_id {
insert_header(&mut extra_headers, "x-client-request-id", conv_id);
}
extra_headers.extend(build_conversation_headers(conversation_id));
if let Some(subagent) = subagent_header(&session_source) {
insert_header(&mut extra_headers, "x-openai-subagent", &subagent);
}
extra_headers
}
pub(crate) fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) {
let Some(input_value) = payload_json.get_mut("input") else {
return;
+8 -10
View File
@@ -43,7 +43,7 @@ struct RecordingState {
}
impl RecordingState {
fn record(&self, req: Request) {
fn record_stream(&self, req: Request) {
let mut guard = self
.stream_requests
.lock()
@@ -78,7 +78,7 @@ impl HttpTransport for RecordingTransport {
}
async fn stream(&self, req: Request) -> Result<StreamResponse, TransportError> {
self.state.record(req);
self.state.record_stream(req);
let stream = futures::stream::iter(Vec::<Result<Bytes, TransportError>>::new());
Ok(StreamResponse {
@@ -325,12 +325,11 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
instructions: "Say hi".into(),
input: Vec::new(),
tools: Vec::new(),
tool_choice: "auto".into(),
parallel_tool_calls: false,
reasoning: None,
store: false,
stream: true,
include: Vec::new(),
store: Some(false),
stream: Some(true),
include: Some(Vec::new()),
service_tier: None,
prompt_cache_key: None,
text: None,
@@ -426,12 +425,11 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
phase: None,
}],
tools: Vec::new(),
tool_choice: "auto".into(),
parallel_tool_calls: false,
reasoning: None,
store: true,
stream: true,
include: Vec::new(),
store: Some(true),
stream: Some(true),
include: Some(Vec::new()),
service_tier: None,
prompt_cache_key: None,
text: None,