1use crate::error::XaiError;
11use crate::error::check_for_model_error;
12use crate::traits::ChatCompletionsFetcher;
13use crate::traits::ClientConfig;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum ContentPart {
22 Text { text: String },
23 ImageUrl { image_url: ImageUrl },
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct ImageUrl {
28 pub url: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub detail: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum MessageContent {
37 Text(String),
38 Parts(Vec<ContentPart>),
39}
40
41impl fmt::Display for ContentPart {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 ContentPart::Text { text } => write!(f, "{}", text),
45 ContentPart::ImageUrl { image_url } => write!(f, "[Image: {}]", image_url.url),
46 }
47 }
48}
49
50impl fmt::Display for MessageContent {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 MessageContent::Text(text) => write!(f, "{}", text),
54 MessageContent::Parts(parts) => {
55 for (i, part) in parts.iter().enumerate() {
56 if i > 0 {
57 write!(f, " ")?;
58 }
59 write!(f, "{}", part)?;
60 }
61 Ok(())
62 }
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct Message {
69 pub role: String,
70 pub content: MessageContent,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub reasoning_content: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub refusal: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub tool_calls: Option<Vec<serde_json::Value>>,
80}
81
82impl Message {
83 pub fn text(role: impl Into<String>, content: impl Into<String>) -> Self {
85 Self {
86 role: role.into(),
87 content: MessageContent::Text(content.into()),
88 reasoning_content: None,
89 refusal: None,
90 tool_calls: None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct SearchParameters {
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub from_date: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub to_date: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub mode: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub return_citations: Option<bool>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub max_search_results: Option<u32>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub sources: Option<Vec<serde_json::Value>>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct StreamOptions {
116 pub include_usage: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ChatCompletionRequest {
121 pub model: String,
122 pub messages: Vec<Message>,
123 pub stream: bool,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub temperature: Option<f32>,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub max_tokens: Option<u32>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub max_completion_tokens: Option<u32>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub frequency_penalty: Option<f32>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub presence_penalty: Option<f32>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub n: Option<u32>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub stop: Option<Vec<String>>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub logprobs: Option<bool>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub top_p: Option<f32>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub top_logprobs: Option<u32>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub seed: Option<u32>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub user: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub logit_bias: Option<HashMap<u32, f32>>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub deferred: Option<bool>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub parallel_tool_calls: Option<bool>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub reasoning_effort: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub response_format: Option<serde_json::Value>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub search_parameters: Option<SearchParameters>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub stream_options: Option<StreamOptions>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub tool_choice: Option<serde_json::Value>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub tools: Option<Vec<serde_json::Value>>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub web_search_options: Option<serde_json::Value>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct Logprobs {
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub content: Option<Vec<serde_json::Value>>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Choice {
189 pub index: u32,
190 pub message: Message,
191 pub finish_reason: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub logprobs: Option<Logprobs>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct PromptTokensDetails {
198 pub text_tokens: u32,
199 pub audio_tokens: u32,
200 pub image_tokens: u32,
201 pub cached_tokens: u32,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct CompletionTokensDetails {
206 pub reasoning_tokens: u32,
207 pub audio_tokens: u32,
208 pub accepted_prediction_tokens: u32,
209 pub rejected_prediction_tokens: u32,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct Usage {
214 pub prompt_tokens: u32,
215 pub completion_tokens: u32,
216 pub total_tokens: u32,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub prompt_tokens_details: Option<PromptTokensDetails>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub completion_tokens_details: Option<CompletionTokensDetails>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub cost_in_usd_ticks: Option<i64>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub num_sources_used: Option<u32>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ChatCompletionResponse {
231 pub id: String,
232 pub object: String,
233 pub created: u64,
234 pub model: String,
235 pub choices: Vec<Choice>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub usage: Option<Usage>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub system_fingerprint: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub citations: Option<Vec<serde_json::Value>>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub output_files: Option<Vec<serde_json::Value>>,
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub request_id: Option<String>,
249}
250
251#[derive(Debug, Clone)]
252pub struct ChatCompletionsRequestBuilder<T: ClientConfig + Clone + Send + Sync> {
253 client: T,
254 request: ChatCompletionRequest,
255}
256
257impl<T> ChatCompletionsRequestBuilder<T>
258where
259 T: ClientConfig + Clone + Send + Sync,
260{
261 pub fn new(client: T, model: String, messages: Vec<Message>) -> Self {
262 Self {
263 client,
264 request: ChatCompletionRequest {
265 model,
266 messages,
267 temperature: None,
268 max_tokens: None,
269 max_completion_tokens: None,
270 frequency_penalty: None,
271 presence_penalty: None,
272 n: None,
273 stop: None,
274 stream: false,
275 logprobs: None,
276 top_p: None,
277 top_logprobs: None,
278 seed: None,
279 user: None,
280 logit_bias: None,
281 deferred: None,
282 parallel_tool_calls: None,
283 reasoning_effort: None,
284 response_format: None,
285 search_parameters: None,
286 stream_options: None,
287 tool_choice: None,
288 tools: None,
289 web_search_options: None,
290 },
291 }
292 }
293
294 pub fn temperature(mut self, temperature: f32) -> Self {
295 self.request.temperature = Some(temperature);
296 self
297 }
298
299 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
301 self.request.max_tokens = Some(max_tokens);
302 self
303 }
304
305 pub fn max_completion_tokens(mut self, max_completion_tokens: u32) -> Self {
306 self.request.max_completion_tokens = Some(max_completion_tokens);
307 self
308 }
309
310 pub fn frequency_penalty(mut self, frequency_penalty: f32) -> Self {
311 self.request.frequency_penalty = Some(frequency_penalty);
312 self
313 }
314
315 pub fn presence_penalty(mut self, presence_penalty: f32) -> Self {
316 self.request.presence_penalty = Some(presence_penalty);
317 self
318 }
319
320 pub fn n(mut self, n: u32) -> Self {
321 self.request.n = Some(n);
322 self
323 }
324
325 pub fn stop(mut self, stop: Vec<String>) -> Self {
326 self.request.stop = Some(stop);
327 self
328 }
329
330 pub fn stream(mut self, stream: bool) -> Self {
331 self.request.stream = stream;
332 self
333 }
334
335 pub fn logprobs(mut self, logprobs: bool) -> Self {
336 self.request.logprobs = Some(logprobs);
337 self
338 }
339
340 pub fn top_p(mut self, top_p: f32) -> Self {
341 self.request.top_p = Some(top_p);
342 self
343 }
344
345 pub fn top_logprobs(mut self, top_logprobs: u32) -> Self {
346 self.request.top_logprobs = Some(top_logprobs);
347 self
348 }
349
350 pub fn seed(mut self, seed: u32) -> Self {
351 self.request.seed = Some(seed);
352 self
353 }
354
355 pub fn user(mut self, user: String) -> Self {
356 self.request.user = Some(user);
357 self
358 }
359
360 pub fn logit_bias(mut self, logit_bias: HashMap<u32, f32>) -> Self {
361 self.request.logit_bias = Some(logit_bias);
362 self
363 }
364
365 pub fn deferred(mut self, deferred: bool) -> Self {
366 self.request.deferred = Some(deferred);
367 self
368 }
369
370 pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
371 self.request.parallel_tool_calls = Some(parallel_tool_calls);
372 self
373 }
374
375 pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
378 self.request.reasoning_effort = Some(effort.into());
379 self
380 }
381
382 pub fn response_format(mut self, response_format: serde_json::Value) -> Self {
383 self.request.response_format = Some(response_format);
384 self
385 }
386
387 pub fn search_parameters(mut self, search_parameters: SearchParameters) -> Self {
388 self.request.search_parameters = Some(search_parameters);
389 self
390 }
391
392 pub fn stream_options(mut self, stream_options: StreamOptions) -> Self {
393 self.request.stream_options = Some(stream_options);
394 self
395 }
396
397 pub fn tool_choice(mut self, tool_choice: serde_json::Value) -> Self {
398 self.request.tool_choice = Some(tool_choice);
399 self
400 }
401
402 pub fn tools(mut self, tools: Vec<serde_json::Value>) -> Self {
403 self.request.tools = Some(tools);
404 self
405 }
406
407 pub fn web_search_options(mut self, web_search_options: serde_json::Value) -> Self {
408 self.request.web_search_options = Some(web_search_options);
409 self
410 }
411
412 pub fn build(self) -> Result<ChatCompletionRequest, XaiError> {
413 Ok(self.request)
414 }
415}
416
417impl<T> ChatCompletionsFetcher for ChatCompletionsRequestBuilder<T>
418where
419 T: ClientConfig + Clone + Send + Sync,
420{
421 async fn create_chat_completion(
422 &self,
423 request: ChatCompletionRequest,
424 ) -> Result<ChatCompletionResponse, XaiError> {
425 let response = self
426 .client
427 .request(reqwest::Method::POST, "chat/completions")?
428 .json(&request)
429 .send()
430 .await?;
431
432 if response.status().is_success() {
433 let chat_completion = response.json::<ChatCompletionResponse>().await?;
434 Ok(chat_completion)
435 } else {
436 let error_body = response.text().await.unwrap_or_else(|_| "".to_string());
437
438 if let Some(model_error) = check_for_model_error(&error_body) {
439 return Err(model_error);
440 }
441
442 Err(XaiError::Http(error_body))
443 }
444 }
445}
446