Skip to main content

openai_compat/types/responses/
input.rs

1//! The `input` polymorphism for Responses requests, mirroring
2//! `openai-python/src/openai/types/responses/response_input_item_param.py`
3//! (v1 subset: message / function_call / function_call_output / reasoning /
4//! item_reference — exotic item types round-trip through `Other`).
5
6use serde::{Deserialize, Serialize};
7
8/// One part of a multimodal message content list.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum InputContent {
12    InputText {
13        text: String,
14    },
15    InputImage {
16        /// `"low" | "high" | "auto" | "original"`.
17        detail: String,
18        #[serde(default, skip_serializing_if = "Option::is_none")]
19        file_id: Option<String>,
20        #[serde(default, skip_serializing_if = "Option::is_none")]
21        image_url: Option<String>,
22    },
23    InputFile {
24        #[serde(default, skip_serializing_if = "Option::is_none")]
25        file_id: Option<String>,
26        #[serde(default, skip_serializing_if = "Option::is_none")]
27        file_url: Option<String>,
28        #[serde(default, skip_serializing_if = "Option::is_none")]
29        filename: Option<String>,
30        #[serde(default, skip_serializing_if = "Option::is_none")]
31        file_data: Option<String>,
32    },
33}
34
35impl InputContent {
36    pub fn text(text: impl Into<String>) -> Self {
37        Self::InputText { text: text.into() }
38    }
39
40    pub fn image_url(url: impl Into<String>) -> Self {
41        Self::InputImage {
42            detail: "auto".to_string(),
43            file_id: None,
44            image_url: Some(url.into()),
45        }
46    }
47}
48
49/// `EasyInputMessage.content`: a plain string or a list of content parts.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(untagged)]
52pub enum EasyContent {
53    Text(String),
54    Parts(Vec<InputContent>),
55}
56
57impl From<&str> for EasyContent {
58    fn from(text: &str) -> Self {
59        Self::Text(text.to_string())
60    }
61}
62
63impl From<String> for EasyContent {
64    fn from(text: String) -> Self {
65        Self::Text(text)
66    }
67}
68
69impl From<Vec<InputContent>> for EasyContent {
70    fn from(parts: Vec<InputContent>) -> Self {
71        Self::Parts(parts)
72    }
73}
74
75/// The typed input item variants this crate models directly. Anything else
76/// (computer_use, shell, apply_patch, skills, MCP, compaction, ...) falls
77/// back to [`InputItem::Other`].
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(tag = "type", rename_all = "snake_case")]
80pub enum KnownInputItem {
81    Message {
82        role: String,
83        content: EasyContent,
84    },
85    FunctionCall {
86        call_id: String,
87        name: String,
88        /// JSON-encoded arguments string (may be invalid JSON — validate
89        /// before use).
90        arguments: String,
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        id: Option<String>,
93    },
94    FunctionCallOutput {
95        call_id: String,
96        /// Text output of the function call. Complex (image/file) outputs
97        /// are not modeled in v1 — use [`InputItem::Other`] for those.
98        output: String,
99    },
100    Reasoning {
101        id: String,
102        summary: Vec<serde_json::Value>,
103        #[serde(default, skip_serializing_if = "Option::is_none")]
104        content: Option<Vec<serde_json::Value>>,
105        #[serde(default, skip_serializing_if = "Option::is_none")]
106        encrypted_content: Option<String>,
107    },
108    ItemReference {
109        id: String,
110    },
111}
112
113/// One item in the `input` list, mirroring `ResponseInputItemParam`.
114///
115/// Unrecognized `type` values deserialize into [`InputItem::Other`] and
116/// round-trip without data loss (object key order is normalized through
117/// `serde_json::Value`, but no field is dropped), so exotic item types
118/// (computer_use, shell, apply_patch, skills, MCP, compaction, ...) survive
119/// a decode/encode cycle.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(untagged)]
122pub enum InputItem {
123    Known(KnownInputItem),
124    Other(serde_json::Value),
125}
126
127impl InputItem {
128    /// A user message with plain text or multimodal content.
129    pub fn user(content: impl Into<EasyContent>) -> Self {
130        Self::Known(KnownInputItem::Message {
131            role: "user".to_string(),
132            content: content.into(),
133        })
134    }
135
136    /// A system/developer/assistant message with plain text or multimodal
137    /// content.
138    pub fn message(role: impl Into<String>, content: impl Into<EasyContent>) -> Self {
139        Self::Known(KnownInputItem::Message {
140            role: role.into(),
141            content: content.into(),
142        })
143    }
144
145    /// A function call previously made by the model (for replaying history).
146    pub fn function_call(
147        call_id: impl Into<String>,
148        name: impl Into<String>,
149        arguments: impl Into<String>,
150    ) -> Self {
151        Self::Known(KnownInputItem::FunctionCall {
152            call_id: call_id.into(),
153            name: name.into(),
154            arguments: arguments.into(),
155            id: None,
156        })
157    }
158
159    /// The result of executing a function call, sent back to the model.
160    pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
161        Self::Known(KnownInputItem::FunctionCallOutput {
162            call_id: call_id.into(),
163            output: output.into(),
164        })
165    }
166
167    /// A reference to a previously created item, by id.
168    pub fn item_reference(id: impl Into<String>) -> Self {
169        Self::Known(KnownInputItem::ItemReference { id: id.into() })
170    }
171}
172
173/// The `input` request field: a plain string, or a list of input items.
174#[derive(Debug, Clone, Serialize, Deserialize, Default)]
175#[serde(untagged)]
176pub enum Input {
177    #[default]
178    Empty,
179    Text(String),
180    Items(Vec<InputItem>),
181}
182
183impl From<&str> for Input {
184    fn from(text: &str) -> Self {
185        Self::Text(text.to_string())
186    }
187}
188
189impl From<String> for Input {
190    fn from(text: String) -> Self {
191        Self::Text(text)
192    }
193}
194
195impl From<Vec<InputItem>> for Input {
196    fn from(items: Vec<InputItem>) -> Self {
197        Self::Items(items)
198    }
199}
200
201impl Input {
202    /// True for [`Input::Empty`] — used to omit `input` entirely from a
203    /// continuation request driven purely by `previous_response_id`, rather
204    /// than sending a JSON `null` the API rejects.
205    pub fn is_empty(&self) -> bool {
206        matches!(self, Self::Empty)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn string_input_serializes_as_string() {
216        let input: Input = "hi".into();
217        assert_eq!(serde_json::to_value(&input).unwrap(), "hi");
218    }
219
220    #[test]
221    fn user_message_item_serializes() {
222        let item = InputItem::user("hi");
223        assert_eq!(
224            serde_json::to_value(&item).unwrap(),
225            serde_json::json!({"type": "message", "role": "user", "content": "hi"})
226        );
227    }
228
229    #[test]
230    fn function_call_output_roundtrip() {
231        let item = InputItem::function_call_output("call_1", "72F and sunny");
232        let json = serde_json::to_value(&item).unwrap();
233        assert_eq!(
234            json,
235            serde_json::json!({
236                "type": "function_call_output",
237                "call_id": "call_1",
238                "output": "72F and sunny"
239            })
240        );
241        let roundtrip: InputItem = serde_json::from_value(json).unwrap();
242        assert!(matches!(
243            roundtrip,
244            InputItem::Known(KnownInputItem::FunctionCallOutput { .. })
245        ));
246    }
247
248    #[test]
249    fn unknown_input_item_preserved_as_other() {
250        let json = serde_json::json!({
251            "type": "computer_call",
252            "call_id": "call_1",
253            "action": {"type": "click", "x": 1, "y": 2}
254        });
255        let item: InputItem = serde_json::from_value(json.clone()).unwrap();
256        assert!(matches!(item, InputItem::Other(_)));
257        assert_eq!(serde_json::to_value(&item).unwrap(), json);
258    }
259
260    #[test]
261    fn multimodal_content_serializes() {
262        let item = InputItem::user(vec![
263            InputContent::text("What is this?"),
264            InputContent::image_url("https://example.com/cat.png"),
265        ]);
266        let json = serde_json::to_value(&item).unwrap();
267        assert_eq!(json["content"][0]["type"], "input_text");
268        assert_eq!(json["content"][1]["type"], "input_image");
269    }
270}