diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 749899498067..7c2c5f55649d 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2289,6 +2289,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2339,6 +2345,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2387,6 +2399,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2430,6 +2448,13 @@ "null" ] }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2467,6 +2492,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2515,6 +2546,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2552,6 +2589,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2586,6 +2629,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "input": { "type": "string" }, @@ -2630,6 +2679,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2676,6 +2731,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2722,6 +2783,12 @@ } ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2755,7 +2822,6 @@ { "properties": { "id": { - "description": "Existing provider ID retained on serialized history for compatibility.", "type": [ "string", "null" @@ -2804,6 +2870,12 @@ "encrypted_content": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -2863,6 +2935,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5b0c59c42555..b0e5972e7679 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14791,6 +14791,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -14841,6 +14847,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -14889,6 +14901,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -14932,6 +14950,13 @@ "null" ] }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -14969,6 +14994,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15017,6 +15048,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15054,6 +15091,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15088,6 +15131,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "input": { "type": "string" }, @@ -15132,6 +15181,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15178,6 +15233,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15224,6 +15285,12 @@ } ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15257,7 +15324,6 @@ { "properties": { "id": { - "description": "Existing provider ID retained on serialized history for compatibility.", "type": [ "string", "null" @@ -15306,6 +15372,12 @@ "encrypted_content": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -15365,6 +15437,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 45cfbd95c21f..c2f0d7e5d4ae 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -11236,6 +11236,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11286,6 +11292,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11334,6 +11346,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11377,6 +11395,13 @@ "null" ] }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11414,6 +11439,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11462,6 +11493,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11499,6 +11536,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11533,6 +11576,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "input": { "type": "string" }, @@ -11577,6 +11626,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11623,6 +11678,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11669,6 +11730,12 @@ } ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11702,7 +11769,6 @@ { "properties": { "id": { - "description": "Existing provider ID retained on serialized history for compatibility.", "type": [ "string", "null" @@ -11751,6 +11817,12 @@ "encrypted_content": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -11810,6 +11882,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index f77a68dc1d71..22385f816f87 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -377,6 +377,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -427,6 +433,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -475,6 +487,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -518,6 +536,13 @@ "null" ] }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -555,6 +580,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -603,6 +634,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -640,6 +677,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -674,6 +717,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "input": { "type": "string" }, @@ -718,6 +767,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -764,6 +819,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -810,6 +871,12 @@ } ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -843,7 +910,6 @@ { "properties": { "id": { - "description": "Existing provider ID retained on serialized history for compatibility.", "type": [ "string", "null" @@ -892,6 +958,12 @@ "encrypted_content": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -951,6 +1023,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index e09dd2c6fc75..528076c6ae28 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -448,6 +448,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -498,6 +504,12 @@ }, "type": "array" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -546,6 +558,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -589,6 +607,13 @@ "null" ] }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -626,6 +651,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -674,6 +705,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -711,6 +748,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -745,6 +788,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "input": { "type": "string" }, @@ -789,6 +838,12 @@ "call_id": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -835,6 +890,12 @@ "execution": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -881,6 +942,12 @@ } ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -914,7 +981,6 @@ { "properties": { "id": { - "description": "Existing provider ID retained on serialized history for compatibility.", "type": [ "string", "null" @@ -963,6 +1029,12 @@ "encrypted_content": { "type": "string" }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { @@ -1022,6 +1094,12 @@ "null" ] }, + "id": { + "type": [ + "string", + "null" + ] + }, "metadata": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts index f0cd1c8ee661..fc399b925f59 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts @@ -12,12 +12,12 @@ import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm import type { ResponseItemMetadata } from "./ResponseItemMetadata"; import type { WebSearchAction } from "./WebSearchAction"; -export type ResponseItem = { "type": "message", role: string, content: Array, phase?: MessagePhase, metadata?: ResponseItemMetadata, } | { "type": "agent_message", author: string, recipient: string, content: Array, metadata?: ResponseItemMetadata, } | { "type": "reasoning", summary: Array, content?: Array, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call", +export type ResponseItem = { "type": "message", id?: string, role: string, content: Array, phase?: MessagePhase, metadata?: ResponseItemMetadata, } | { "type": "agent_message", id?: string, author: string, recipient: string, content: Array, metadata?: ResponseItemMetadata, } | { "type": "reasoning", id?: string, summary: Array, content?: Array, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call", /** - * Set when using the Responses API. + * Legacy id field retained for compatibility with older payloads. */ -call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", +id?: string, /** - * Existing provider ID retained on serialized history for compatibility. + * Set when using the Responses API. */ -id?: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" }; +call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", id?: string, encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", id?: string, encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" }; diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs index c8730235fd5f..fd3fd17dbca7 100644 --- a/codex-rs/codex-api/src/endpoint/compact.rs +++ b/codex-rs/codex-api/src/endpoint/compact.rs @@ -9,7 +9,6 @@ use codex_protocol::models::ResponseItem; use http::HeaderMap; use http::Method; use serde::Deserialize; -use serde_json::to_value; use std::sync::Arc; use std::sync::OnceLock; use std::time::Duration; @@ -76,7 +75,7 @@ impl CompactClient { request_timeout: Duration, turn_state: Option<&OnceLock>, ) -> Result, ApiError> { - let body = to_value(input) + let body = serde_json::to_value(input) .map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?; self.compact(body, extra_headers, request_timeout, turn_state) .await diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index ad79adf33dff..804f0027ff84 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -5,7 +5,6 @@ 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; @@ -82,16 +81,8 @@ impl ResponsesClient { 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 body = 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 { diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs index 1c231d6bef85..864f7ec9b0a8 100644 --- a/codex-rs/codex-api/src/endpoint/search.rs +++ b/codex-rs/codex-api/src/endpoint/search.rs @@ -149,7 +149,7 @@ mod tests { model: "gpt-test".to_string(), reasoning: None, input: Some(SearchInput::Items(vec![ResponseItem::Message { - id: None, + id: Some("msg_search".to_string()), role: "user".to_string(), content: vec![ ContentItem::InputText { @@ -228,6 +228,7 @@ mod tests { "model": "gpt-test", "input": [{ "type": "message", + "id": "msg_search", "role": "user", "content": [ {"type": "input_text", "text": "find this"}, diff --git a/codex-rs/codex-api/src/requests/mod.rs b/codex-rs/codex-api/src/requests/mod.rs index 1c357b2a613b..abe57a886dce 100644 --- a/codex-rs/codex-api/src/requests/mod.rs +++ b/codex-rs/codex-api/src/requests/mod.rs @@ -2,4 +2,3 @@ pub(crate) mod headers; pub(crate) mod responses; pub use responses::Compression; -pub(crate) use responses::attach_item_ids; diff --git a/codex-rs/codex-api/src/requests/responses.rs b/codex-rs/codex-api/src/requests/responses.rs index 836f525c49b7..9a16ceb1c44c 100644 --- a/codex-rs/codex-api/src/requests/responses.rs +++ b/codex-rs/codex-api/src/requests/responses.rs @@ -1,37 +1,6 @@ -use codex_protocol::models::ResponseItem; -use serde_json::Value; - #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum Compression { #[default] None, Zstd, } - -pub(crate) fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) { - let Some(input_value) = payload_json.get_mut("input") else { - return; - }; - let Value::Array(items) = input_value else { - return; - }; - - for (value, item) in items.iter_mut().zip(original_items.iter()) { - if let ResponseItem::Reasoning { id: Some(id), .. } - | ResponseItem::Message { id: Some(id), .. } - | ResponseItem::WebSearchCall { id: Some(id), .. } - | ResponseItem::FunctionCall { id: Some(id), .. } - | ResponseItem::ToolSearchCall { id: Some(id), .. } - | ResponseItem::LocalShellCall { id: Some(id), .. } - | ResponseItem::CustomToolCall { id: Some(id), .. } = item - { - if id.is_empty() { - continue; - } - - if let Some(obj) = value.as_object_mut() { - obj.insert("id".to_string(), Value::String(id.clone())); - } - } - } -} diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index d8489ed7487f..488f6dc67436 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -301,7 +301,7 @@ async fn responses_client_uses_responses_path() -> Result<()> { } #[tokio::test] -async fn responses_client_stream_request_preserves_exact_json_body() -> Result<()> { +async fn responses_client_stream_request_preserves_item_ids() -> Result<()> { let state = RecordingState::default(); let transport = RecordingTransport::new(state.clone()); let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); @@ -327,7 +327,7 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<( text: None, client_metadata: None, }; - let expected = serde_json::to_vec(&request)?; + let expected = serde_json::to_value(&request)?; let _stream = client .stream_request(request, ResponsesOptions::default()) @@ -338,7 +338,10 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<( let prepared = requests[0] .prepare_body_for_send() .expect("body should prepare"); - assert_eq!(prepared.body.as_deref(), Some(expected.as_slice())); + let body: serde_json::Value = + serde_json::from_slice(prepared.body.as_deref().expect("body should be JSON"))?; + assert_eq!(body, expected); + assert_eq!(body["input"][0]["id"], "msg_1"); assert_eq!( prepared.headers.get(http::header::CONTENT_TYPE), Some(&HeaderValue::from_static("application/json")) @@ -502,7 +505,7 @@ async fn streaming_client_does_not_retry_auth_build_error() -> Result<()> { } #[tokio::test] -async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { +async fn azure_store_sends_ids_and_headers() -> Result<()> { let state = RecordingState::default(); let transport = RecordingTransport::new(state.clone()); let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth)); diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 9944c2994cd2..19c6f1a56539 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -523,6 +523,9 @@ "in_app_browser": { "type": "boolean" }, + "item_ids": { + "type": "boolean" + }, "js_repl": { "type": "boolean" }, @@ -4684,6 +4687,9 @@ "in_app_browser": { "type": "boolean" }, + "item_ids": { + "type": "boolean" + }, "js_repl": { "type": "boolean" }, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 4ea8c0ff1fb8..fe290c16bf2b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -178,6 +178,7 @@ struct ModelClientState { enable_request_compression: bool, include_timing_metrics: bool, beta_features_header: Option, + item_ids_enabled: bool, include_attestation: bool, attestation_provider: Option>, disable_websockets: AtomicBool, @@ -378,6 +379,7 @@ impl ModelClient { enable_request_compression: bool, include_timing_metrics: bool, beta_features_header: Option, + item_ids_enabled: bool, attestation_provider: Option>, ) -> Self { let model_provider = create_model_provider(provider_info, auth_manager); @@ -398,6 +400,7 @@ impl ModelClient { enable_request_compression, include_timing_metrics, beta_features_header, + item_ids_enabled, include_attestation, attestation_provider, disable_websockets: AtomicBool::new(false), @@ -520,7 +523,7 @@ impl ModelClient { let ResponsesApiRequest { model, instructions, - input, + mut input, tools, parallel_tool_calls, reasoning, @@ -529,6 +532,7 @@ impl ModelClient { text, .. } = request; + self.prepare_response_items_for_request(&mut input, /*store*/ false); let payload = ApiCompactionInput { model: &model, input: &input, @@ -830,6 +834,16 @@ impl ModelClient { Ok(request) } + fn prepare_response_items_for_request(&self, input: &mut [ResponseItem], store: bool) { + if self.state.item_ids_enabled || store { + return; + } + + for item in input { + item.set_id(/*new_id*/ None); + } + } + /// Returns whether the Responses-over-WebSocket transport is active for this session. /// /// WebSocket use is controlled by provider capability and session-scoped fallback state. @@ -1299,7 +1313,7 @@ impl ModelClientSession { ) .await; - let request = self.client.build_responses_request( + let mut request = self.client.build_responses_request( &client_setup.api_provider, prompt, model_info, @@ -1308,6 +1322,9 @@ impl ModelClientSession { service_tier.clone(), responses_metadata, )?; + let store = request.store; + self.client + .prepare_response_items_for_request(&mut request.input, store); let inference_trace_attempt = inference_trace.start_attempt(); inference_trace_attempt.add_request_headers(&mut options.extra_headers); inference_trace_attempt.record_started(&request); @@ -1475,6 +1492,10 @@ impl ModelClientSession { inference_trace.start_attempt() }; stamp_ws_stream_request_start_ms(&mut ws_request); + let ResponsesWsRequest::ResponseCreate(ws_payload) = &mut ws_request; + let store = ws_payload.store; + self.client + .prepare_response_items_for_request(&mut ws_payload.input, store); if previous_response_id_from_untraced_warmup { // The transport can reuse an untraced warmup response id and omit the // already-sent input, but rollout replay needs the logical model-visible diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 06074a5d2024..1d0c4c48c29a 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -76,6 +76,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ) } @@ -566,6 +567,7 @@ fn model_client_with_counting_attestation( /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, Some(Arc::new(CountingAttestationProvider { calls: attestation_calls.clone(), })), diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 6c3a939024a6..3b79789c02ba 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -231,10 +231,7 @@ async fn run_remote_compact_task_inner_impl( ) .await?; let mut input = prompt_input.clone(); - input.push(ResponseItem::CompactionTrigger { - id: None, - metadata: None, - }); + input.push(ResponseItem::CompactionTrigger { metadata: None }); let prompt = Prompt { input, tools: tool_router.model_visible_specs(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index cf191235919d..7c725fa151d6 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2634,17 +2634,47 @@ impl Session { turn_context: &TurnContext, items: &'a [ResponseItem], ) -> Cow<'a, [ResponseItem]> { - if !turn_context + let mut items = Cow::Borrowed(items); + if turn_context .config .features .enabled(Feature::ResizeAllImages) { - return Cow::Borrowed(items); + prepare_response_items(items.to_mut()); + } + if turn_context.config.features.enabled(Feature::ItemIds) { + Self::assign_missing_response_item_ids(&mut items); } + items + } - let mut prepared_items = items.to_vec(); - prepare_response_items(&mut prepared_items); - Cow::Owned(prepared_items) + fn assign_missing_response_item_ids(items: &mut Cow<'_, [ResponseItem]>) { + if items.iter().all(|item| item.id().is_some()) { + return; + } + for item in items.to_mut() { + if item.id().is_some() { + continue; + } + let prefix = match item { + ResponseItem::Message { .. } => "msg", + ResponseItem::Reasoning { .. } => "rs", + ResponseItem::LocalShellCall { .. } => "lsh", + ResponseItem::FunctionCall { .. } => "fc", + ResponseItem::ToolSearchCall { .. } => "tsc", + ResponseItem::FunctionCallOutput { .. } => "fco", + ResponseItem::CustomToolCall { .. } => "ctc", + ResponseItem::CustomToolCallOutput { .. } => "ctco", + ResponseItem::ToolSearchOutput { .. } => "tso", + ResponseItem::WebSearchCall { .. } => "ws", + ResponseItem::ImageGenerationCall { .. } => "ig", + ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => "cmp", + ResponseItem::AgentMessage { .. } + | ResponseItem::CompactionTrigger { .. } + | ResponseItem::Other => continue, + }; + item.set_id(Some(format!("{prefix}_{}", Uuid::now_v7()))); + } } pub(crate) fn response_item_from_user_input( diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index c4bc064a473f..e3686baffc01 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1027,6 +1027,7 @@ impl Session { config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Self::build_model_client_beta_features_header(config.as_ref()), + /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), attestation_provider, ) .with_prompt_cache_key_override( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 724c35f2e815..1a3745a4e80d 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -42,6 +42,7 @@ use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::ActivePermissionProfile; +use codex_protocol::models::AgentMessageInputContent; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::FunctionCallOutputBody; @@ -197,6 +198,27 @@ fn user_message(text: &str) -> ResponseItem { } } +#[test] +fn assign_missing_response_item_ids_skips_agent_messages() { + let mut items = Cow::Owned(vec![ + ResponseItem::AgentMessage { + id: None, + author: "worker".to_string(), + recipient: "root".to_string(), + content: vec![AgentMessageInputContent::InputText { + text: "done".to_string(), + }], + metadata: None, + }, + user_message("hello"), + ]); + + Session::assign_missing_response_item_ids(&mut items); + + assert_eq!(items[0].id(), None); + assert!(items[1].id().is_some_and(|id| id.starts_with("msg_"))); +} + fn assistant_message(text: &str) -> ResponseItem { ResponseItem::Message { id: None, @@ -437,6 +459,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ) .new_session() @@ -1660,6 +1683,7 @@ async fn resize_all_images_prepares_failures_before_history_insertion() { Vec::new(), |config| { let _ = config.features.enable(Feature::ResizeAllImages); + let _ = config.features.enable(Feature::ItemIds); }, ) .await; @@ -1689,8 +1713,18 @@ async fn resize_all_images_prepares_failures_before_history_insertion() { .record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&item)) .await; + let history = session.state.lock().await.clone_history(); + let id = history.raw_items()[0] + .id() + .expect("history item should have an ID") + .to_string(); + let uuid = id + .strip_prefix("fco_") + .expect("function call output ID should have the Responses API prefix"); + let parsed_id = Uuid::parse_str(uuid).expect("history item should have a UUID ID"); + assert_eq!(parsed_id.get_version(), Some(uuid::Version::SortRand)); let expected = vec![ResponseItem::FunctionCallOutput { - id: None, + id: Some(id), call_id: "call-1".to_string(), output: FunctionCallOutputPayload { body: FunctionCallOutputBody::ContentItems(vec![ @@ -1709,10 +1743,7 @@ async fn resize_all_images_prepares_failures_before_history_insertion() { }, metadata: None, }]; - assert_eq!( - session.state.lock().await.clone_history().raw_items(), - expected.as_slice() - ); + assert_eq!(history.raw_items(), expected.as_slice()); } #[tokio::test] @@ -5039,6 +5070,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Session::build_model_client_beta_features_header(config.as_ref()), + /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, ), code_mode_service: crate::tools::code_mode::CodeModeService::new(), @@ -7082,6 +7114,7 @@ where config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Session::build_model_client_beta_features_header(config.as_ref()), + /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, ), code_mode_service: crate::tools::code_mode::CodeModeService::new(), diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index b5bc2b0d4440..cf9b9a939fcd 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -127,6 +127,7 @@ async fn responses_stream_includes_subagent_header_on_review() { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); @@ -258,6 +259,7 @@ async fn responses_stream_includes_subagent_header_on_other() { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); @@ -375,6 +377,7 @@ async fn responses_respects_model_info_overrides_from_config() { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 54e24d49cbe8..0c7844d55ee4 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -134,6 +134,22 @@ fn message_input_text_contains(request: &ResponsesRequest, role: &str, needle: & .any(|text| text.contains(needle)) } +fn response_message_item_id(request: &ResponsesRequest, role: &str, text: &str) -> String { + request + .inputs_of_type("message") + .into_iter() + .find(|item| { + item.get("role").and_then(serde_json::Value::as_str) == Some(role) + && message_input_texts(item).contains(&text) + }) + .and_then(|item| { + item.get("id") + .and_then(serde_json::Value::as_str) + .map(str::to_string) + }) + .unwrap_or_else(|| panic!("missing item ID for {role} message {text:?}")) +} + fn assert_codex_client_metadata( request_body: &serde_json::Value, installation_id: &str, @@ -216,9 +232,74 @@ async fn non_openai_responses_requests_omit_item_turn_metadata() { item.get("metadata").is_none(), "input item should omit metadata: {item}" ); + assert!( + item.get("id").is_none(), + "input item should omit generated IDs: {item}" + ); } } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> anyhow::Result<()> { + let server = MockServer::start().await; + let response_mock = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg_server", "first reply"), + ev_completed("resp-1"), + ]), + sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), + ], + ) + .await; + let mut builder = test_codex().with_config(|config| { + let _ = config.features.enable(Feature::ItemIds); + }); + let initial = builder.build(&server).await?; + let home = Arc::clone(&initial.home); + let rollout_path = initial + .session_configured + .rollout_path + .clone() + .expect("rollout path"); + + initial.submit_turn("before resume").await?; + initial.codex.submit(Op::Shutdown).await?; + wait_for_event(&initial.codex, |event| { + matches!(event, EventMsg::ShutdownComplete) + }) + .await; + + builder = builder.with_config(|config| { + let _ = config.features.enable(Feature::ItemIds); + }); + let resumed = builder.resume(&server, home, rollout_path).await?; + resumed.submit_turn("after resume").await?; + + let requests = response_mock.requests(); + assert_eq!(requests.len(), 2); + let user_id = response_message_item_id(&requests[0], "user", "before resume"); + let user_uuid = user_id + .strip_prefix("msg_") + .expect("message ID should have the Responses API prefix"); + assert_eq!( + Uuid::parse_str(user_uuid)?.get_version(), + Some(uuid::Version::SortRand) + ); + assert_eq!( + response_message_item_id(&requests[1], "user", "before resume"), + user_id + ); + assert_eq!( + response_message_item_id(&requests[1], "assistant", "first reply"), + "msg_server" + ); + + Ok(()) +} + /// Writes an `auth.json` into the provided `codex_home` with the specified parameters. /// Returns the fake JWT string written to `tokens.id_token`. #[expect(clippy::unwrap_used)] @@ -1022,6 +1103,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, ); let responses_metadata = test_turn_responses_metadata(&client, thread_id); @@ -2514,6 +2596,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, + /*item_ids_enabled*/ false, /*attestation_provider*/ None, ); let responses_metadata = test_turn_responses_metadata(&client, thread_id); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index cee54e24b310..1ccbd302d8a4 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -157,7 +157,8 @@ async fn responses_websocket_streams_request() { let harness = websocket_harness(&server).await; let mut client_session = harness.client.new_session(); - let prompt = prompt_with_input(vec![message_item("hello")]); + let mut prompt = prompt_with_input(vec![message_item("hello")]); + prompt.input[0].set_id(Some("msg_existing".to_string())); stream_until_complete(&mut client_session, &harness, &prompt).await; @@ -169,6 +170,7 @@ async fn responses_websocket_streams_request() { assert_eq!(body["model"].as_str(), Some(MODEL)); assert_eq!(body["stream"], serde_json::Value::Bool(true)); assert_eq!(body["input"].as_array().map(Vec::len), Some(1)); + assert_eq!(body["input"][0].get("id"), None); let handshake = server.single_handshake(); assert_eq!( handshake.header(OPENAI_BETA_HEADER), @@ -2189,6 +2191,7 @@ async fn websocket_harness_with_provider_options( /*enable_request_compression*/ false, runtime_metrics_enabled, /*beta_features_header*/ None, + /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, ); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 22363eabea3a..ef7a305bd762 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -649,8 +649,8 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { ); assert_eq!( image_generation_calls[0]["id"].as_str(), - Some("ig_123"), - "expected the original image generation call id to be preserved" + None, + "expected the image generation call id to be omitted" ); assert_eq!( image_generation_calls[0]["result"].as_str(), @@ -766,8 +766,8 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima ); assert_eq!( image_generation_calls[0]["id"].as_str(), - Some("ig_123"), - "second request should preserve the original generated image call id" + None, + "second request should omit the generated image call id" ); assert_eq!( image_generation_calls[0]["result"].as_str(), diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs index 3065ed178b10..df3faf123c3a 100644 --- a/codex-rs/ext/web-search/src/history.rs +++ b/codex-rs/ext/web-search/src/history.rs @@ -29,7 +29,9 @@ pub(crate) fn recent_input(items: &[ResponseItem]) -> Option { fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { match item { ResponseItem::Message { role, .. } if role == ASSISTANT_ROLE => { - messages.push(item.clone()); + let mut message = item.clone(); + message.set_id(/*new_id*/ None); + messages.push(message); } ResponseItem::AgentMessage { author, @@ -50,7 +52,7 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { } } ResponseItem::Message { - id, + id: _, role, content, phase, @@ -65,7 +67,7 @@ fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { .collect::>(); if !content.is_empty() { messages.push(ResponseItem::Message { - id: id.clone(), + id: None, role: role.clone(), content, phase: phase.clone(), @@ -108,11 +110,15 @@ mod tests { #[test] fn keeps_current_user_and_previous_visible_turn() { + let mut previous_user = message(USER_ROLE, "previous user"); + previous_user.set_id(Some("msg_previous_user".to_string())); + let mut previous_assistant = message(ASSISTANT_ROLE, "previous assistant"); + previous_assistant.set_id(Some("msg_previous_assistant".to_string())); let items = vec![ message("system", "system"), message(USER_ROLE, "old user"), message(ASSISTANT_ROLE, "old assistant"), - message(USER_ROLE, "previous user"), + previous_user, ResponseItem::FunctionCall { id: None, name: "tool".to_string(), @@ -121,7 +127,7 @@ mod tests { call_id: "call-1".to_string(), metadata: None, }, - message(ASSISTANT_ROLE, "previous assistant"), + previous_assistant, message("developer", "developer"), message(USER_ROLE, "current user"), message(ASSISTANT_ROLE, "current commentary"), diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index b887237a18cb..1267239cecd9 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -189,6 +189,8 @@ pub enum Feature { ImageGenExt, /// Resize all inline data-URL images before recording them in history. ResizeAllImages, + /// Generate Responses API item IDs for client-created history items. + ItemIds, /// Allow prompting and installing missing MCP dependencies. SkillMcpDependencyInstall, /// Removed compatibility flag for deleted skill env var dependency prompting. @@ -1113,6 +1115,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::ItemIds, + key: "item_ids", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::SkillMcpDependencyInstall, key: "skill_mcp_dependency_install", diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 8dae6ca61704..74d8445b7566 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -235,6 +235,7 @@ impl MemoryStartupContext { config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), /*beta_features_header*/ None, + config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, ); diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 3c0753cb43ab..b0d373cfb053 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -918,9 +918,8 @@ pub struct ResponseItemMetadata { #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { Message { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, role: String, content: Vec, @@ -935,9 +934,8 @@ pub enum ResponseItem { metadata: Option, }, AgentMessage { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, author: String, recipient: String, @@ -947,9 +945,8 @@ pub enum ResponseItem { metadata: Option, }, Reasoning { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, summary: Vec, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] @@ -962,9 +959,8 @@ pub enum ResponseItem { }, LocalShellCall { /// Legacy id field retained for compatibility with older payloads. - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, /// Set when using the Responses API. call_id: Option, @@ -975,9 +971,8 @@ pub enum ResponseItem { metadata: Option, }, FunctionCall { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, name: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -993,9 +988,8 @@ pub enum ResponseItem { metadata: Option, }, ToolSearchCall { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1014,9 +1008,8 @@ pub enum ResponseItem { // - an array of structured content items (`content_items`) // We keep this behavior centralized in `FunctionCallOutputPayload`. FunctionCallOutput { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, call_id: String, #[ts(as = "FunctionCallOutputBody")] @@ -1027,9 +1020,8 @@ pub enum ResponseItem { metadata: Option, }, CustomToolCall { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1046,9 +1038,8 @@ pub enum ResponseItem { // `function_call_output.output` so freeform tools can return either plain // text or structured content items. CustomToolCallOutput { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1062,9 +1053,8 @@ pub enum ResponseItem { metadata: Option, }, ToolSearchOutput { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, call_id: Option, status: String, @@ -1084,9 +1074,8 @@ pub enum ResponseItem { // "action": {"type":"search","query":"weather: San Francisco, CA"} // } WebSearchCall { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1108,7 +1097,6 @@ pub enum ResponseItem { // "result":"..." // } ImageGenerationCall { - /// Existing provider ID retained on serialized history for compatibility. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] id: Option, @@ -1123,28 +1111,24 @@ pub enum ResponseItem { }, #[serde(alias = "compaction_summary")] Compaction { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, encrypted_content: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option, }, + // Compaction triggers are request controls, and the Responses API does not + // accept an `id` field for them. CompactionTrigger { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] - id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] metadata: Option, }, ContextCompaction { - #[serde(default, skip_serializing)] - #[ts(skip)] - #[schemars(skip)] + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1179,14 +1163,13 @@ impl ResponseItem { | Self::Reasoning { id, .. } | Self::ImageGenerationCall { id, .. } | Self::Compaction { id, .. } - | Self::CompactionTrigger { id, .. } | Self::ContextCompaction { id, .. } => id.as_deref().filter(|id| !id.is_empty()), - Self::Other => None, + Self::CompactionTrigger { .. } | Self::Other => None, } } - /// Sets the Responses API item ID for variants that carry one. - pub fn set_id(&mut self, new_id: String) { + /// Sets or clears the Responses API item ID for variants that carry one. + pub fn set_id(&mut self, new_id: Option) { match self { Self::Message { id, .. } | Self::AgentMessage { id, .. } @@ -1201,9 +1184,8 @@ impl ResponseItem { | Self::Reasoning { id, .. } | Self::ImageGenerationCall { id, .. } | Self::Compaction { id, .. } - | Self::CompactionTrigger { id, .. } - | Self::ContextCompaction { id, .. } => *id = Some(new_id), - Self::Other => {} + | Self::ContextCompaction { id, .. } => *id = new_id, + Self::CompactionTrigger { .. } | Self::Other => {} } } @@ -2150,9 +2132,13 @@ mod tests { let mut item = response_item_with_metadata(/*metadata*/ None); assert_eq!(item.id(), None); - item.set_id("msg_test".to_string()); + item.set_id(Some("msg_test".to_string())); assert_eq!(item.id(), Some("msg_test")); + + item.set_id(/*new_id*/ None); + + assert_eq!(item.id(), None); } fn response_item_with_metadata(metadata: Option) -> ResponseItem { @@ -3022,10 +3008,7 @@ mod tests { #[test] fn serializes_compaction_trigger_without_payload() -> Result<()> { - let item = ResponseItem::CompactionTrigger { - id: None, - metadata: None, - }; + let item = ResponseItem::CompactionTrigger { metadata: None }; assert_eq!( serde_json::to_value(item)?, @@ -3038,10 +3021,7 @@ mod tests { #[test] fn serializes_stamped_compaction_trigger_metadata() -> Result<()> { - let mut item = ResponseItem::CompactionTrigger { - id: None, - metadata: None, - }; + let mut item = ResponseItem::CompactionTrigger { metadata: None }; item.stamp_turn_id_if_missing("turn-1"); assert_eq!( @@ -3062,13 +3042,7 @@ mod tests { let item: ResponseItem = serde_json::from_str(json)?; - assert_eq!( - item, - ResponseItem::CompactionTrigger { - id: None, - metadata: None, - } - ); + assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None }); Ok(()) } @@ -3109,7 +3083,6 @@ mod tests { queries: Some(vec!["weather seattle".into(), "seattle weather now".into()]), }), Some("completed".into()), - true, ), ( r#"{ @@ -3125,7 +3098,6 @@ mod tests { url: Some("https://example.com".into()), }), Some("open".into()), - true, ), ( r#"{ @@ -3143,7 +3115,6 @@ mod tests { pattern: Some("installation".into()), }), Some("in_progress".into()), - true, ), ( r#"{ @@ -3154,12 +3125,10 @@ mod tests { Some("ws_partial".into()), None, Some("in_progress".into()), - false, ), ]; - for (json_literal, expected_id, expected_action, expected_status, expect_roundtrip) in cases - { + for (json_literal, expected_id, expected_action, expected_status) in cases { let parsed: ResponseItem = serde_json::from_str(json_literal)?; let expected = ResponseItem::WebSearchCall { id: expected_id.clone(), @@ -3170,10 +3139,7 @@ mod tests { assert_eq!(parsed, expected); let serialized = serde_json::to_value(&parsed)?; - let mut expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?; - if !expect_roundtrip && let Some(obj) = expected_serialized.as_object_mut() { - obj.remove("id"); - } + let expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?; assert_eq!(serialized, expected_serialized); } diff --git a/codex-rs/rollout-trace/src/inference.rs b/codex-rs/rollout-trace/src/inference.rs index 7329fd3cf219..7afdcf584727 100644 --- a/codex-rs/rollout-trace/src/inference.rs +++ b/codex-rs/rollout-trace/src/inference.rs @@ -515,6 +515,7 @@ mod tests { traced, json!({ "type": "reasoning", + "id": "rs-1", "summary": [{"type": "summary_text", "text": "summary"}], "content": [{"type": "text", "text": "raw reasoning"}], "encrypted_content": "encoded",