mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
495da45643
## Why Responses HTTP requests were converted from `ResponsesApiRequest` into a full `serde_json::Value`. `EndpointSession` then deep-cloned that value for each retry, and the transport serialized and compressed it again before every send. Large histories make those copies expensive. Retry attempts should reuse the same immutable request bytes. ## What - Serialize standard Responses requests directly into a ref-counted `EncodedJsonBody`. - Preserve the Azure path that attaches item IDs before encoding. - Prepare JSON, compression, and derived content headers once before the retry loop. - Clone the prepared request per attempt so body clones only bump the `Bytes` reference count. - Keep auth inside the retry loop. Signing auth sees the exact final headers and body bytes that the transport sends. - Preserve request-body TRACE output. With TRACE plus compression, retain the original JSON bytes for logging; normal requests keep only the final wire bytes. - Leave non-Responses endpoint bodies on the existing `Value` path. ## Performance A temporary release-mode measurement used a 10 MiB JSON body and 10 retry preparations: - old `Value` clone + serialize path: 30 ms total - prepared shared-byte path: less than 1 ms total That is about 3 ms avoided per retry for this payload on the test machine. Each retry also stops allocating another request-sized JSON tree and serialized buffer. Without TRACE, compressed requests retain only the final compressed wire bytes. ## Validation - `just test -p codex-client` — 28 passed - `just test -p codex-api` — 125 passed - `just fix -p codex-client` - `just fix -p codex-api`
174 lines
5.3 KiB
Rust
174 lines
5.3 KiB
Rust
use crate::auth::SharedAuthProvider;
|
|
use crate::common::ResponseStream;
|
|
use crate::common::ResponsesApiRequest;
|
|
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_session_headers;
|
|
use crate::requests::headers::insert_header;
|
|
use crate::requests::headers::subagent_header;
|
|
use crate::sse::spawn_response_stream;
|
|
use crate::telemetry::SseTelemetry;
|
|
use codex_client::EncodedJsonBody;
|
|
use codex_client::HttpTransport;
|
|
use codex_client::RequestCompression;
|
|
use codex_client::RequestTelemetry;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use http::HeaderMap;
|
|
use http::HeaderValue;
|
|
use http::Method;
|
|
use serde_json::Value;
|
|
use std::sync::Arc;
|
|
use std::sync::OnceLock;
|
|
use tracing::instrument;
|
|
|
|
pub struct ResponsesClient<T: HttpTransport> {
|
|
session: EndpointSession<T>,
|
|
sse_telemetry: Option<Arc<dyn SseTelemetry>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct ResponsesOptions {
|
|
pub session_id: Option<String>,
|
|
pub thread_id: Option<String>,
|
|
pub session_source: Option<SessionSource>,
|
|
pub extra_headers: HeaderMap,
|
|
pub compression: Compression,
|
|
pub turn_state: Option<Arc<OnceLock<String>>>,
|
|
}
|
|
|
|
impl<T: HttpTransport> ResponsesClient<T> {
|
|
pub fn new(transport: T, provider: Provider, auth: SharedAuthProvider) -> Self {
|
|
Self {
|
|
session: EndpointSession::new(transport, provider, auth),
|
|
sse_telemetry: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_telemetry(
|
|
self,
|
|
request: Option<Arc<dyn RequestTelemetry>>,
|
|
sse: Option<Arc<dyn SseTelemetry>>,
|
|
) -> Self {
|
|
Self {
|
|
session: self.session.with_request_telemetry(request),
|
|
sse_telemetry: sse,
|
|
}
|
|
}
|
|
|
|
#[instrument(
|
|
name = "responses.stream_request",
|
|
level = "info",
|
|
skip_all,
|
|
fields(
|
|
transport = "responses_http",
|
|
http.method = "POST",
|
|
api.path = "responses"
|
|
)
|
|
)]
|
|
pub async fn stream_request(
|
|
&self,
|
|
request: ResponsesApiRequest,
|
|
options: ResponsesOptions,
|
|
) -> Result<ResponseStream, ApiError> {
|
|
let ResponsesOptions {
|
|
session_id,
|
|
thread_id,
|
|
session_source,
|
|
extra_headers,
|
|
compression,
|
|
turn_state,
|
|
} = options;
|
|
|
|
let body = if request.store && self.session.provider().is_azure_responses_endpoint() {
|
|
let mut body = serde_json::to_value(&request).map_err(|e| {
|
|
ApiError::Stream(format!("failed to encode responses request: {e}"))
|
|
})?;
|
|
attach_item_ids(&mut body, &request.input);
|
|
EncodedJsonBody::encode(&body)
|
|
} else {
|
|
EncodedJsonBody::encode(&request)
|
|
}
|
|
.map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
|
|
|
|
let mut headers = extra_headers;
|
|
if let Some(ref thread_id) = thread_id {
|
|
insert_header(&mut headers, "x-client-request-id", thread_id);
|
|
}
|
|
headers.extend(build_session_headers(session_id, thread_id));
|
|
if let Some(subagent) = subagent_header(&session_source) {
|
|
insert_header(&mut headers, "x-openai-subagent", &subagent);
|
|
}
|
|
|
|
self.stream_encoded(body, headers, compression, turn_state)
|
|
.await
|
|
}
|
|
|
|
fn path() -> &'static str {
|
|
"responses"
|
|
}
|
|
|
|
#[instrument(
|
|
name = "responses.stream",
|
|
level = "info",
|
|
skip_all,
|
|
fields(
|
|
transport = "responses_http",
|
|
http.method = "POST",
|
|
api.path = "responses",
|
|
turn.has_state = turn_state.is_some()
|
|
)
|
|
)]
|
|
pub async fn stream(
|
|
&self,
|
|
body: Value,
|
|
extra_headers: HeaderMap,
|
|
compression: Compression,
|
|
turn_state: Option<Arc<OnceLock<String>>>,
|
|
) -> Result<ResponseStream, ApiError> {
|
|
let body = EncodedJsonBody::encode(&body)
|
|
.map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
|
|
self.stream_encoded(body, extra_headers, compression, turn_state)
|
|
.await
|
|
}
|
|
|
|
async fn stream_encoded(
|
|
&self,
|
|
body: EncodedJsonBody,
|
|
extra_headers: HeaderMap,
|
|
compression: Compression,
|
|
turn_state: Option<Arc<OnceLock<String>>>,
|
|
) -> Result<ResponseStream, ApiError> {
|
|
let request_compression = match compression {
|
|
Compression::None => RequestCompression::None,
|
|
Compression::Zstd => RequestCompression::Zstd,
|
|
};
|
|
|
|
let stream_response = self
|
|
.session
|
|
.stream_encoded_json_with(
|
|
Method::POST,
|
|
Self::path(),
|
|
extra_headers,
|
|
Some(body),
|
|
|req| {
|
|
req.headers.insert(
|
|
http::header::ACCEPT,
|
|
HeaderValue::from_static("text/event-stream"),
|
|
);
|
|
req.compression = request_compression;
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
Ok(spawn_response_stream(
|
|
stream_response,
|
|
self.session.provider().stream_idle_timeout,
|
|
self.sse_telemetry.clone(),
|
|
turn_state,
|
|
))
|
|
}
|
|
}
|