mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Preserve content item kinds in message metadata (#40174)
## What changed - Add `ContentItemKind` as an open-ended string classification and carry an optional list of kinds in `InternalChatMessageMetadataPassthrough`. - Keep unknown classification values intact during round trips, while treating a malformed `content_item_kinds` value as absent so the response item can still be loaded. ## Testing - Cover round trips for future classification values and deserialization of malformed metadata. GitOrigin-RevId: 5398cae5bb294a9f71ddd192e297177fab54a576
This commit is contained in:
@@ -10,6 +10,7 @@ use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::ser::Serializer;
|
||||
use serde_with::serde_as;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::local_media::audio_mime_for_path;
|
||||
@@ -32,6 +33,7 @@ use crate::mcp::CallToolResult;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
mod executed_tool_calls;
|
||||
mod item_metadata;
|
||||
|
||||
pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES;
|
||||
pub use crate::local_media::snapshot_local_user_input;
|
||||
@@ -42,6 +44,7 @@ pub use executed_tool_calls::ExecutedToolCallTruncation;
|
||||
pub use executed_tool_calls::bound_executed_tool_calls_for_prompt;
|
||||
pub use executed_tool_calls::bound_executed_tool_calls_for_prompt_prioritizing_recent;
|
||||
pub use executed_tool_calls::executed_tool_call_metadata_bytes;
|
||||
pub use item_metadata::ContentItemKind;
|
||||
|
||||
/// Controls the per-command sandbox override requested by a shell-like tool call.
|
||||
#[derive(
|
||||
@@ -904,6 +907,7 @@ pub enum MessagePhase {
|
||||
///
|
||||
/// Responses API strongly types this payload. Do not modify it without first getting API
|
||||
/// approval and making the corresponding Responses API change.
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
pub struct InternalChatMessageMetadataPassthrough {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -914,6 +918,12 @@ pub struct InternalChatMessageMetadataPassthrough {
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub create_time: Option<serde_json::Number>,
|
||||
/// Harness-owned classifications aligned with the item's content entries.
|
||||
#[serde_as(deserialize_as = "serde_with::DefaultOnError")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
#[ts(skip)]
|
||||
pub content_item_kinds: Option<Vec<ContentItemKind>>,
|
||||
/// Warehouse-only Responses metadata, not part of the public app-server protocol.
|
||||
#[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(skip)]
|
||||
|
||||
11
codex-rs/protocol/src/models/item_metadata.rs
Normal file
11
codex-rs/protocol/src/models/item_metadata.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Harness-owned classification for one position in an item's content array.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(transparent)]
|
||||
pub struct ContentItemKind(pub String);
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "item_metadata_tests.rs"]
|
||||
mod tests;
|
||||
79
codex-rs/protocol/src/models/item_metadata_tests.rs
Normal file
79
codex-rs/protocol/src/models/item_metadata_tests.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use super::ContentItemKind;
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::InternalChatMessageMetadataPassthrough;
|
||||
use crate::models::ResponseItem;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn content_kinds_round_trip_without_restricting_future_values() -> Result<()> {
|
||||
let item = response_item(InternalChatMessageMetadataPassthrough {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
content_item_kinds: Some(vec![ContentItemKind("future_content_kind".to_string())]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let serialized = serde_json::to_value(&item)?;
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "hello"}],
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn-1",
|
||||
"content_item_kinds": ["future_content_kind"],
|
||||
},
|
||||
})
|
||||
);
|
||||
assert_eq!(serde_json::from_value::<ResponseItem>(serialized)?, item);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_content_kinds_do_not_prevent_loading_response_item() -> Result<()> {
|
||||
for malformed_kinds in [
|
||||
json!("user_text"),
|
||||
json!({"kind": "user_text"}),
|
||||
json!(["user_text", 123]),
|
||||
] {
|
||||
let item = serde_json::from_value::<ResponseItem>(response_item_json(json!({
|
||||
"turn_id": "turn-1",
|
||||
"content_item_kinds": malformed_kinds,
|
||||
})))?;
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
response_item(InternalChatMessageMetadataPassthrough {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn response_item(metadata: InternalChatMessageMetadataPassthrough) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "hello".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: Some(metadata),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_item_json(metadata: Value) -> Value {
|
||||
json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": "hello"}],
|
||||
"internal_chat_message_metadata_passthrough": metadata,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user