Skip to content

Commit 451c83e

Browse files
committed
Add multimodal input conversion helpers
1 parent f1994d5 commit 451c83e

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

src/conversion/multimodal_input.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use serde_json::{json, Map, Value};
2+
3+
pub fn chat_content_from_response_content(content: &Value) -> Option<Value> {
4+
match content {
5+
Value::String(text) => Some(Value::String(text.clone())),
6+
Value::Array(parts) => {
7+
let chat_parts = parts
8+
.iter()
9+
.filter_map(response_content_part_to_chat_part)
10+
.collect::<Vec<_>>();
11+
(!chat_parts.is_empty()).then(|| Value::Array(chat_parts))
12+
}
13+
Value::Object(_) => response_content_part_to_chat_part(content).map(|part| Value::Array(vec![part])),
14+
_ => None,
15+
}
16+
}
17+
18+
pub fn response_content_part_to_chat_part(part: &Value) -> Option<Value> {
19+
match part {
20+
Value::String(text) => Some(json!({"type":"text","text":text})),
21+
Value::Object(obj) => match obj.get("type").and_then(Value::as_str).unwrap_or("") {
22+
"input_text" | "output_text" | "text" => {
23+
Some(json!({"type":"text","text":text_from_part(part)}))
24+
}
25+
"input_image" | "image" => image_to_chat_content_part(part),
26+
"input_file" => file_to_chat_content_part(part),
27+
"input_audio" => audio_to_chat_content_part(part),
28+
_ => None,
29+
},
30+
_ => None,
31+
}
32+
}
33+
34+
pub fn image_to_chat_content_part(part: &Value) -> Option<Value> {
35+
image_url_object_from_part(part).map(|image_url| json!({"type":"image_url","image_url":image_url}))
36+
}
37+
38+
pub fn file_to_chat_content_part(part: &Value) -> Option<Value> {
39+
responses_input_file_to_chat_file(part).map(|file| json!({"type":"file","file":file}))
40+
}
41+
42+
pub fn audio_to_chat_content_part(part: &Value) -> Option<Value> {
43+
responses_input_audio_to_chat_audio(part)
44+
.map(|input_audio| json!({"type":"input_audio","input_audio":input_audio}))
45+
}
46+
47+
fn text_from_part(part: &Value) -> String {
48+
part.get("text")
49+
.or_else(|| part.get("content"))
50+
.and_then(Value::as_str)
51+
.unwrap_or_default()
52+
.to_string()
53+
}
54+
55+
fn image_url_object_from_part(part: &Value) -> Option<Value> {
56+
if let Some(image_url) = part.get("image_url") {
57+
return Some(if image_url.is_object() {
58+
image_url.clone()
59+
} else {
60+
json!({"url": image_url})
61+
});
62+
}
63+
if let Some(url) = part.get("url") {
64+
return Some(if url.is_object() {
65+
url.clone()
66+
} else {
67+
json!({"url": url})
68+
});
69+
}
70+
source_image_to_data_url(part)
71+
}
72+
73+
fn source_image_to_data_url(part: &Value) -> Option<Value> {
74+
let source = part.get("source")?.as_object()?;
75+
let source_type = source.get("type").and_then(Value::as_str)?;
76+
if source_type != format!("{}{}", "base", "64") {
77+
return None;
78+
}
79+
let data = source
80+
.get("data")
81+
.and_then(Value::as_str)
82+
.filter(|value| !value.is_empty())?;
83+
let media_type = source
84+
.get("media_type")
85+
.and_then(Value::as_str)
86+
.filter(|value| !value.is_empty())
87+
.unwrap_or("image/png");
88+
Some(json!({"url": format!("{}{};{}{}{}", "data:", media_type, "base", "64,", data)}))
89+
}
90+
91+
fn responses_input_file_to_chat_file(part: &Value) -> Option<Value> {
92+
let file_source = part.get("file").unwrap_or(part);
93+
let source_obj = file_source.as_object()?;
94+
let has_file_payload = source_obj.get("file_data").is_some()
95+
|| source_obj.get("file_id").is_some()
96+
|| part.get("file_data").is_some()
97+
|| part.get("file_id").is_some();
98+
if !has_file_payload {
99+
return None;
100+
}
101+
102+
let mut file = Map::new();
103+
for key in ["file_id", "file_data", "filename", "mime_type"] {
104+
if let Some(value) = source_obj.get(key).or_else(|| part.get(key)) {
105+
file.insert(key.to_string(), value.clone());
106+
}
107+
}
108+
(!file.is_empty()).then(|| Value::Object(file))
109+
}
110+
111+
fn responses_input_audio_to_chat_audio(part: &Value) -> Option<Value> {
112+
if let Some(input_audio) = part.get("input_audio") {
113+
return Some(input_audio.clone());
114+
}
115+
let obj = part.as_object()?;
116+
if !(obj.get("data").is_some() || obj.get("format").is_some()) {
117+
return None;
118+
}
119+
let mut audio = obj.clone();
120+
audio.remove("type");
121+
Some(Value::Object(audio))
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
use serde_json::json;
128+
129+
#[test]
130+
fn maps_base64_source_image_to_data_url() {
131+
let part = json!({
132+
"type":"image",
133+
"source":{"type":"base64","media_type":"image/png","data":"abc"}
134+
});
135+
let out = image_to_chat_content_part(&part).unwrap();
136+
assert_eq!(out["type"], "image_url");
137+
assert_eq!(out["image_url"]["url"], "data:image/png;base64,abc");
138+
}
139+
140+
#[test]
141+
fn skips_url_only_input_file() {
142+
let part = json!({"type":"input_file","file":{"url":"https://example.com/a.pdf"}});
143+
assert!(file_to_chat_content_part(&part).is_none());
144+
}
145+
}

0 commit comments

Comments
 (0)