infinity_provider_models_dev/
convert.rs1use infinity_provider_protocol::{
5 AssistantContent, Image, ImageMediaType, ImageSource, Message, ToolDefinition,
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 let text = r
33 .content
34 .iter()
35 .map(|b| match b {
36 ToolResultContent::Text(t) => t.text.clone(),
37 ToolResultContent::Image(img) => image_to_data_url(img),
38 })
39 .collect::<Vec<_>>()
40 .join("\n");
41 tool_msgs.push(json!({
42 "role": "tool",
43 "tool_call_id": r.id,
44 "content": text,
45 }));
46 }
47 }
48 }
49 if tool_msgs.is_empty() {
50 messages.push(user_message(
51 &text_parts.join("\n"),
52 &image_parts,
53 supports_images,
54 ));
55 } else {
56 if !(text_parts.is_empty() && image_parts.is_empty()) {
59 messages.push(user_message(
60 &text_parts.join("\n"),
61 &image_parts,
62 supports_images,
63 ));
64 }
65 messages.extend(tool_msgs);
66 }
67 }
68 Message::Assistant { content } => {
69 let mut text = String::new();
70 let mut tool_calls = Vec::new();
71 for c in content {
72 match c {
73 AssistantContent::Text(t) => {
74 if !text.is_empty() {
75 text.push('\n');
76 }
77 text.push_str(&t.text);
78 }
79 AssistantContent::ToolCall(tc) => {
80 tool_calls.push(json!({
81 "id": tc.id,
82 "type": "function",
83 "function": {
84 "name": tc.function.name,
85 "arguments": arguments_to_string(&tc.function.arguments),
86 }
87 }));
88 }
89 AssistantContent::Reasoning(_) => {}
90 }
91 }
92 if tool_calls.is_empty() {
93 messages.push(json!({"role": "assistant", "content": text}));
94 } else {
95 messages.push(json!({
96 "role": "assistant",
97 "content": if text.is_empty() { Value::Null } else { Value::String(text) },
98 "tool_calls": tool_calls,
99 }));
100 }
101 }
102 }
103 }
104
105 (messages, system)
106}
107
108fn user_message(text: &str, images: &[String], supports_images: bool) -> Value {
109 if images.is_empty() || !supports_images {
110 let mut t = text.to_owned();
111 if !images.is_empty() {
112 if !t.is_empty() {
113 t.push('\n');
114 }
115 t.push_str("[image omitted: backend does not accept images]");
116 }
117 return json!({"role": "user", "content": t});
118 }
119 let mut parts = Vec::new();
120 if !text.is_empty() {
121 parts.push(json!({"type": "text", "text": text}));
122 }
123 for url in images {
124 parts.push(json!({"type": "image_url", "image_url": {"url": url}}));
125 }
126 json!({"role": "user", "content": parts})
127}
128
129fn image_to_data_url(img: &Image) -> String {
130 match &img.data {
131 ImageSource::Url(u) => u.clone(),
132 ImageSource::Base64(b) => {
133 let mime = img
134 .media_type
135 .as_ref()
136 .map_or("image/png", ImageMediaType::to_mime_type);
137 format!("data:{mime};base64,{b}")
138 }
139 }
140}
141
142#[must_use]
143pub fn arguments_to_string(v: &Value) -> String {
144 match v {
145 Value::String(s) => s.clone(),
146 _ => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_owned()),
147 }
148}
149
150#[must_use]
151pub fn to_openai_tools(tools: &[ToolDefinition]) -> Vec<Value> {
152 tools
153 .iter()
154 .map(|t| {
155 json!({
156 "type": "function",
157 "function": {
158 "name": t.name,
159 "description": t.description,
160 "parameters": t.parameters,
161 }
162 })
163 })
164 .collect()
165}
166
167#[must_use]
169pub fn classify_http_status(status: u16, body: &str) -> infinity_provider_protocol::ErrorClass {
170 use infinity_provider_protocol::ErrorClass as C;
171 if status == 429 {
172 return C::Throttled;
173 }
174 if status == 529 || (500..600).contains(&status) {
175 return C::Transient;
176 }
177 if status == 408 || status == 504 {
178 return C::Transient;
179 }
180 if status == 400 || status == 413 || status == 422 {
181 let msg = body.to_ascii_lowercase();
182 if msg.contains("context")
183 || msg.contains("too long")
184 || msg.contains("too large")
185 || msg.contains("token limit")
186 || msg.contains("max_tokens")
187 || msg.contains("input length")
188 {
189 return C::ContextOverflow;
190 }
191 if msg.contains("rate limit")
193 || msg.contains("too many requests")
194 || msg.contains("try again")
195 {
196 return C::Throttled;
197 }
198 return C::Fatal;
199 }
200 C::Fatal
201}
202
203#[must_use]
205pub fn error_message_from_body(body: &str) -> String {
206 if let Ok(v) = serde_json::from_str::<Value>(body) {
207 if let Some(m) = v.pointer("/error/message").and_then(Value::as_str) {
208 return m.to_owned();
209 }
210 if let Some(m) = v.pointer("/error").and_then(Value::as_str) {
211 return m.to_owned();
212 }
213 if let Some(m) = v.pointer("/message").and_then(Value::as_str) {
214 return m.to_owned();
215 }
216 }
217 body.trim().chars().take(500).collect()
218}