1use 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#[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 #[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#[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 pub fn messages(mut self, messages: Vec<Message>) -> Self {
94 self.request.messages = messages;
95 self
96 }
97
98 pub fn message(mut self, message: Message) -> Self {
100 self.request.messages.push(message);
101 self
102 }
103
104 pub fn tools(mut self, tools: Vec<Tool>) -> Self {
106 self.request.tools = Some(tools);
107 self
108 }
109
110 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
112 self.request.tool_choice = Some(choice);
113 self
114 }
115
116 pub fn temperature(mut self, temperature: f32) -> Self {
118 self.request.temperature = Some(temperature);
119 self
120 }
121
122 pub fn top_p(mut self, top_p: f32) -> Self {
124 self.request.top_p = Some(top_p);
125 self
126 }
127
128 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
130 self.request.max_tokens = Some(max_tokens);
131 self
132 }
133
134 pub fn response_format(mut self, format: ResponseFormat) -> Self {
136 self.request.response_format = Some(format);
137 self
138 }
139
140 pub fn n(mut self, n: u32) -> Self {
142 self.request.n = Some(n);
143 self
144 }
145
146 pub fn stop(mut self, stop: Vec<String>) -> Self {
148 self.request.stop = Some(stop);
149 self
150 }
151
152 pub fn presence_penalty(mut self, penalty: f32) -> Self {
154 self.request.presence_penalty = Some(penalty);
155 self
156 }
157
158 pub fn frequency_penalty(mut self, penalty: f32) -> Self {
160 self.request.frequency_penalty = Some(penalty);
161 self
162 }
163
164 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#[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 pub fn temperature(mut self, temperature: f32) -> Self {
230 self.request.temperature = Some(temperature);
231 self
232 }
233
234 pub fn top_p(mut self, top_p: f32) -> Self {
236 self.request.top_p = Some(top_p);
237 self
238 }
239
240 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
242 self.request.max_tokens = Some(max_tokens);
243 self
244 }
245
246 pub fn n(mut self, n: u32) -> Self {
248 self.request.n = Some(n);
249 self
250 }
251
252 pub fn stop(mut self, stop: Vec<String>) -> Self {
254 self.request.stop = Some(stop);
255 self
256 }
257
258 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#[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 pub fn messages(mut self, messages: Vec<Message>) -> Self {
315 self.request.messages = messages;
316 self
317 }
318
319 pub fn message(mut self, message: Message) -> Self {
321 self.request.messages.push(message);
322 self
323 }
324
325 pub fn tools(mut self, tools: Vec<Tool>) -> Self {
327 self.request.tools = Some(tools);
328 self
329 }
330
331 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
333 self.request.tool_choice = Some(choice);
334 self
335 }
336
337 pub fn temperature(mut self, temperature: f32) -> Self {
339 self.request.temperature = Some(temperature);
340 self
341 }
342
343 pub fn top_p(mut self, top_p: f32) -> Self {
345 self.request.top_p = Some(top_p);
346 self
347 }
348
349 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
351 self.request.max_tokens = Some(max_tokens);
352 self
353 }
354
355 pub fn response_format(mut self, format: ResponseFormat) -> Self {
357 self.request.response_format = Some(format);
358 self
359 }
360
361 pub fn n(mut self, n: u32) -> Self {
363 self.request.n = Some(n);
364 self
365 }
366
367 pub fn stop(mut self, stop: Vec<String>) -> Self {
369 self.request.stop = Some(stop);
370 self
371 }
372
373 pub fn presence_penalty(mut self, penalty: f32) -> Self {
375 self.request.presence_penalty = Some(penalty);
376 self
377 }
378
379 pub fn frequency_penalty(mut self, penalty: f32) -> Self {
381 self.request.frequency_penalty = Some(penalty);
382 self
383 }
384
385 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#[derive(Debug, Clone, Deserialize)]
403pub struct ChatCompletion {
404 pub id: String,
406 pub object: String,
408 pub created: i64,
410 pub model: String,
412 pub choices: Vec<ChatChoice>,
414 #[serde(default)]
416 pub usage: Usage,
417 #[serde(default)]
419 pub system_fingerprint: Option<String>,
420}
421
422impl ChatCompletion {
423 pub fn text(&self) -> Option<&str> {
425 self.choices
426 .first()
427 .and_then(|c| c.message.content.as_deref())
428 }
429}
430
431#[derive(Debug, Clone, Deserialize)]
433pub struct ChatChoice {
434 pub index: u32,
436 pub message: ChatMessage,
438 pub finish_reason: Option<String>,
440}
441
442#[derive(Debug, Clone, Deserialize)]
444pub struct ChatMessage {
445 pub role: String,
447 #[serde(default)]
449 pub content: Option<String>,
450 #[serde(default)]
452 pub tool_calls: Option<Vec<ToolCall>>,
453 #[serde(default)]
455 pub reasoning_content: Option<String>,
456}
457
458impl ChatApi {
459 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 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 pub fn start_deferred_completion(&self, model: impl Into<String>) -> MessagesBuilder {
489 MessagesBuilder::new(self.client.clone(), model.into())
490 }
491
492 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}