infinity_provider_models_dev/
convert.rs1use infinity_provider_protocol::{
5 AssistantContent, Image, ImageMediaType, ImageSource, Message, ToolDefinition, ToolResult,
6 ToolResultContent, UserContent,
7};
8use serde_json::{Value, json};
9
10#[must_use]
13pub fn to_openai_messages(
14 preamble: Option<&str>,
15 history: &[Message],
16 supports_images: bool,
17) -> (Vec<Value>, Option<String>) {
18 let mut messages = Vec::new();
19 let system: Option<String> = preamble.map(str::to_owned);
20
21 for msg in history {
22 match msg {
23 Message::User { content } => {
24 let mut text_parts = Vec::new();
25 let mut image_parts = Vec::new();
26 let mut tool_msgs = Vec::new();
27 for c in content {
28 match c {
29 UserContent::Text(t) => text_parts.push(t.text.clone()),
30 UserContent::Image(img) => image_parts.push(image_to_data_url(img)),
31 UserContent::ToolResult(r) => {
32 tool_msgs.push(tool_result_message(r, supports_images));
33 }
34 }
35 }
36 if tool_msgs.is_empty() {
37 messages.push(user_message(
38 &text_parts.join("\n"),
39 &image_parts,
40 supports_images,
41 ));
42 } else {
43 if !(text_parts.is_empty() && image_parts.is_empty()) {
46 messages.push(user_message(
47 &text_parts.join("\n"),
48 &image_parts,
49 supports_images,
50 ));
51 }
52 messages.extend(tool_msgs);
53 }
54 }
55 Message::Assistant { content } => {
56 let mut text = String::new();
57 let mut reasoning_text = String::new();
58 let mut tool_calls = Vec::new();
59 for c in content {
60 match c {
61 AssistantContent::Text(t) => {
62 if !text.is_empty() {
63 text.push('\n');
64 }
65 text.push_str(&t.text);
66 }
67 AssistantContent::ToolCall(tc) => {
68 tool_calls.push(json!({
69 "id": tc.id,
70 "type": "function",
71 "function": {
72 "name": tc.function.name,
73 "arguments": arguments_to_string(&tc.function.arguments),
74 }
75 }));
76 }
77 AssistantContent::Reasoning(r) => {
83 let thinking = r.display_text();
84 if !thinking.is_empty() {
85 if !reasoning_text.is_empty() {
86 reasoning_text.push('\n');
87 }
88 reasoning_text.push_str(&thinking);
89 }
90 }
91 }
92 }
93 messages.push(assistant_message(text, reasoning_text, tool_calls));
94 }
95 }
96 }
97
98 (messages, system)
99}
100
101fn assistant_message(text: String, reasoning: String, tool_calls: Vec<Value>) -> Value {
102 let mut map = serde_json::Map::new();
103 map.insert("role".to_owned(), json!("assistant"));
104 map.insert("content".to_owned(), Value::String(text));
107 if !reasoning.is_empty() {
108 map.insert("reasoning_content".to_owned(), Value::String(reasoning));
109 }
110 if !tool_calls.is_empty() {
111 map.insert("tool_calls".to_owned(), Value::Array(tool_calls));
112 }
113 Value::Object(map)
114}
115
116fn tool_result_message(result: &ToolResult, supports_images: bool) -> Value {
121 let mut text_parts = Vec::new();
122 let mut image_parts = Vec::new();
123 for block in &result.content {
124 match block {
125 ToolResultContent::Text(t) => text_parts.push(t.text.clone()),
126 ToolResultContent::Image(img) => {
127 if supports_images {
128 image_parts.push(image_to_data_url(img));
129 } else {
130 text_parts.push("[image omitted: model does not accept images]".to_owned());
131 }
132 }
133 }
134 }
135 let text = text_parts.join("\n");
136 if image_parts.is_empty() {
137 return json!({
138 "role": "tool",
139 "tool_call_id": result.id,
140 "content": text,
141 });
142 }
143 let mut parts = Vec::new();
144 if !text.is_empty() {
145 parts.push(json!({"type": "text", "text": text}));
146 }
147 for url in image_parts {
148 parts.push(json!({"type": "image_url", "image_url": {"url": url}}));
149 }
150 json!({
151 "role": "tool",
152 "tool_call_id": result.id,
153 "content": parts,
154 })
155}
156
157fn user_message(text: &str, images: &[String], supports_images: bool) -> Value {
158 if images.is_empty() || !supports_images {
159 let mut t = text.to_owned();
160 if !images.is_empty() {
161 if !t.is_empty() {
162 t.push('\n');
163 }
164 t.push_str("[image omitted: backend does not accept images]");
165 }
166 return json!({"role": "user", "content": t});
167 }
168 let mut parts = Vec::new();
169 if !text.is_empty() {
170 parts.push(json!({"type": "text", "text": text}));
171 }
172 for url in images {
173 parts.push(json!({"type": "image_url", "image_url": {"url": url}}));
174 }
175 json!({"role": "user", "content": parts})
176}
177
178fn image_to_data_url(img: &Image) -> String {
179 match &img.data {
180 ImageSource::Url(u) => u.clone(),
181 ImageSource::Base64(b) => {
182 let mime = img
183 .media_type
184 .as_ref()
185 .map_or("image/png", ImageMediaType::to_mime_type);
186 format!("data:{mime};base64,{b}")
187 }
188 }
189}
190
191#[must_use]
192pub fn arguments_to_string(v: &Value) -> String {
193 match v {
194 Value::String(s) => s.clone(),
195 _ => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_owned()),
196 }
197}
198
199#[must_use]
200pub fn to_openai_tools(tools: &[ToolDefinition]) -> Vec<Value> {
201 tools
202 .iter()
203 .map(|t| {
204 json!({
205 "type": "function",
206 "function": {
207 "name": t.name,
208 "description": t.description,
209 "parameters": t.parameters,
210 }
211 })
212 })
213 .collect()
214}
215
216#[must_use]
218pub fn classify_http_status(status: u16, body: &str) -> infinity_provider_protocol::ErrorClass {
219 use infinity_provider_protocol::ErrorClass as C;
220 if status == 429 {
221 return C::Throttled;
222 }
223 if status == 529 || (500..600).contains(&status) {
224 return C::Transient;
225 }
226 if status == 408 || status == 504 {
227 return C::Transient;
228 }
229 if status == 400 || status == 413 || status == 422 {
230 let msg = body.to_ascii_lowercase();
231 if msg.contains("context")
232 || msg.contains("too long")
233 || msg.contains("too large")
234 || msg.contains("token limit")
235 || msg.contains("max_tokens")
236 || msg.contains("input length")
237 {
238 return C::ContextOverflow;
239 }
240 if msg.contains("rate limit")
242 || msg.contains("too many requests")
243 || msg.contains("try again")
244 {
245 return C::Throttled;
246 }
247 if msg.contains("overloaded") || msg.contains("capacity") {
250 return C::Transient;
251 }
252 return C::Fatal;
253 }
254 C::Fatal
255}
256
257#[must_use]
264pub fn take_frames(buf: &mut String) -> Vec<String> {
265 let mut frames = Vec::new();
266 loop {
267 let lf = buf.find("\n\n");
268 let crlf = buf.find("\r\n\r\n");
269 let (end, len) = match (lf, crlf) {
270 (Some(a), Some(b)) if b < a => (b, 4),
271 (Some(a), _) => (a, 2),
272 (None, Some(b)) => (b, 4),
273 (None, None) => break,
274 };
275 let frame: String = buf.drain(..end.saturating_add(len)).collect();
276 frames.push(frame);
277 }
278 frames
279}
280
281pub fn push_bytes(out: &mut String, pending: &mut Vec<u8>, bytes: &[u8]) {
287 pending.extend_from_slice(bytes);
288 let valid_up_to = match std::str::from_utf8(pending) {
289 Ok(_) => pending.len(),
290 Err(error) => error.valid_up_to(),
291 };
292 if valid_up_to > 0 {
293 let valid: Vec<u8> = pending.drain(..valid_up_to).collect();
296 out.push_str(&String::from_utf8_lossy(&valid));
297 }
298}
299
300#[must_use]
302pub fn error_message_from_body(body: &str) -> String {
303 if let Ok(v) = serde_json::from_str::<Value>(body) {
304 if let Some(m) = v.pointer("/error/message").and_then(Value::as_str) {
305 return m.to_owned();
306 }
307 if let Some(m) = v.pointer("/error").and_then(Value::as_str) {
308 return m.to_owned();
309 }
310 if let Some(m) = v.pointer("/message").and_then(Value::as_str) {
311 return m.to_owned();
312 }
313 }
314 body.trim().chars().take(500).collect()
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use infinity_provider_protocol::ErrorClass;
321
322 #[test]
323 fn overloaded_and_capacity_are_transient() {
324 assert_eq!(
325 classify_http_status(400, "the engine is overloaded"),
326 ErrorClass::Transient
327 );
328 assert_eq!(
329 classify_http_status(400, "no capacity available right now"),
330 ErrorClass::Transient
331 );
332 assert_eq!(
334 classify_http_status(400, "overloaded, please try again later"),
335 ErrorClass::Throttled
336 );
337 assert_eq!(
339 classify_http_status(400, "input length exceeds context capacity"),
340 ErrorClass::ContextOverflow
341 );
342 assert_eq!(
343 classify_http_status(400, "invalid request"),
344 ErrorClass::Fatal
345 );
346 }
347
348 #[test]
349 fn splits_lf_frames_and_keeps_tail() {
350 let mut buf = "data: 1\n\ndata: 2\n\npartial".to_owned();
351 assert_eq!(
352 take_frames(&mut buf),
353 vec!["data: 1\n\n".to_owned(), "data: 2\n\n".to_owned()]
354 );
355 assert_eq!(buf, "partial");
356 }
357
358 #[test]
359 fn splits_crlf_frames() {
360 let mut buf = "data: 1\r\n\r\ndata: 2\r\n\r\n".to_owned();
361 assert_eq!(
362 take_frames(&mut buf),
363 vec!["data: 1\r\n\r\n".to_owned(), "data: 2\r\n\r\n".to_owned()]
364 );
365 assert_eq!(buf, String::new());
366 }
367
368 #[test]
369 fn earliest_delimiter_wins_and_partial_tail_waits() {
370 let mut buf = "data: 1\r\n\r\ndata:".to_owned();
371 assert_eq!(take_frames(&mut buf), vec!["data: 1\r\n\r\n".to_owned()]);
372 assert_eq!(buf, "data:");
373 buf.push_str(" 2\n\n");
375 assert_eq!(take_frames(&mut buf), vec!["data: 2\n\n".to_owned()]);
376 assert_eq!(buf, String::new());
377 }
378
379 #[test]
380 fn no_delimiter_yields_nothing() {
381 let mut buf = "data: incomplete".to_owned();
382 assert_eq!(take_frames(&mut buf), Vec::<String>::new());
383 assert_eq!(buf, "data: incomplete");
384 }
385}