gproxy_protocol/protocol/openai/generate_content/
response_items.rs1use 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 #[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 #[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 #[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}