1use serde::de::{self, MapAccess, Visitor};
7use serde::ser::SerializeMap;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10use std::collections::HashSet;
11
12use super::{ContentPart, ItemStatus};
13use vtcode_macros::StringNewtype;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, StringNewtype)]
17#[serde(transparent)]
18pub struct OutputItemId(String);
19
20#[derive(Debug, Clone, PartialEq)]
28pub enum OutputItem {
29 Message(MessageItem),
31
32 Reasoning(ReasoningItem),
34
35 FunctionCall(FunctionCallItem),
37
38 FunctionCallOutput(FunctionCallOutputItem),
40
41 Custom(CustomItem),
43}
44
45impl Serialize for OutputItem {
46 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47 where
48 S: Serializer,
49 {
50 match self {
51 Self::Message(item) => {
52 let mut map = serializer.serialize_map(None)?;
53 map.serialize_entry("type", "message")?;
54 map.serialize_entry("id", &item.id)?;
55 map.serialize_entry("status", &item.status)?;
56 map.serialize_entry("role", &item.role)?;
57 map.serialize_entry("content", &item.content)?;
58 map.end()
59 }
60 Self::Reasoning(item) => {
61 let mut map = serializer.serialize_map(None)?;
62 map.serialize_entry("type", "reasoning")?;
63 map.serialize_entry("id", &item.id)?;
64 map.serialize_entry("status", &item.status)?;
65 if let Some(ref summary) = item.summary {
66 map.serialize_entry("summary", summary)?;
67 }
68 if let Some(ref content) = item.content {
69 map.serialize_entry("content", content)?;
70 }
71 if let Some(ref encrypted) = item.encrypted_content {
72 map.serialize_entry("encrypted_content", encrypted)?;
73 }
74 map.end()
75 }
76 Self::FunctionCall(item) => {
77 let mut map = serializer.serialize_map(None)?;
78 map.serialize_entry("type", "function_call")?;
79 map.serialize_entry("id", &item.id)?;
80 map.serialize_entry("status", &item.status)?;
81 map.serialize_entry("name", &item.name)?;
82 map.serialize_entry("arguments", &item.arguments)?;
83 if let Some(ref call_id) = item.call_id {
84 map.serialize_entry("call_id", call_id)?;
85 }
86 map.end()
87 }
88 Self::FunctionCallOutput(item) => {
89 let mut map = serializer.serialize_map(None)?;
90 map.serialize_entry("type", "function_call_output")?;
91 map.serialize_entry("id", &item.id)?;
92 map.serialize_entry("status", &item.status)?;
93 if let Some(ref call_id) = item.call_id {
94 map.serialize_entry("call_id", call_id)?;
95 }
96 map.serialize_entry("output", &item.output)?;
97 map.end()
98 }
99 Self::Custom(item) => {
100 let mut map = serializer.serialize_map(None)?;
102 map.serialize_entry("type", &item.custom_type)?;
103 map.serialize_entry("id", &item.id)?;
104 map.serialize_entry("status", &item.status)?;
105 map.serialize_entry("data", &item.data)?;
106 map.end()
107 }
108 }
109 }
110}
111
112impl<'de> Deserialize<'de> for OutputItem {
113 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114 where
115 D: Deserializer<'de>,
116 {
117 struct OutputItemVisitor;
118
119 impl<'de> Visitor<'de> for OutputItemVisitor {
120 type Value = OutputItem;
121
122 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 formatter.write_str("an output item object with a type field")
124 }
125
126 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
127 where
128 M: MapAccess<'de>,
129 {
130 let mut type_field: Option<String> = None;
131 let mut id: Option<String> = None;
132 let mut status: Option<ItemStatus> = None;
133 let mut role: Option<MessageRole> = None;
134 let mut content: Option<Vec<ContentPart>> = None;
135 let mut summary: Option<String> = None;
136 let mut reasoning_content: Option<String> = None;
137 let mut encrypted_content: Option<String> = None;
138 let mut name: Option<String> = None;
139 let mut arguments: Option<Value> = None;
140 let mut call_id: Option<String> = None;
141 let mut output: Option<String> = None;
142 let mut data: Option<Value> = None;
143 let mut seen_keys = HashSet::new();
144
145 while let Some(key) = map.next_key::<String>()? {
146 if !seen_keys.insert(key.clone()) {
147 return Err(de::Error::custom(format!("duplicate OpenResponses field {key:?}")));
148 }
149 match key.as_str() {
150 "type" => type_field = Some(map.next_value()?),
151 "id" => id = Some(map.next_value()?),
152 "status" => status = Some(map.next_value()?),
153 "role" => role = Some(map.next_value()?),
154 "content" => {
155 let val: Value = map.next_value()?;
157 if let Value::Array(_) = &val {
158 content = Some(serde_json::from_value(val).map_err(de::Error::custom)?);
159 } else if let Value::String(s) = val {
160 reasoning_content = Some(s);
161 }
162 }
163 "summary" => summary = Some(map.next_value()?),
164 "encrypted_content" => encrypted_content = Some(map.next_value()?),
165 "name" => name = Some(map.next_value()?),
166 "arguments" => arguments = Some(map.next_value()?),
167 "call_id" => call_id = Some(map.next_value()?),
168 "output" => output = Some(map.next_value()?),
169 "data" => data = Some(map.next_value()?),
170 _ => {
171 let _: Value = map.next_value()?;
173 }
174 }
175 }
176
177 let type_str = type_field.ok_or_else(|| de::Error::missing_field("type"))?;
178 let id: OutputItemId = id.ok_or_else(|| de::Error::missing_field("id"))?.into();
179 let status = status.unwrap_or(ItemStatus::InProgress);
180
181 match type_str.as_str() {
182 "message" => Ok(OutputItem::Message(MessageItem {
183 id,
184 status,
185 role: role.unwrap_or_default(),
186 content: content.unwrap_or_default(),
187 })),
188 "reasoning" => Ok(OutputItem::Reasoning(ReasoningItem {
189 id,
190 status,
191 summary,
192 content: reasoning_content,
193 encrypted_content,
194 })),
195 "function_call" => Ok(OutputItem::FunctionCall(FunctionCallItem {
196 id,
197 status,
198 name: name.ok_or_else(|| de::Error::missing_field("name"))?,
199 arguments: arguments.unwrap_or(Value::Null),
200 call_id,
201 })),
202 "function_call_output" => Ok(OutputItem::FunctionCallOutput(FunctionCallOutputItem {
203 id,
204 status,
205 call_id,
206 output: output.ok_or_else(|| de::Error::missing_field("output"))?,
207 })),
208 custom_type => Ok(OutputItem::Custom(CustomItem {
210 id,
211 status,
212 custom_type: custom_type.to_string(),
213 data: data.unwrap_or(Value::Null),
214 })),
215 }
216 }
217 }
218
219 deserializer.deserialize_map(OutputItemVisitor)
220 }
221}
222
223impl OutputItem {
224 fn id(&self) -> &str {
226 match self {
227 Self::Message(m) => &m.id,
228 Self::Reasoning(r) => &r.id,
229 Self::FunctionCall(f) => &f.id,
230 Self::FunctionCallOutput(f) => &f.id,
231 Self::Custom(c) => &c.id,
232 }
233 }
234
235 pub fn status(&self) -> ItemStatus {
237 match self {
238 Self::Message(m) => m.status,
239 Self::Reasoning(r) => r.status,
240 Self::FunctionCall(f) => f.status,
241 Self::FunctionCallOutput(f) => f.status,
242 Self::Custom(c) => c.status,
243 }
244 }
245
246 fn type_name(&self) -> &str {
248 match self {
249 Self::Message(_) => "message",
250 Self::Reasoning(_) => "reasoning",
251 Self::FunctionCall(_) => "function_call",
252 Self::FunctionCallOutput(_) => "function_call_output",
253 Self::Custom(c) => &c.custom_type,
254 }
255 }
256
257 pub fn message(id: impl Into<OutputItemId>, role: MessageRole, content: Vec<ContentPart>) -> Self {
259 Self::Message(MessageItem {
260 id: id.into(),
261 status: ItemStatus::InProgress,
262 role,
263 content,
264 })
265 }
266
267 pub(crate) fn completed_message(id: impl Into<OutputItemId>, role: MessageRole, content: Vec<ContentPart>) -> Self {
269 Self::Message(MessageItem {
270 id: id.into(),
271 status: ItemStatus::Completed,
272 role,
273 content,
274 })
275 }
276
277 pub(crate) fn reasoning(id: impl Into<OutputItemId>) -> Self {
279 Self::Reasoning(ReasoningItem {
280 id: id.into(),
281 status: ItemStatus::InProgress,
282 summary: None,
283 content: None,
284 encrypted_content: None,
285 })
286 }
287
288 pub fn function_call(id: impl Into<OutputItemId>, name: impl Into<String>, arguments: Value) -> Self {
290 Self::FunctionCall(FunctionCallItem {
291 id: id.into(),
292 status: ItemStatus::InProgress,
293 name: name.into(),
294 arguments,
295 call_id: None,
296 })
297 }
298
299 pub fn function_call_output(
304 id: impl Into<OutputItemId>,
305 call_id: Option<String>,
306 output: impl Into<String>,
307 ) -> Self {
308 Self::FunctionCallOutput(FunctionCallOutputItem {
309 id: id.into(),
310 status: ItemStatus::InProgress,
311 call_id,
312 output: output.into(),
313 })
314 }
315
316 pub fn completed_function_call_output(
320 id: impl Into<OutputItemId>,
321 call_id: Option<String>,
322 output: impl Into<String>,
323 ) -> Self {
324 Self::FunctionCallOutput(FunctionCallOutputItem {
325 id: id.into(),
326 status: ItemStatus::Completed,
327 call_id,
328 output: output.into(),
329 })
330 }
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
335pub struct MessageItem {
336 pub(crate) id: OutputItemId,
338
339 pub status: ItemStatus,
341
342 pub(crate) role: MessageRole,
344
345 pub(crate) content: Vec<ContentPart>,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
351#[serde(rename_all = "lowercase")]
352pub enum MessageRole {
353 User,
355 #[default]
357 Assistant,
358 System,
360 Developer,
362}
363
364impl std::fmt::Display for MessageRole {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 match self {
367 Self::User => write!(f, "user"),
368 Self::Assistant => write!(f, "assistant"),
369 Self::System => write!(f, "system"),
370 Self::Developer => write!(f, "developer"),
371 }
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct ReasoningItem {
378 pub(crate) id: OutputItemId,
380
381 pub(crate) status: ItemStatus,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub(crate) summary: Option<String>,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub(crate) content: Option<String>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub(crate) encrypted_content: Option<String>,
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
399pub struct FunctionCallItem {
400 pub(crate) id: OutputItemId,
402
403 pub(crate) status: ItemStatus,
405
406 pub name: String,
408
409 pub arguments: Value,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub(crate) call_id: Option<String>,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419pub struct FunctionCallOutputItem {
420 pub(crate) id: OutputItemId,
422
423 pub(crate) status: ItemStatus,
425
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub(crate) call_id: Option<String>,
429
430 pub(crate) output: String,
432}
433
434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
438pub struct CustomItem {
439 pub(crate) id: OutputItemId,
441
442 pub(crate) status: ItemStatus,
444
445 pub(crate) custom_type: String,
447
448 pub(crate) data: Value,
450}
451
452impl CustomItem {
453 fn vtcode(id: impl Into<OutputItemId>, name: &str, data: Value) -> Self {
455 Self {
456 id: id.into(),
457 status: ItemStatus::InProgress,
458 custom_type: format!("vtcode:{name}"),
459 data,
460 }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_output_item_id() {
470 let item = OutputItem::message("msg_1", MessageRole::Assistant, vec![]);
471 assert_eq!(item.id(), "msg_1");
472 assert_eq!(item.type_name(), "message");
473 }
474
475 #[test]
476 fn test_function_call_serialization() {
477 let item = OutputItem::function_call("fc_1", "exec_command", serde_json::json!({"path": "/etc/passwd"}));
478 let json = serde_json::to_string(&item).unwrap();
479 assert!(json.contains("\"type\":\"function_call\""));
480 assert!(json.contains("\"name\":\"exec_command\""));
481 }
482
483 #[test]
484 fn test_custom_item_vtcode() {
485 let item =
486 CustomItem::vtcode("custom_1", "file_change", serde_json::json!({"path": "test.rs", "kind": "update"}));
487 assert_eq!(item.custom_type, "vtcode:file_change");
488 }
489
490 #[test]
491 fn test_custom_item_serializes_with_custom_type_as_type() {
492 let item =
493 OutputItem::Custom(CustomItem::vtcode("custom_1", "file_change", serde_json::json!({"path": "test.rs"})));
494 let json = serde_json::to_string(&item).unwrap();
495 assert!(json.contains("\"type\":\"vtcode:file_change\""));
497 assert!(!json.contains("\"type\":\"custom\""));
498 assert!(!json.contains("\"custom_type\""));
499 }
500
501 #[test]
502 fn test_custom_item_roundtrip() {
503 let original = OutputItem::Custom(CustomItem::vtcode(
504 "custom_1",
505 "file_change",
506 serde_json::json!({"path": "test.rs", "kind": "update"}),
507 ));
508 let json = serde_json::to_string(&original).unwrap();
509 let parsed: OutputItem = serde_json::from_str(&json).unwrap();
510 assert_eq!(original, parsed);
511
512 if let OutputItem::Custom(c) = &parsed {
513 assert_eq!(c.custom_type, "vtcode:file_change");
514 assert_eq!(c.data["path"], "test.rs");
515 } else {
516 panic!("Expected Custom variant");
517 }
518 }
519
520 #[test]
521 fn test_deserialize_unknown_type_as_custom() {
522 let json = r#"{"type":"vendor:special_item","id":"item_1","status":"completed","data":{"key":"value"}}"#;
523 let item: OutputItem = serde_json::from_str(json).unwrap();
524 if let OutputItem::Custom(c) = item {
525 assert_eq!(c.custom_type, "vendor:special_item");
526 assert_eq!(c.id.as_str(), "item_1");
527 assert_eq!(c.status, ItemStatus::Completed);
528 assert_eq!(c.data["key"], "value");
529 } else {
530 panic!("Expected Custom variant for unknown type");
531 }
532 }
533
534 #[test]
535 fn test_duplicate_output_item_keys_are_rejected() {
536 let json = r#"{
537 "type":"message",
538 "id":"item_1",
539 "id":"item_2",
540 "role":"assistant",
541 "content":[]
542 }"#;
543 let error = serde_json::from_str::<OutputItem>(json).expect_err("duplicate keys must be rejected");
544 assert!(error.to_string().contains("duplicate OpenResponses field"));
545 }
546
547 #[test]
548 fn test_completed_message_has_completed_status() {
549 let item = OutputItem::completed_message("msg_1", MessageRole::Assistant, vec![]);
550 assert_eq!(item.status(), ItemStatus::Completed);
551 if let OutputItem::Message(m) = item {
552 assert_eq!(m.status, ItemStatus::Completed);
553 } else {
554 panic!("Expected Message variant");
555 }
556 }
557
558 #[test]
559 fn test_completed_function_call_output_has_completed_status() {
560 let item = OutputItem::completed_function_call_output("fco_1", Some("fc_1".to_string()), "result");
561 assert_eq!(item.status(), ItemStatus::Completed);
562 if let OutputItem::FunctionCallOutput(f) = item {
563 assert_eq!(f.status, ItemStatus::Completed);
564 assert_eq!(f.call_id, Some("fc_1".to_string()));
565 assert_eq!(f.output, "result");
566 } else {
567 panic!("Expected FunctionCallOutput variant");
568 }
569 }
570}