1use serde::{Deserialize, Serialize};
9
10use crate::completion::{self, CompletionError};
11use crate::message::{Message as RigMessage, MimeType};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type", rename = "snake_case")]
20#[allow(clippy::enum_variant_names)]
21pub enum Message {
22 Message { role: Role, content: Content },
24 FunctionCall {
26 call_id: String,
27 name: String,
28 arguments: String,
29 },
30 FunctionCallOutput { call_id: String, output: String },
32}
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Role {
37 System,
38 User,
39 Assistant,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum Content {
45 Text(String),
46 Array(Vec<ContentItem>),
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(tag = "type")]
52pub enum ContentItem {
53 #[serde(rename = "input_text")]
54 Text { text: String },
55 #[serde(rename = "input_image")]
56 Image {
57 image_url: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 detail: Option<String>,
60 },
61 #[serde(rename = "input_file")]
62 File {
63 #[serde(skip_serializing_if = "Option::is_none")]
64 file_url: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 file_data: Option<String>,
67 },
68}
69
70impl Message {
71 pub fn system(content: impl Into<String>) -> Self {
72 Self::Message {
73 role: Role::System,
74 content: Content::Text(content.into()),
75 }
76 }
77
78 pub fn user(content: impl Into<String>) -> Self {
79 Self::Message {
80 role: Role::User,
81 content: Content::Text(content.into()),
82 }
83 }
84
85 pub fn user_with_content(content: Vec<ContentItem>) -> Self {
86 Self::Message {
87 role: Role::User,
88 content: Content::Array(content),
89 }
90 }
91
92 pub fn assistant(content: impl Into<String>) -> Self {
93 Self::Message {
94 role: Role::Assistant,
95 content: Content::Text(content.into()),
96 }
97 }
98
99 pub fn function_call(call_id: String, name: String, arguments: String) -> Self {
100 Self::FunctionCall {
101 call_id,
102 name,
103 arguments,
104 }
105 }
106
107 pub fn function_call_output(call_id: String, output: String) -> Self {
108 Self::FunctionCallOutput { call_id, output }
109 }
110}
111
112impl TryFrom<RigMessage> for Vec<Message> {
113 type Error = CompletionError;
114
115 fn try_from(msg: RigMessage) -> Result<Self, Self::Error> {
116 use crate::message::{
117 AssistantContent, Document, DocumentSourceKind, Image as RigImage, Text,
118 ToolResultContent, UserContent,
119 };
120
121 fn image_item(img: RigImage) -> Result<ContentItem, CompletionError> {
122 let url = match img.data {
123 DocumentSourceKind::Url(u) => u,
124 DocumentSourceKind::Base64(data) => {
125 let mime = img
126 .media_type
127 .map(|m| m.to_mime_type())
128 .unwrap_or("image/png");
129 format!("data:{mime};base64,{data}")
130 }
131 _ => {
132 return Err(CompletionError::RequestError(
133 "xAI does not support raw image data; use base64 or URL".into(),
134 ));
135 }
136 };
137 Ok(ContentItem::Image {
138 image_url: url,
139 detail: img.detail.map(|d| format!("{d:?}").to_lowercase()),
140 })
141 }
142
143 fn document_item(doc: Document) -> Result<ContentItem, CompletionError> {
144 let (file_data, file_url) = match doc.data {
145 DocumentSourceKind::Url(url) => (None, Some(url)),
146 DocumentSourceKind::Base64(data) => {
147 let mime = doc
148 .media_type
149 .map(|m| m.to_mime_type())
150 .unwrap_or("application/pdf");
151 (Some(format!("data:{mime};base64,{data}")), None)
152 }
153 DocumentSourceKind::String(text) => {
154 return Ok(ContentItem::Text { text });
156 }
157 _ => {
158 return Err(CompletionError::RequestError(
159 "xAI does not support raw document data; use base64 or URL".into(),
160 ));
161 }
162 };
163 Ok(ContentItem::File {
164 file_url,
165 file_data,
166 })
167 }
168
169 match msg {
170 RigMessage::User { content } => {
171 let mut items = Vec::new();
172 let mut text_parts = Vec::new();
173 let mut content_items = Vec::new();
174 let mut has_images = false;
175
176 for c in content {
177 match c {
178 UserContent::Text(Text { text }) => text_parts.push(text),
179 UserContent::Image(img) => {
180 has_images = true;
181 content_items.push(image_item(img)?);
182 }
183 UserContent::ToolResult(tr) => {
184 if has_images {
186 let mut msg_items: Vec<_> = text_parts
187 .drain(..)
188 .map(|t| ContentItem::Text { text: t })
189 .collect();
190 msg_items.append(&mut content_items);
191 if !msg_items.is_empty() {
192 items.push(Message::user_with_content(msg_items));
193 }
194 } else if !text_parts.is_empty() {
195 items.push(Message::user(text_parts.join("\n")));
196 }
197 has_images = false;
198
199 let output = tr
201 .content
202 .into_iter()
203 .map(|tc| match tc {
204 ToolResultContent::Text(t) => Ok(t.text),
205 ToolResultContent::Image(_) => {
206 Err(CompletionError::RequestError(
207 "xAI does not support images in tool results".into(),
208 ))
209 }
210 })
211 .collect::<Result<Vec<_>, _>>()?
212 .join("\n");
213 items.push(Message::function_call_output(
214 tr.call_id.unwrap_or_default(),
215 output,
216 ));
217 }
218 UserContent::Document(doc) => {
219 has_images = true; content_items.push(document_item(doc)?);
221 }
222 UserContent::Audio(_) => {
223 return Err(CompletionError::RequestError(
224 "xAI does not support audio".into(),
225 ));
226 }
227 UserContent::Video(_) => {
228 return Err(CompletionError::RequestError(
229 "xAI does not support video".into(),
230 ));
231 }
232 }
233 }
234
235 if has_images {
237 let mut msg_items: Vec<_> = text_parts
238 .into_iter()
239 .map(|t| ContentItem::Text { text: t })
240 .collect();
241 msg_items.append(&mut content_items);
242 if !msg_items.is_empty() {
243 items.push(Message::user_with_content(msg_items));
244 }
245 } else if !text_parts.is_empty() {
246 items.push(Message::user(text_parts.join("\n")));
247 }
248
249 Ok(items)
250 }
251 RigMessage::Assistant { content, .. } => {
252 let mut items = Vec::new();
253 let mut text_parts = Vec::new();
254
255 for c in content {
256 match c {
257 AssistantContent::Text(t) => text_parts.push(t.text),
258 AssistantContent::ToolCall(tc) => {
259 if !text_parts.is_empty() {
261 items.push(Message::assistant(text_parts.join("\n")));
262 }
263 items.push(Message::function_call(
265 tc.call_id.unwrap_or_default(),
266 tc.function.name,
267 tc.function.arguments.to_string(),
268 ));
269 }
270 AssistantContent::Reasoning(r) => text_parts.extend(r.reasoning),
271 AssistantContent::Image(_) => {
272 return Err(CompletionError::RequestError(
273 "xAI does not support images in assistant content".into(),
274 ));
275 }
276 }
277 }
278
279 if !text_parts.is_empty() {
281 items.push(Message::assistant(text_parts.join("\n")));
282 }
283
284 Ok(items)
285 }
286 }
287 }
288}
289
290#[derive(Clone, Debug, Deserialize, Serialize)]
291pub struct ToolDefinition {
292 pub r#type: String,
293 #[serde(flatten)]
294 pub function: completion::ToolDefinition,
295}
296
297impl From<completion::ToolDefinition> for ToolDefinition {
298 fn from(tool: completion::ToolDefinition) -> Self {
299 Self {
300 r#type: "function".to_string(),
301 function: tool,
302 }
303 }
304}
305
306#[derive(Debug, Deserialize)]
312pub struct ApiError {
313 pub error: String,
314 pub code: String,
315}
316
317impl ApiError {
318 pub fn message(&self) -> String {
319 format!("Code `{}`: {}", self.code, self.error)
320 }
321}
322
323#[derive(Debug, Deserialize)]
324#[serde(untagged)]
325pub enum ApiResponse<T> {
326 Ok(T),
327 Error(ApiError),
328}