Skip to main content

gproxy_protocol/protocol/openai/generate_content/
response_items.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize, de};
4use serde_json::Value;
5
6use super::super::common::*;
7
8mod actions;
9mod content;
10mod message;
11mod typed;
12
13pub use actions::*;
14pub use content::*;
15pub use message::*;
16pub use typed::*;
17
18#[derive(Debug, Clone, PartialEq, Serialize)]
19#[serde(untagged)]
20#[non_exhaustive]
21pub enum ResponseItem {
22    Message(ResponseMessageItem),
23    Typed(TypedResponseItem),
24    Unknown(UnknownResponseItem),
25}
26
27impl<'de> Deserialize<'de> for ResponseItem {
28    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
29    where
30        D: serde::Deserializer<'de>,
31    {
32        let value = Value::deserialize(deserializer)?;
33        let type_name = value.get("type").and_then(Value::as_str);
34
35        let Some(type_name) = type_name else {
36            if let Ok(message) = serde_json::from_value::<ResponseMessageItem>(value.clone()) {
37                return Ok(Self::Message(message));
38            }
39
40            if let Some(item_reference) = item_reference_without_type(&value) {
41                return Ok(Self::Typed(item_reference));
42            }
43
44            return serde_json::from_value(value)
45                .map(Self::Unknown)
46                .map_err(de::Error::custom);
47        };
48
49        let item_type =
50            serde_json::from_value::<ResponseItemType>(Value::String(type_name.to_owned()))
51                .map_err(de::Error::custom)?;
52
53        match item_type {
54            ResponseItemType::Known(ResponseItemTypeKnown::Message) => {
55                serde_json::from_value(value)
56                    .map(Self::Message)
57                    .map_err(de::Error::custom)
58            }
59            ResponseItemType::Known(_) => serde_json::from_value(value)
60                .map(Self::Typed)
61                .map_err(de::Error::custom),
62            ResponseItemType::Unknown(_) => serde_json::from_value(value)
63                .map(Self::Unknown)
64                .map_err(de::Error::custom),
65        }
66    }
67}
68
69fn item_reference_without_type(value: &Value) -> Option<TypedResponseItem> {
70    let object = value.as_object()?;
71    let id = object.get("id")?.as_str()?.to_owned();
72    let mut extra = Extra::new();
73
74    for (key, value) in object {
75        if key != "id" {
76            extra.insert(key.clone(), value.clone());
77        }
78    }
79
80    Some(TypedResponseItem::ItemReference { id, extra })
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize)]
84#[serde(transparent)]
85#[non_exhaustive]
86pub struct ResponseOutputItem(pub ResponseItem);
87
88impl ResponseOutputItem {
89    pub fn try_new(item: ResponseItem) -> Result<Self, &'static str> {
90        validate_response_output_item(&item)?;
91        Ok(Self(item))
92    }
93
94    pub fn new(item: ResponseItem) -> Self {
95        Self::try_new(item).expect("valid Responses output item")
96    }
97
98    pub fn as_inner(&self) -> &ResponseItem {
99        &self.0
100    }
101
102    pub fn into_inner(self) -> ResponseItem {
103        self.0
104    }
105}
106
107impl<'de> Deserialize<'de> for ResponseOutputItem {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: serde::Deserializer<'de>,
111    {
112        let item = ResponseItem::deserialize(deserializer)?;
113        validate_response_output_item(&item).map_err(de::Error::custom)?;
114        Ok(Self(item))
115    }
116}
117
118fn validate_response_output_item(item: &ResponseItem) -> Result<(), &'static str> {
119    let ResponseItem::Typed(typed) = item else {
120        return Ok(());
121    };
122
123    match typed {
124        TypedResponseItem::ComputerCallOutput { id, status, .. } => {
125            require_some(id, "computer_call_output.id")?;
126            require_some(status, "computer_call_output.status")?;
127        }
128        TypedResponseItem::FunctionCallOutput { id, status, .. } => {
129            require_some(id, "function_call_output.id")?;
130            require_some(status, "function_call_output.status")?;
131        }
132        TypedResponseItem::ToolSearchCall {
133            id,
134            call_id,
135            execution,
136            status,
137            ..
138        } => {
139            require_some(id, "tool_search_call.id")?;
140            require_some(call_id, "tool_search_call.call_id")?;
141            require_some(execution, "tool_search_call.execution")?;
142            require_some(status, "tool_search_call.status")?;
143        }
144        TypedResponseItem::ToolSearchOutput {
145            id,
146            call_id,
147            execution,
148            status,
149            ..
150        } => {
151            require_some(id, "tool_search_output.id")?;
152            require_some(call_id, "tool_search_output.call_id")?;
153            require_some(execution, "tool_search_output.execution")?;
154            require_some(status, "tool_search_output.status")?;
155        }
156        TypedResponseItem::AdditionalTools { id, .. } => {
157            require_some(id, "additional_tools.id")?;
158        }
159        TypedResponseItem::ShellCall {
160            id,
161            environment,
162            status,
163            ..
164        } => {
165            require_some(id, "shell_call.id")?;
166            require_some(environment, "shell_call.environment")?;
167            require_some(status, "shell_call.status")?;
168        }
169        TypedResponseItem::ShellCallOutput {
170            id,
171            max_output_length,
172            status,
173            ..
174        } => {
175            require_some(id, "shell_call_output.id")?;
176            require_some(max_output_length, "shell_call_output.max_output_length")?;
177            require_some(status, "shell_call_output.status")?;
178        }
179        _ => {}
180    }
181
182    Ok(())
183}
184
185fn require_some<T>(value: &Option<T>, field: &'static str) -> Result<(), &'static str> {
186    value.as_ref().map(|_| ()).ok_or(field)
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
190#[non_exhaustive]
191pub struct UnknownResponseItem {
192    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
193    pub type_: Option<ResponseItemType>,
194    #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
195    pub extra: Extra,
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    /// Regression: `ResponseItem`/`ResponseMessageItem` must serialize flat,
203    /// matching their hand-written flat `Deserialize` implementations.
204    #[test]
205    fn input_message_serializes_flat() {
206        let flat = serde_json::json!({"type": "message", "role": "user", "content": "hi"});
207        let item: ResponseItem = serde_json::from_value(flat.clone()).unwrap();
208        let back = serde_json::to_value(&item).unwrap();
209        assert!(
210            back.get("Message").is_none() && back.get("EasyInput").is_none(),
211            "must not be externally tagged: {back}"
212        );
213        assert_eq!(back["role"], "user", "{back}");
214        assert_eq!(back, flat);
215    }
216
217    /// Regression (#146): Codex CLI replays assistant turns without `id` / `status`,
218    /// carrying `output_text` parts. They must decode as easy-input `OutputParts`
219    /// and round-trip unchanged instead of failing the whole request.
220    #[test]
221    fn replayed_assistant_history_decodes_as_easy_input_output_parts() {
222        let replayed = serde_json::json!({
223            "type": "message",
224            "role": "assistant",
225            "content": [{"type": "output_text", "text": "hello"}]
226        });
227        let item: ResponseItem = serde_json::from_value(replayed.clone()).unwrap();
228        let ResponseItem::Message(ResponseMessageItem::EasyInput(message)) = &item else {
229            panic!("expected EasyInput, got: {item:?}");
230        };
231        assert!(
232            matches!(&message.content, ResponseEasyInputContent::OutputParts(parts) if parts.len() == 1),
233            "expected OutputParts: {:?}",
234            message.content
235        );
236        assert_eq!(serde_json::to_value(&item).unwrap(), replayed);
237    }
238
239    /// Guard: ordinary input parts must keep matching `Parts`, not be swallowed
240    /// by the `OutputParts` arm added after it.
241    #[test]
242    fn easy_input_text_parts_still_decode_as_input_parts() {
243        let body = serde_json::json!({
244            "type": "message",
245            "role": "assistant",
246            "content": [{"type": "input_text", "text": "hi"}]
247        });
248        let item: ResponseItem = serde_json::from_value(body).unwrap();
249        let ResponseItem::Message(ResponseMessageItem::EasyInput(message)) = &item else {
250            panic!("expected EasyInput, got: {item:?}");
251        };
252        assert!(
253            matches!(&message.content, ResponseEasyInputContent::Parts(parts) if parts.len() == 1),
254            "expected Parts: {:?}",
255            message.content
256        );
257    }
258}