use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContent {
InputText {
text: String,
},
InputImage {
detail: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
image_url: Option<String>,
},
InputFile {
#[serde(default, skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
file_data: Option<String>,
},
}
impl InputContent {
pub fn text(text: impl Into<String>) -> Self {
Self::InputText { text: text.into() }
}
pub fn image_url(url: impl Into<String>) -> Self {
Self::InputImage {
detail: "auto".to_string(),
file_id: None,
image_url: Some(url.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EasyContent {
Text(String),
Parts(Vec<InputContent>),
}
impl From<&str> for EasyContent {
fn from(text: &str) -> Self {
Self::Text(text.to_string())
}
}
impl From<String> for EasyContent {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<Vec<InputContent>> for EasyContent {
fn from(parts: Vec<InputContent>) -> Self {
Self::Parts(parts)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum KnownInputItem {
Message {
role: String,
content: EasyContent,
},
FunctionCall {
call_id: String,
name: String,
arguments: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
},
FunctionCallOutput {
call_id: String,
output: String,
},
Reasoning {
id: String,
summary: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
content: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
encrypted_content: Option<String>,
},
ItemReference {
id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InputItem {
Known(KnownInputItem),
Other(serde_json::Value),
}
impl InputItem {
pub fn user(content: impl Into<EasyContent>) -> Self {
Self::Known(KnownInputItem::Message {
role: "user".to_string(),
content: content.into(),
})
}
pub fn message(role: impl Into<String>, content: impl Into<EasyContent>) -> Self {
Self::Known(KnownInputItem::Message {
role: role.into(),
content: content.into(),
})
}
pub fn function_call(
call_id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self::Known(KnownInputItem::FunctionCall {
call_id: call_id.into(),
name: name.into(),
arguments: arguments.into(),
id: None,
})
}
pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
Self::Known(KnownInputItem::FunctionCallOutput {
call_id: call_id.into(),
output: output.into(),
})
}
pub fn item_reference(id: impl Into<String>) -> Self {
Self::Known(KnownInputItem::ItemReference { id: id.into() })
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(untagged)]
pub enum Input {
#[default]
Empty,
Text(String),
Items(Vec<InputItem>),
}
impl From<&str> for Input {
fn from(text: &str) -> Self {
Self::Text(text.to_string())
}
}
impl From<String> for Input {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<Vec<InputItem>> for Input {
fn from(items: Vec<InputItem>) -> Self {
Self::Items(items)
}
}
impl Input {
pub fn is_empty(&self) -> bool {
matches!(self, Self::Empty)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_input_serializes_as_string() {
let input: Input = "hi".into();
assert_eq!(serde_json::to_value(&input).unwrap(), "hi");
}
#[test]
fn user_message_item_serializes() {
let item = InputItem::user("hi");
assert_eq!(
serde_json::to_value(&item).unwrap(),
serde_json::json!({"type": "message", "role": "user", "content": "hi"})
);
}
#[test]
fn function_call_output_roundtrip() {
let item = InputItem::function_call_output("call_1", "72F and sunny");
let json = serde_json::to_value(&item).unwrap();
assert_eq!(
json,
serde_json::json!({
"type": "function_call_output",
"call_id": "call_1",
"output": "72F and sunny"
})
);
let roundtrip: InputItem = serde_json::from_value(json).unwrap();
assert!(matches!(
roundtrip,
InputItem::Known(KnownInputItem::FunctionCallOutput { .. })
));
}
#[test]
fn unknown_input_item_preserved_as_other() {
let json = serde_json::json!({
"type": "computer_call",
"call_id": "call_1",
"action": {"type": "click", "x": 1, "y": 2}
});
let item: InputItem = serde_json::from_value(json.clone()).unwrap();
assert!(matches!(item, InputItem::Other(_)));
assert_eq!(serde_json::to_value(&item).unwrap(), json);
}
#[test]
fn multimodal_content_serializes() {
let item = InputItem::user(vec![
InputContent::text("What is this?"),
InputContent::image_url("https://example.com/cat.png"),
]);
let json = serde_json::to_value(&item).unwrap();
assert_eq!(json["content"][0]["type"], "input_text");
assert_eq!(json["content"][1]["type"], "input_image");
}
}