Skip to main content

xai_rust/api/
chat.rs

1//! Legacy Chat Completions API (deprecated).
2//!
3//! Note: This API is deprecated. Use the Responses API instead.
4//! This deprecation does not apply to `xai_rust::chat` helper functions/models.
5
6use serde::{Deserialize, Serialize};
7
8use crate::client::XaiClient;
9use crate::models::message::Message;
10use crate::models::response::ResponseFormat;
11use crate::models::tool::{Tool, ToolCall, ToolChoice};
12use crate::models::usage::Usage;
13use crate::{Error, Result};
14
15/// Legacy Chat Completions API.
16///
17/// Note: This API is deprecated. Use `client.responses()` instead.
18#[derive(Debug, Clone)]
19pub struct ChatApi {
20    client: XaiClient,
21}
22
23impl ChatApi {
24    pub(crate) fn new(client: XaiClient) -> Self {
25        Self { client }
26    }
27
28    /// Create a chat completion request.
29    #[deprecated(note = "Use ResponsesApi instead")]
30    pub fn create(&self, model: impl Into<String>) -> ChatCompletionBuilder {
31        ChatCompletionBuilder::new(self.client.clone(), model.into())
32    }
33}
34
35/// Builder for chat completion requests.
36#[derive(Debug)]
37pub struct ChatCompletionBuilder {
38    client: XaiClient,
39    request: ChatCompletionRequest,
40}
41
42#[derive(Debug, Clone, Serialize)]
43struct ChatCompletionRequest {
44    model: String,
45    messages: Vec<Message>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    tools: Option<Vec<Tool>>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    tool_choice: Option<ToolChoice>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    temperature: Option<f32>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    top_p: Option<f32>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    max_tokens: Option<u32>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    stream: Option<bool>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    response_format: Option<ResponseFormat>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    n: Option<u32>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    stop: Option<Vec<String>>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    presence_penalty: Option<f32>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    frequency_penalty: Option<f32>,
68}
69
70impl ChatCompletionBuilder {
71    fn new(client: XaiClient, model: String) -> Self {
72        Self {
73            client,
74            request: ChatCompletionRequest {
75                model,
76                messages: Vec::new(),
77                tools: None,
78                tool_choice: None,
79                temperature: None,
80                top_p: None,
81                max_tokens: None,
82                stream: None,
83                response_format: None,
84                n: None,
85                stop: None,
86                presence_penalty: None,
87                frequency_penalty: None,
88            },
89        }
90    }
91
92    /// Add messages to the conversation.
93    pub fn messages(mut self, messages: Vec<Message>) -> Self {
94        self.request.messages = messages;
95        self
96    }
97
98    /// Add a single message.
99    pub fn message(mut self, message: Message) -> Self {
100        self.request.messages.push(message);
101        self
102    }
103
104    /// Add tools.
105    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
106        self.request.tools = Some(tools);
107        self
108    }
109
110    /// Set tool choice.
111    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
112        self.request.tool_choice = Some(choice);
113        self
114    }
115
116    /// Set temperature.
117    pub fn temperature(mut self, temperature: f32) -> Self {
118        self.request.temperature = Some(temperature);
119        self
120    }
121
122    /// Set top_p.
123    pub fn top_p(mut self, top_p: f32) -> Self {
124        self.request.top_p = Some(top_p);
125        self
126    }
127
128    /// Set max tokens.
129    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
130        self.request.max_tokens = Some(max_tokens);
131        self
132    }
133
134    /// Set response format.
135    pub fn response_format(mut self, format: ResponseFormat) -> Self {
136        self.request.response_format = Some(format);
137        self
138    }
139
140    /// Set number of completions.
141    pub fn n(mut self, n: u32) -> Self {
142        self.request.n = Some(n);
143        self
144    }
145
146    /// Set stop sequences.
147    pub fn stop(mut self, stop: Vec<String>) -> Self {
148        self.request.stop = Some(stop);
149        self
150    }
151
152    /// Set presence penalty.
153    pub fn presence_penalty(mut self, penalty: f32) -> Self {
154        self.request.presence_penalty = Some(penalty);
155        self
156    }
157
158    /// Set frequency penalty.
159    pub fn frequency_penalty(mut self, penalty: f32) -> Self {
160        self.request.frequency_penalty = Some(penalty);
161        self
162    }
163
164    /// Send the request.
165    pub async fn send(self) -> Result<ChatCompletion> {
166        let url = format!("{}/chat/completions", self.client.base_url());
167
168        let response = self
169            .client
170            .send(self.client.http().post(&url).json(&self.request))
171            .await?;
172
173        if !response.status().is_success() {
174            return Err(Error::from_response(response).await);
175        }
176
177        Ok(response.json().await?)
178    }
179}
180
181/// Builder for completion-style requests.
182#[derive(Debug)]
183pub struct CompletionBuilder {
184    client: XaiClient,
185    request: CompletionRequest,
186    endpoint: CompletionEndpoint,
187}
188
189#[derive(Debug, Clone, Copy)]
190enum CompletionEndpoint {
191    Complete,
192    Completions,
193}
194
195#[derive(Debug, Clone, Serialize)]
196struct CompletionRequest {
197    model: String,
198    prompt: String,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    temperature: Option<f32>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    top_p: Option<f32>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    max_tokens: Option<u32>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    n: Option<u32>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    stop: Option<Vec<String>>,
209}
210
211impl CompletionBuilder {
212    fn new(client: XaiClient, model: String, prompt: String, endpoint: CompletionEndpoint) -> Self {
213        Self {
214            client,
215            request: CompletionRequest {
216                model,
217                prompt,
218                temperature: None,
219                top_p: None,
220                max_tokens: None,
221                n: None,
222                stop: None,
223            },
224            endpoint,
225        }
226    }
227
228    /// Set temperature.
229    pub fn temperature(mut self, temperature: f32) -> Self {
230        self.request.temperature = Some(temperature);
231        self
232    }
233
234    /// Set top_p.
235    pub fn top_p(mut self, top_p: f32) -> Self {
236        self.request.top_p = Some(top_p);
237        self
238    }
239
240    /// Set max tokens.
241    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
242        self.request.max_tokens = Some(max_tokens);
243        self
244    }
245
246    /// Set number of responses.
247    pub fn n(mut self, n: u32) -> Self {
248        self.request.n = Some(n);
249        self
250    }
251
252    /// Set stop sequences.
253    pub fn stop(mut self, stop: Vec<String>) -> Self {
254        self.request.stop = Some(stop);
255        self
256    }
257
258    /// Send the request.
259    pub async fn send(self) -> Result<ChatCompletion> {
260        let response = match self.endpoint {
261            CompletionEndpoint::Complete => self.client.send(
262                self.client
263                    .http()
264                    .post(format!("{}/v1/complete", self.client.base_url()))
265                    .json(&self.request),
266            ),
267            CompletionEndpoint::Completions => self.client.send(
268                self.client
269                    .http()
270                    .post(format!("{}/v1/completions", self.client.base_url()))
271                    .json(&self.request),
272            ),
273        }
274        .await?;
275
276        if !response.status().is_success() {
277            return Err(Error::from_response(response).await);
278        }
279
280        Ok(response.json().await?)
281    }
282}
283
284/// Builder for deferred completion-style messages endpoint.
285#[derive(Debug)]
286pub struct MessagesBuilder {
287    client: XaiClient,
288    request: ChatCompletionRequest,
289}
290
291impl MessagesBuilder {
292    fn new(client: XaiClient, model: String) -> Self {
293        Self {
294            client,
295            request: ChatCompletionRequest {
296                model,
297                messages: Vec::new(),
298                tools: None,
299                tool_choice: None,
300                temperature: None,
301                top_p: None,
302                max_tokens: None,
303                stream: None,
304                response_format: None,
305                n: None,
306                stop: None,
307                presence_penalty: None,
308                frequency_penalty: None,
309            },
310        }
311    }
312
313    /// Add messages to the conversation.
314    pub fn messages(mut self, messages: Vec<Message>) -> Self {
315        self.request.messages = messages;
316        self
317    }
318
319    /// Add a single message.
320    pub fn message(mut self, message: Message) -> Self {
321        self.request.messages.push(message);
322        self
323    }
324
325    /// Add tools.
326    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
327        self.request.tools = Some(tools);
328        self
329    }
330
331    /// Set tool choice.
332    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
333        self.request.tool_choice = Some(choice);
334        self
335    }
336
337    /// Set temperature.
338    pub fn temperature(mut self, temperature: f32) -> Self {
339        self.request.temperature = Some(temperature);
340        self
341    }
342
343    /// Set top_p.
344    pub fn top_p(mut self, top_p: f32) -> Self {
345        self.request.top_p = Some(top_p);
346        self
347    }
348
349    /// Set max tokens.
350    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
351        self.request.max_tokens = Some(max_tokens);
352        self
353    }
354
355    /// Set response format.
356    pub fn response_format(mut self, format: ResponseFormat) -> Self {
357        self.request.response_format = Some(format);
358        self
359    }
360
361    /// Set number of completions.
362    pub fn n(mut self, n: u32) -> Self {
363        self.request.n = Some(n);
364        self
365    }
366
367    /// Set stop sequences.
368    pub fn stop(mut self, stop: Vec<String>) -> Self {
369        self.request.stop = Some(stop);
370        self
371    }
372
373    /// Set presence penalty.
374    pub fn presence_penalty(mut self, penalty: f32) -> Self {
375        self.request.presence_penalty = Some(penalty);
376        self
377    }
378
379    /// Set frequency penalty.
380    pub fn frequency_penalty(mut self, penalty: f32) -> Self {
381        self.request.frequency_penalty = Some(penalty);
382        self
383    }
384
385    /// Send the request.
386    pub async fn send(self) -> Result<ChatCompletion> {
387        let url = format!("{}/messages", self.client.base_url());
388        let response = self
389            .client
390            .send(self.client.http().post(&url).json(&self.request))
391            .await?;
392
393        if !response.status().is_success() {
394            return Err(Error::from_response(response).await);
395        }
396
397        Ok(response.json().await?)
398    }
399}
400
401/// Chat completion response.
402#[derive(Debug, Clone, Deserialize)]
403pub struct ChatCompletion {
404    /// Unique identifier.
405    pub id: String,
406    /// Object type.
407    pub object: String,
408    /// Creation timestamp.
409    pub created: i64,
410    /// Model used.
411    pub model: String,
412    /// Completion choices.
413    pub choices: Vec<ChatChoice>,
414    /// Usage statistics.
415    #[serde(default)]
416    pub usage: Usage,
417    /// System fingerprint.
418    #[serde(default)]
419    pub system_fingerprint: Option<String>,
420}
421
422impl ChatCompletion {
423    /// Get the text from the first choice.
424    pub fn text(&self) -> Option<&str> {
425        self.choices
426            .first()
427            .and_then(|c| c.message.content.as_deref())
428    }
429}
430
431/// A completion choice.
432#[derive(Debug, Clone, Deserialize)]
433pub struct ChatChoice {
434    /// Choice index.
435    pub index: u32,
436    /// The generated message.
437    pub message: ChatMessage,
438    /// Finish reason.
439    pub finish_reason: Option<String>,
440}
441
442/// A chat message in the response.
443#[derive(Debug, Clone, Deserialize)]
444pub struct ChatMessage {
445    /// Role of the message.
446    pub role: String,
447    /// Text content.
448    #[serde(default)]
449    pub content: Option<String>,
450    /// Tool calls.
451    #[serde(default)]
452    pub tool_calls: Option<Vec<ToolCall>>,
453    /// Reasoning content (for reasoning models).
454    #[serde(default)]
455    pub reasoning_content: Option<String>,
456}
457
458impl ChatApi {
459    /// Create a completion request using `/v1/completions`.
460    pub fn completions(
461        &self,
462        model: impl Into<String>,
463        prompt: impl Into<String>,
464    ) -> CompletionBuilder {
465        CompletionBuilder::new(
466            self.client.clone(),
467            model.into(),
468            prompt.into(),
469            CompletionEndpoint::Completions,
470        )
471    }
472
473    /// Create a completion request using `/v1/complete`.
474    pub fn complete(
475        &self,
476        model: impl Into<String>,
477        prompt: impl Into<String>,
478    ) -> CompletionBuilder {
479        CompletionBuilder::new(
480            self.client.clone(),
481            model.into(),
482            prompt.into(),
483            CompletionEndpoint::Complete,
484        )
485    }
486
487    /// Create a deferred completion request using `/v1/messages`.
488    pub fn start_deferred_completion(&self, model: impl Into<String>) -> MessagesBuilder {
489        MessagesBuilder::new(self.client.clone(), model.into())
490    }
491
492    /// Fetch a deferred completion result by ID using `/v1/chat/deferred-completion/{id}`.
493    pub async fn get_deferred_completion(
494        &self,
495        deferred_id: impl AsRef<str>,
496    ) -> Result<ChatCompletion> {
497        let id = XaiClient::encode_path(deferred_id.as_ref());
498        let url = format!("{}/chat/deferred-completion/{}", self.client.base_url(), id);
499
500        let response = self.client.send(self.client.http().get(&url)).await?;
501
502        if !response.status().is_success() {
503            return Err(Error::from_response(response).await);
504        }
505
506        Ok(response.json().await?)
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use serde_json::json;
514    use wiremock::matchers::{body_partial_json, method, path};
515    use wiremock::{Mock, MockServer, ResponseTemplate};
516
517    #[tokio::test]
518    #[allow(deprecated)]
519    async fn chat_completion_builder_forwards_payload() {
520        let server = MockServer::start().await;
521        let expected_body = json!({
522            "model": "grok-4",
523            "messages": [
524                {"role": "system", "content": "You are concise."},
525                {"role": "user", "content": "Hello"}
526            ],
527            "tools": [{
528                "type": "function",
529                "function": {
530                    "name": "weather",
531                    "description": "Get weather"
532                }
533            }],
534            "tool_choice": {
535                "type": "function",
536                "function": {"name": "weather"}
537            },
538            "temperature": 0.2,
539            "top_p": 0.9,
540            "max_tokens": 12,
541            "response_format": {"type":"text"},
542            "n": 2,
543            "stop": ["STOP"],
544            "presence_penalty": 0.1,
545            "frequency_penalty": 0.2
546        });
547
548        Mock::given(method("POST"))
549            .and(path("/chat/completions"))
550            .and(body_partial_json(expected_body))
551            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
552                "id": "chatcmpl_1",
553                "object": "chat.completion",
554                "created": 1700000000,
555                "model": "grok-4",
556                "choices": [{
557                    "index": 0,
558                    "message": {
559                        "role": "assistant",
560                        "content": "hello"
561                    },
562                    "finish_reason": "stop"
563                }]
564            })))
565            .mount(&server)
566            .await;
567
568        let client = XaiClient::builder()
569            .api_key("test-key")
570            .base_url(server.uri())
571            .build()
572            .unwrap();
573
574        let tool = Tool::function("weather", "Get weather", json!({}));
575        let response = client
576            .chat()
577            .create("grok-4")
578            .message(Message::system("You are concise."))
579            .message(Message::user("Hello"))
580            .tools(vec![tool])
581            .tool_choice(ToolChoice::function("weather"))
582            .temperature(0.2)
583            .top_p(0.9)
584            .max_tokens(12)
585            .response_format(ResponseFormat::text())
586            .n(2)
587            .stop(vec!["STOP".to_string()])
588            .presence_penalty(0.1)
589            .frequency_penalty(0.2)
590            .send()
591            .await
592            .unwrap();
593
594        assert_eq!(response.id, "chatcmpl_1");
595        assert_eq!(response.text(), Some("hello"));
596    }
597
598    #[tokio::test]
599    #[allow(deprecated)]
600    async fn chat_completion_text_helper_returns_none_when_missing() {
601        let server = MockServer::start().await;
602
603        Mock::given(method("POST"))
604            .and(path("/chat/completions"))
605            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
606                "id": "chatcmpl_2",
607                "object": "chat.completion",
608                "created": 1700000000,
609                "model": "grok-4",
610                "choices": []
611            })))
612            .mount(&server)
613            .await;
614
615        let client = XaiClient::builder()
616            .api_key("test-key")
617            .base_url(server.uri())
618            .build()
619            .unwrap();
620
621        let response = client
622            .chat()
623            .create("grok-4")
624            .message(Message::system("No text response"))
625            .send()
626            .await
627            .unwrap();
628
629        assert_eq!(response.text(), None);
630    }
631
632    #[tokio::test]
633    #[allow(deprecated)]
634    async fn completion_builder_uses_complete_path() {
635        let server = MockServer::start().await;
636
637        Mock::given(method("POST"))
638            .and(path("/v1/complete"))
639            .and(body_partial_json(json!({
640                "model": "grok-4",
641                "prompt": "Prompt 1"
642            })))
643            .respond_with(ResponseTemplate::new(200).set_body_json(json!( {
644                "id": "chatcmpl_complete",
645                "object": "text_completion",
646                "created": 1700000000,
647                "model": "grok-4",
648                "choices": []
649            })))
650            .mount(&server)
651            .await;
652
653        let client = XaiClient::builder()
654            .api_key("test-key")
655            .base_url(server.uri())
656            .build()
657            .unwrap();
658
659        let response = client
660            .chat()
661            .complete("grok-4", "Prompt 1")
662            .send()
663            .await
664            .unwrap();
665
666        assert_eq!(response.id, "chatcmpl_complete");
667    }
668
669    #[tokio::test]
670    #[allow(deprecated)]
671    async fn completion_builder_uses_completions_path() {
672        let server = MockServer::start().await;
673
674        Mock::given(method("POST"))
675            .and(path("/v1/completions"))
676            .and(body_partial_json(json!({
677                "model": "grok-4",
678                "prompt": "Prompt 2"
679            })))
680            .respond_with(ResponseTemplate::new(200).set_body_json(json!( {
681                "id": "chatcmpl_completions",
682                "object": "text_completion",
683                "created": 1700000000,
684                "model": "grok-4",
685                "choices": []
686            })))
687            .mount(&server)
688            .await;
689
690        let client = XaiClient::builder()
691            .api_key("test-key")
692            .base_url(server.uri())
693            .build()
694            .unwrap();
695
696        let response = client
697            .chat()
698            .completions("grok-4", "Prompt 2")
699            .send()
700            .await
701            .unwrap();
702
703        assert_eq!(response.id, "chatcmpl_completions");
704    }
705
706    #[tokio::test]
707    #[allow(deprecated)]
708    async fn get_deferred_completion_encodes_id_in_path() {
709        let server = MockServer::start().await;
710        let encoded_id = XaiClient::encode_path("deferred request");
711
712        Mock::given(method("GET"))
713            .and(path(format!("/chat/deferred-completion/{encoded_id}")))
714            .respond_with(ResponseTemplate::new(200).set_body_json(json!( {
715                "id": "chat_deferred_1",
716                "object": "chat.completion",
717                "created": 1700000000,
718                "model": "grok-4",
719                "choices": [],
720                "output": [],
721                "usage": { "prompt_tokens": 1, "completion_tokens": 0, "total_tokens": 1 }
722            })))
723            .mount(&server)
724            .await;
725
726        let client = XaiClient::builder()
727            .api_key("test-key")
728            .base_url(server.uri())
729            .build()
730            .unwrap();
731
732        let response = client
733            .chat()
734            .get_deferred_completion("deferred request")
735            .await
736            .unwrap();
737
738        assert_eq!(response.id, "chat_deferred_1");
739    }
740
741    #[tokio::test]
742    #[allow(deprecated)]
743    async fn chat_completion_builder_supports_messages_vec_and_stream() {
744        let server = MockServer::start().await;
745
746        Mock::given(method("POST"))
747            .and(path("/chat/completions"))
748            .and(body_partial_json(json!({
749                "model": "grok-4",
750                "messages": [
751                    {"role": "system", "content": "You are concise."},
752                    {"role": "user", "content": "Summarize this."}
753                ],
754                "temperature": 0.2,
755                "top_p": 0.9,
756                "max_tokens": 128,
757                "n": 1,
758                "stop": ["STOP"]
759            })))
760            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
761                "id": "chatcmpl_stream",
762                "object": "chat.completion",
763                "created": 1700000000,
764                "model": "grok-4",
765                "choices": []
766            })))
767            .mount(&server)
768            .await;
769
770        let client = XaiClient::builder()
771            .api_key("test-key")
772            .base_url(server.uri())
773            .build()
774            .unwrap();
775
776        let response = client
777            .chat()
778            .create("grok-4")
779            .messages(vec![
780                Message::system("You are concise."),
781                Message::user("Summarize this."),
782            ])
783            .temperature(0.2)
784            .top_p(0.9)
785            .max_tokens(128)
786            .n(1)
787            .stop(vec!["STOP".to_string()])
788            .send()
789            .await
790            .unwrap();
791
792        assert_eq!(response.id, "chatcmpl_stream");
793    }
794
795    #[tokio::test]
796    #[allow(deprecated)]
797    async fn completion_builder_includes_options_for_complete_request() {
798        let server = MockServer::start().await;
799
800        Mock::given(method("POST"))
801            .and(path("/v1/complete"))
802            .and(body_partial_json(json!({
803                "model": "grok-4",
804                "prompt": "Prompt 1",
805                "temperature": 0.7,
806                "top_p": 0.8,
807                "max_tokens": 50,
808                "n": 2,
809                "stop": ["stop_1", "stop_2"]
810            })))
811            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
812                "id": "chatcmpl_complete_options",
813                "object": "text_completion",
814                "created": 1700000000,
815                "model": "grok-4",
816                "choices": []
817            })))
818            .mount(&server)
819            .await;
820
821        let client = XaiClient::builder()
822            .api_key("test-key")
823            .base_url(server.uri())
824            .build()
825            .unwrap();
826
827        let response = client
828            .chat()
829            .complete("grok-4", "Prompt 1")
830            .temperature(0.7)
831            .top_p(0.8)
832            .max_tokens(50)
833            .n(2)
834            .stop(vec!["stop_1".to_string(), "stop_2".to_string()])
835            .send()
836            .await
837            .unwrap();
838
839        assert_eq!(response.id, "chatcmpl_complete_options");
840    }
841
842    #[tokio::test]
843    #[allow(deprecated)]
844    async fn completion_builder_send_propagates_api_error() {
845        let server = MockServer::start().await;
846
847        Mock::given(method("POST"))
848            .and(path("/v1/completions"))
849            .and(body_partial_json(json!({
850                "model": "grok-4",
851                "prompt": "bad prompt"
852            })))
853            .respond_with(ResponseTemplate::new(500).set_body_json(json!({
854                "error": { "message": "invalid completion request" }
855            })))
856            .mount(&server)
857            .await;
858
859        let client = XaiClient::builder()
860            .api_key("test-key")
861            .base_url(server.uri())
862            .build()
863            .unwrap();
864
865        let err = client
866            .chat()
867            .completions("grok-4", "bad prompt")
868            .send()
869            .await
870            .unwrap_err();
871
872        match err {
873            Error::Api {
874                status, message, ..
875            } => {
876                assert_eq!(status, 500);
877                assert_eq!(message, "invalid completion request");
878            }
879            _ => panic!("expected Error::Api"),
880        }
881    }
882
883    #[tokio::test]
884    #[allow(deprecated)]
885    async fn chat_completion_builder_send_propagates_api_error() {
886        let server = MockServer::start().await;
887
888        Mock::given(method("POST"))
889            .and(path("/chat/completions"))
890            .and(body_partial_json(json!({
891                "model": "grok-4",
892                "messages": [{"role": "user", "content": "oops"}]
893            })))
894            .respond_with(ResponseTemplate::new(500).set_body_json(json!({
895                "error": {"message": "invalid chat completion request"}
896            })))
897            .mount(&server)
898            .await;
899
900        let client = XaiClient::builder()
901            .api_key("test-key")
902            .base_url(server.uri())
903            .build()
904            .unwrap();
905
906        let err = client
907            .chat()
908            .create("grok-4")
909            .message(Message::user("oops"))
910            .send()
911            .await
912            .unwrap_err();
913
914        match err {
915            Error::Api {
916                status, message, ..
917            } => {
918                assert_eq!(status, 500);
919                assert_eq!(message, "invalid chat completion request");
920            }
921            _ => panic!("expected Error::Api"),
922        }
923    }
924
925    #[tokio::test]
926    #[allow(deprecated)]
927    async fn messages_builder_supports_messages_and_tool_choice_payload_fields() {
928        let server = MockServer::start().await;
929
930        Mock::given(method("POST"))
931            .and(path("/messages"))
932            .and(body_partial_json(json!({
933                "model": "grok-4",
934                "messages": [
935                    {"role": "user", "content": "How's the weather?"},
936                    {"role": "assistant", "content": "Checking..."}
937                ],
938                "tools": [
939                    {
940                        "type": "function",
941                        "function": {
942                            "name": "get_weather",
943                            "description": "Get weather"
944                        }
945                    }
946                ],
947                "tool_choice": {
948                    "type": "function",
949                    "function": { "name": "get_weather" }
950                },
951                "temperature": 0.1,
952                "top_p": 0.95,
953                "max_tokens": 64,
954                "response_format": {"type":"json_object"},
955                "n": 2,
956                "stop": ["END"]
957            })))
958            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
959                "id": "chatcmpl_messages",
960                "object": "chat.completion",
961                "created": 1700000000,
962                "model": "grok-4",
963                "choices": []
964            })))
965            .mount(&server)
966            .await;
967
968        let tool = Tool::function("get_weather", "Get weather", json!({}));
969        let client = XaiClient::builder()
970            .api_key("test-key")
971            .base_url(server.uri())
972            .build()
973            .unwrap();
974
975        let response = client
976            .chat()
977            .start_deferred_completion("grok-4")
978            .messages(vec![Message::user("How's the weather?")])
979            .message(Message::assistant("Checking..."))
980            .tools(vec![tool])
981            .tool_choice(ToolChoice::function("get_weather"))
982            .temperature(0.1)
983            .top_p(0.95)
984            .max_tokens(64)
985            .response_format(ResponseFormat::json_object())
986            .n(2)
987            .stop(vec!["END".to_string()])
988            .send()
989            .await
990            .unwrap();
991
992        assert_eq!(response.id, "chatcmpl_messages");
993    }
994
995    #[tokio::test]
996    #[allow(deprecated)]
997    async fn messages_builder_forwards_penalty_fields() {
998        let server = MockServer::start().await;
999
1000        Mock::given(method("POST"))
1001            .and(path("/messages"))
1002            .and(body_partial_json(json!({
1003                "model": "grok-4",
1004                "presence_penalty": -0.2,
1005                "frequency_penalty": 0.4
1006            })))
1007            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1008                "id": "chatcmpl_penalties",
1009                "object": "chat.completion",
1010                "created": 1700000000,
1011                "model": "grok-4",
1012                "choices": []
1013            })))
1014            .mount(&server)
1015            .await;
1016
1017        let client = XaiClient::builder()
1018            .api_key("test-key")
1019            .base_url(server.uri())
1020            .build()
1021            .unwrap();
1022
1023        let response = client
1024            .chat()
1025            .start_deferred_completion("grok-4")
1026            .message(Message::user("Apply penalty settings."))
1027            .presence_penalty(-0.2)
1028            .frequency_penalty(0.4)
1029            .send()
1030            .await
1031            .unwrap();
1032
1033        assert_eq!(response.id, "chatcmpl_penalties");
1034    }
1035
1036    #[tokio::test]
1037    #[allow(deprecated)]
1038    async fn get_deferred_completion_propagates_api_error() {
1039        let server = MockServer::start().await;
1040        let encoded_id = XaiClient::encode_path("missing deferred");
1041
1042        Mock::given(method("GET"))
1043            .and(path(format!("/chat/deferred-completion/{encoded_id}")))
1044            .respond_with(ResponseTemplate::new(404).set_body_json(json!({
1045                "error": {"message": "deferred completion not found"}
1046            })))
1047            .mount(&server)
1048            .await;
1049
1050        let client = XaiClient::builder()
1051            .api_key("test-key")
1052            .base_url(server.uri())
1053            .build()
1054            .unwrap();
1055
1056        let err = client
1057            .chat()
1058            .get_deferred_completion("missing deferred")
1059            .await
1060            .unwrap_err();
1061
1062        match err {
1063            Error::Api {
1064                status, message, ..
1065            } => {
1066                assert_eq!(status, 404);
1067                assert_eq!(message, "deferred completion not found");
1068            }
1069            _ => panic!("expected Error::Api"),
1070        }
1071    }
1072}