1use std::future::Future;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::runtime::{Builder, Runtime};
11
12use crate::api::chat::{ChatCompletion, ChatCompletionBuilder};
13#[cfg(feature = "files")]
14use crate::api::files::UploadFileBuilder;
15use crate::api::images::ImageGenerationBuilder;
16use crate::api::models::{Model, ModelListResponse};
17use crate::api::responses::{CreateResponseBuilder, DeferredResponsePoller, StatefulChat};
18use crate::models::auth::ApiKeyInfo;
19use crate::models::batch::{
20 Batch, BatchListResponse, BatchRequest, BatchRequestListResponse, BatchResult,
21 BatchResultListResponse,
22};
23use crate::models::collection::{
24 AddDocumentsResponse, BatchGetDocumentsRequest, Collection, CollectionListResponse,
25 CreateCollectionRequest, Document, DocumentListResponse, SearchRequest, SearchResponse,
26 UpdateCollectionRequest,
27};
28#[cfg(feature = "files")]
29use crate::models::file::{
30 DeleteFileResponse, FileDownloadRequest, FileListResponse, FileObject, FilePurpose,
31 FileUploadChunksRequest, FileUploadChunksResponse, FileUploadInitializeRequest,
32 FileUploadInitializeResponse,
33};
34use crate::models::image::{ImageGenerationResponse, ImageResponseFormat};
35use crate::models::message::{Message, MessageContent, Role};
36use crate::models::response::{Response, ResponseFormat};
37use crate::models::tokenizer::{TokenizeRequest, TokenizeResponse};
38use crate::models::tool::{Tool, ToolCall, ToolChoice};
39use crate::models::videos::Video;
40use crate::{Error, Result, XaiClient};
41
42const ASYNC_RUNTIME_GUARD_MESSAGE: &str =
43 "SyncXaiClient cannot run inside an async runtime; use XaiClient for async contexts";
44const DEFAULT_SYNC_TOOL_LOOP_MAX_ROUNDS: u32 = 8;
45
46#[derive(Clone)]
48pub struct SyncXaiClient {
49 client: XaiClient,
50 runtime: Arc<Runtime>,
51}
52
53impl std::fmt::Debug for SyncXaiClient {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("SyncXaiClient")
56 .field("base_url", &self.base_url())
57 .finish()
58 }
59}
60
61impl SyncXaiClient {
62 pub fn new(api_key: impl Into<String>) -> Result<Self> {
64 Self::with_async_client(XaiClient::new(api_key)?)
65 }
66
67 pub fn from_env() -> Result<Self> {
69 Self::with_async_client(XaiClient::from_env()?)
70 }
71
72 pub fn with_async_client(client: XaiClient) -> Result<Self> {
74 let runtime = Builder::new_multi_thread().enable_all().build()?;
75 Ok(Self {
76 client,
77 runtime: Arc::new(runtime),
78 })
79 }
80
81 pub fn as_async_client(&self) -> &XaiClient {
83 &self.client
84 }
85
86 pub fn base_url(&self) -> &str {
88 self.client.base_url()
89 }
90
91 pub fn responses(&self) -> SyncResponsesApi {
93 SyncResponsesApi {
94 client: self.clone(),
95 }
96 }
97
98 pub fn auth(&self) -> SyncAuthApi {
100 SyncAuthApi {
101 client: self.clone(),
102 }
103 }
104
105 pub fn models(&self) -> SyncModelsApi {
107 SyncModelsApi {
108 client: self.clone(),
109 }
110 }
111
112 pub fn chat(&self) -> SyncChatApi {
114 SyncChatApi {
115 client: self.clone(),
116 }
117 }
118
119 pub fn images(&self) -> SyncImagesApi {
121 SyncImagesApi {
122 client: self.clone(),
123 }
124 }
125
126 pub fn videos(&self) -> SyncVideosApi {
128 SyncVideosApi {
129 client: self.clone(),
130 }
131 }
132
133 pub fn batch(&self) -> SyncBatchApi {
135 SyncBatchApi {
136 client: self.clone(),
137 }
138 }
139
140 pub fn collections(&self) -> SyncCollectionsApi {
142 SyncCollectionsApi {
143 client: self.clone(),
144 }
145 }
146
147 #[cfg(feature = "files")]
149 pub fn files(&self) -> SyncFilesApi {
150 SyncFilesApi {
151 client: self.clone(),
152 }
153 }
154
155 pub fn tokenizer(&self) -> SyncTokenizerApi {
157 SyncTokenizerApi {
158 client: self.clone(),
159 }
160 }
161
162 pub fn run<T, F>(&self, future: F) -> Result<T>
166 where
167 F: Future<Output = Result<T>>,
168 {
169 if tokio::runtime::Handle::try_current().is_ok() {
170 return Err(Error::Config(ASYNC_RUNTIME_GUARD_MESSAGE.to_string()));
171 }
172 self.runtime.block_on(future)
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct SyncResponsesApi {
179 client: SyncXaiClient,
180}
181
182impl SyncResponsesApi {
183 pub fn create(&self, model: impl Into<String>) -> SyncCreateResponseBuilder {
185 SyncCreateResponseBuilder {
186 client: self.client.clone(),
187 inner: self.client.client.responses().create(model),
188 }
189 }
190
191 pub fn deferred(&self, response_id: impl Into<String>) -> SyncDeferredResponsePoller {
193 SyncDeferredResponsePoller {
194 client: self.client.clone(),
195 inner: self.client.client.responses().deferred(response_id),
196 }
197 }
198
199 pub fn chat(&self, model: impl Into<String>) -> SyncStatefulChat {
201 SyncStatefulChat {
202 client: self.client.clone(),
203 inner: self.client.client.responses().chat(model),
204 }
205 }
206
207 pub fn get(&self, response_id: &str) -> Result<Response> {
209 self.client
210 .run(self.client.client.responses().get(response_id))
211 }
212
213 pub fn delete(&self, response_id: &str) -> Result<()> {
215 self.client
216 .run(self.client.client.responses().delete(response_id))
217 }
218
219 pub fn poll_until_ready(&self, response_id: &str, max_attempts: u32) -> Result<Response> {
221 self.client.run(
222 self.client
223 .client
224 .responses()
225 .poll_until_ready(response_id, max_attempts),
226 )
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct SyncModelsApi {
233 client: SyncXaiClient,
234}
235
236impl SyncModelsApi {
237 pub fn list(&self) -> Result<ModelListResponse> {
239 self.client.run(self.client.client.models().list())
240 }
241
242 pub fn get(&self, model_id: &str) -> Result<Model> {
244 self.client.run(self.client.client.models().get(model_id))
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct SyncAuthApi {
251 client: SyncXaiClient,
252}
253
254impl SyncAuthApi {
255 pub fn api_key(&self) -> Result<ApiKeyInfo> {
257 self.client.run(self.client.client.auth().api_key())
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct SyncVideosApi {
264 client: SyncXaiClient,
265}
266
267impl SyncVideosApi {
268 pub fn get(&self, video_id: impl AsRef<str>) -> Result<Video> {
270 self.client
271 .run(self.client.client.videos().get(video_id.as_ref()))
272 }
273}
274
275#[derive(Debug, Clone)]
277pub struct SyncTokenizerApi {
278 client: SyncXaiClient,
279}
280
281impl SyncTokenizerApi {
282 pub fn tokenize(&self, request: TokenizeRequest) -> Result<TokenizeResponse> {
284 self.client
285 .run(self.client.client.tokenizer().tokenize(request))
286 }
287
288 pub fn tokenize_text(
290 &self,
291 model: impl Into<String>,
292 text: impl Into<String>,
293 ) -> Result<TokenizeResponse> {
294 self.client
295 .run(self.client.client.tokenizer().tokenize_text(model, text))
296 }
297
298 pub fn count_tokens(&self, model: impl Into<String>, text: impl Into<String>) -> Result<usize> {
300 self.client
301 .run(self.client.client.tokenizer().count_tokens(model, text))
302 }
303}
304
305#[derive(Debug, Clone)]
307pub struct SyncChatApi {
308 client: SyncXaiClient,
309}
310
311impl SyncChatApi {
312 #[allow(deprecated)]
314 pub fn create(&self, model: impl Into<String>) -> SyncChatCompletionBuilder {
315 SyncChatCompletionBuilder {
316 client: self.client.clone(),
317 inner: self.client.client.chat().create(model),
318 }
319 }
320}
321
322#[derive(Debug)]
324pub struct SyncChatCompletionBuilder {
325 client: SyncXaiClient,
326 inner: ChatCompletionBuilder,
327}
328
329impl SyncChatCompletionBuilder {
330 pub fn messages(mut self, messages: Vec<Message>) -> Self {
332 self.inner = self.inner.messages(messages);
333 self
334 }
335
336 pub fn message(mut self, message: Message) -> Self {
338 self.inner = self.inner.message(message);
339 self
340 }
341
342 pub fn tools(mut self, tools: Vec<Tool>) -> Self {
344 self.inner = self.inner.tools(tools);
345 self
346 }
347
348 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
350 self.inner = self.inner.tool_choice(choice);
351 self
352 }
353
354 pub fn temperature(mut self, temperature: f32) -> Self {
356 self.inner = self.inner.temperature(temperature);
357 self
358 }
359
360 pub fn top_p(mut self, top_p: f32) -> Self {
362 self.inner = self.inner.top_p(top_p);
363 self
364 }
365
366 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
368 self.inner = self.inner.max_tokens(max_tokens);
369 self
370 }
371
372 pub fn response_format(mut self, format: ResponseFormat) -> Self {
374 self.inner = self.inner.response_format(format);
375 self
376 }
377
378 pub fn n(mut self, n: u32) -> Self {
380 self.inner = self.inner.n(n);
381 self
382 }
383
384 pub fn stop(mut self, stop: Vec<String>) -> Self {
386 self.inner = self.inner.stop(stop);
387 self
388 }
389
390 pub fn presence_penalty(mut self, penalty: f32) -> Self {
392 self.inner = self.inner.presence_penalty(penalty);
393 self
394 }
395
396 pub fn frequency_penalty(mut self, penalty: f32) -> Self {
398 self.inner = self.inner.frequency_penalty(penalty);
399 self
400 }
401
402 pub fn send(self) -> Result<ChatCompletion> {
404 self.client.run(self.inner.send())
405 }
406}
407
408#[derive(Debug, Clone)]
410pub struct SyncImagesApi {
411 client: SyncXaiClient,
412}
413
414impl SyncImagesApi {
415 pub fn generate(
417 &self,
418 model: impl Into<String>,
419 prompt: impl Into<String>,
420 ) -> SyncImageGenerationBuilder {
421 SyncImageGenerationBuilder {
422 client: self.client.clone(),
423 inner: self.client.client.images().generate(model, prompt),
424 }
425 }
426}
427
428#[derive(Debug)]
430pub struct SyncImageGenerationBuilder {
431 client: SyncXaiClient,
432 inner: ImageGenerationBuilder,
433}
434
435impl SyncImageGenerationBuilder {
436 pub fn n(mut self, n: u8) -> Self {
438 self.inner = self.inner.n(n);
439 self
440 }
441
442 pub fn response_format(mut self, format: ImageResponseFormat) -> Self {
444 self.inner = self.inner.response_format(format);
445 self
446 }
447
448 pub fn url_format(mut self) -> Self {
450 self.inner = self.inner.url_format();
451 self
452 }
453
454 pub fn base64_format(mut self) -> Self {
456 self.inner = self.inner.base64_format();
457 self
458 }
459
460 pub fn send(self) -> Result<ImageGenerationResponse> {
462 self.client.run(self.inner.send())
463 }
464}
465
466#[derive(Debug, Clone)]
468pub struct SyncBatchApi {
469 client: SyncXaiClient,
470}
471
472impl SyncBatchApi {
473 pub fn create(&self, name: impl Into<String>) -> Result<Batch> {
475 self.client.run(self.client.client.batch().create(name))
476 }
477
478 pub fn get(&self, batch_id: impl AsRef<str>) -> Result<Batch> {
480 self.client.run(self.client.client.batch().get(batch_id))
481 }
482
483 pub fn list(&self) -> Result<BatchListResponse> {
485 self.client.run(self.client.client.batch().list())
486 }
487
488 pub fn list_with_options(
490 &self,
491 limit: Option<u32>,
492 next_token: Option<&str>,
493 ) -> Result<BatchListResponse> {
494 self.client.run(
495 self.client
496 .client
497 .batch()
498 .list_with_options(limit, next_token),
499 )
500 }
501
502 pub fn cancel(&self, batch_id: impl AsRef<str>) -> Result<Batch> {
504 self.client.run(self.client.client.batch().cancel(batch_id))
505 }
506
507 pub fn add_requests(
509 &self,
510 batch_id: impl AsRef<str>,
511 requests: Vec<BatchRequest>,
512 ) -> Result<()> {
513 self.client
514 .run(self.client.client.batch().add_requests(batch_id, requests))
515 }
516
517 pub fn list_requests(&self, batch_id: impl AsRef<str>) -> Result<BatchRequestListResponse> {
519 self.client
520 .run(self.client.client.batch().list_requests(batch_id))
521 }
522
523 pub fn list_requests_with_options(
525 &self,
526 batch_id: impl AsRef<str>,
527 limit: Option<u32>,
528 next_token: Option<&str>,
529 ) -> Result<BatchRequestListResponse> {
530 self.client.run(
531 self.client
532 .client
533 .batch()
534 .list_requests_with_options(batch_id, limit, next_token),
535 )
536 }
537
538 pub fn list_results(&self, batch_id: impl AsRef<str>) -> Result<BatchResultListResponse> {
540 self.client
541 .run(self.client.client.batch().list_results(batch_id))
542 }
543
544 pub fn list_results_with_options(
546 &self,
547 batch_id: impl AsRef<str>,
548 limit: Option<u32>,
549 next_token: Option<&str>,
550 ) -> Result<BatchResultListResponse> {
551 self.client.run(
552 self.client
553 .client
554 .batch()
555 .list_results_with_options(batch_id, limit, next_token),
556 )
557 }
558
559 pub fn get_result(
561 &self,
562 batch_id: impl AsRef<str>,
563 request_id: impl AsRef<str>,
564 ) -> Result<BatchResult> {
565 self.client
566 .run(self.client.client.batch().get_result(batch_id, request_id))
567 }
568}
569
570#[derive(Debug, Clone)]
572pub struct SyncCollectionsApi {
573 client: SyncXaiClient,
574}
575
576impl SyncCollectionsApi {
577 pub fn create(&self, request: CreateCollectionRequest) -> Result<Collection> {
579 self.client
580 .run(self.client.client.collections().create(request))
581 }
582
583 pub fn create_named(&self, name: impl Into<String>) -> Result<Collection> {
585 self.client
586 .run(self.client.client.collections().create_named(name))
587 }
588
589 pub fn get(&self, collection_id: impl AsRef<str>) -> Result<Collection> {
591 self.client
592 .run(self.client.client.collections().get(collection_id))
593 }
594
595 pub fn list(&self) -> Result<CollectionListResponse> {
597 self.client.run(self.client.client.collections().list())
598 }
599
600 pub fn list_with_options(
602 &self,
603 limit: Option<u32>,
604 next_token: Option<&str>,
605 ) -> Result<CollectionListResponse> {
606 self.client.run(
607 self.client
608 .client
609 .collections()
610 .list_with_options(limit, next_token),
611 )
612 }
613
614 pub fn delete(&self, collection_id: impl AsRef<str>) -> Result<()> {
616 self.client
617 .run(self.client.client.collections().delete(collection_id))
618 }
619
620 pub fn add_documents(
622 &self,
623 collection_id: impl AsRef<str>,
624 documents: Vec<Document>,
625 ) -> Result<AddDocumentsResponse> {
626 self.client.run(
627 self.client
628 .client
629 .collections()
630 .add_documents(collection_id, documents),
631 )
632 }
633
634 pub fn add_document(
636 &self,
637 collection_id: impl AsRef<str>,
638 document: Document,
639 ) -> Result<String> {
640 self.client.run(
641 self.client
642 .client
643 .collections()
644 .add_document(collection_id, document),
645 )
646 }
647
648 pub fn update(
650 &self,
651 collection_id: impl AsRef<str>,
652 request: UpdateCollectionRequest,
653 ) -> Result<Collection> {
654 self.client.run(
655 self.client
656 .client
657 .collections()
658 .update(collection_id, request),
659 )
660 }
661
662 pub fn upsert_document(
664 &self,
665 collection_id: impl AsRef<str>,
666 document: Document,
667 ) -> Result<Document> {
668 self.client.run(
669 self.client
670 .client
671 .collections()
672 .upsert_document(collection_id, document),
673 )
674 }
675
676 pub fn batch_get_documents(
678 &self,
679 collection_id: impl AsRef<str>,
680 request: BatchGetDocumentsRequest,
681 ) -> Result<DocumentListResponse> {
682 self.client.run(
683 self.client
684 .client
685 .collections()
686 .batch_get_documents(collection_id, request),
687 )
688 }
689
690 pub fn list_documents(&self, collection_id: impl AsRef<str>) -> Result<DocumentListResponse> {
692 self.client.run(
693 self.client
694 .client
695 .collections()
696 .list_documents(collection_id),
697 )
698 }
699
700 pub fn list_documents_with_options(
702 &self,
703 collection_id: impl AsRef<str>,
704 limit: Option<u32>,
705 next_token: Option<&str>,
706 ) -> Result<DocumentListResponse> {
707 self.client.run(
708 self.client
709 .client
710 .collections()
711 .list_documents_with_options(collection_id, limit, next_token),
712 )
713 }
714
715 pub fn get_document(
717 &self,
718 collection_id: impl AsRef<str>,
719 document_id: impl AsRef<str>,
720 ) -> Result<Document> {
721 self.client.run(
722 self.client
723 .client
724 .collections()
725 .get_document(collection_id, document_id),
726 )
727 }
728
729 pub fn delete_document(
731 &self,
732 collection_id: impl AsRef<str>,
733 document_id: impl AsRef<str>,
734 ) -> Result<()> {
735 self.client.run(
736 self.client
737 .client
738 .collections()
739 .delete_document(collection_id, document_id),
740 )
741 }
742
743 pub fn search(
745 &self,
746 collection_id: impl AsRef<str>,
747 request: SearchRequest,
748 ) -> Result<SearchResponse> {
749 self.client.run(
750 self.client
751 .client
752 .collections()
753 .search(collection_id, request),
754 )
755 }
756
757 pub fn search_query(
759 &self,
760 collection_id: impl AsRef<str>,
761 query: impl Into<String>,
762 ) -> Result<SearchResponse> {
763 self.client.run(
764 self.client
765 .client
766 .collections()
767 .search_query(collection_id, query),
768 )
769 }
770}
771
772#[cfg(feature = "files")]
774#[derive(Debug, Clone)]
775pub struct SyncFilesApi {
776 client: SyncXaiClient,
777}
778
779#[cfg(feature = "files")]
780impl SyncFilesApi {
781 pub fn list(&self) -> Result<FileListResponse> {
783 self.client.run(self.client.client.files().list())
784 }
785
786 pub fn get(&self, file_id: &str) -> Result<FileObject> {
788 self.client.run(self.client.client.files().get(file_id))
789 }
790
791 pub fn content(&self, file_id: &str) -> Result<Vec<u8>> {
793 self.client.run(self.client.client.files().content(file_id))
794 }
795
796 pub fn delete(&self, file_id: &str) -> Result<DeleteFileResponse> {
798 self.client.run(self.client.client.files().delete(file_id))
799 }
800
801 pub fn upload(&self, filename: impl Into<String>, data: Vec<u8>) -> SyncUploadFileBuilder {
803 SyncUploadFileBuilder {
804 client: self.client.clone(),
805 inner: self.client.client.files().upload(filename, data),
806 }
807 }
808
809 pub fn download(&self, request: FileDownloadRequest) -> Result<Vec<u8>> {
811 self.client
812 .run(self.client.client.files().download(request))
813 }
814
815 pub fn initialize_upload(
817 &self,
818 request: FileUploadInitializeRequest,
819 ) -> Result<FileUploadInitializeResponse> {
820 self.client
821 .run(self.client.client.files().initialize_upload(request))
822 }
823
824 pub fn upload_chunks(
826 &self,
827 request: FileUploadChunksRequest,
828 ) -> Result<FileUploadChunksResponse> {
829 self.client
830 .run(self.client.client.files().upload_chunks(request))
831 }
832}
833
834#[cfg(feature = "files")]
836#[derive(Debug)]
837pub struct SyncUploadFileBuilder {
838 client: SyncXaiClient,
839 inner: UploadFileBuilder,
840}
841
842#[cfg(feature = "files")]
843impl SyncUploadFileBuilder {
844 pub fn purpose(mut self, purpose: FilePurpose) -> Self {
846 self.inner = self.inner.purpose(purpose);
847 self
848 }
849
850 pub fn send(self) -> Result<FileObject> {
852 self.client.run(self.inner.send())
853 }
854}
855
856#[derive(Debug)]
858pub struct SyncCreateResponseBuilder {
859 client: SyncXaiClient,
860 inner: CreateResponseBuilder,
861}
862
863impl SyncCreateResponseBuilder {
864 pub fn message(mut self, role: Role, content: impl Into<MessageContent>) -> Self {
866 self.inner = self.inner.message(role, content);
867 self
868 }
869
870 pub fn system(mut self, content: impl Into<String>) -> Self {
872 self.inner = self.inner.system(content);
873 self
874 }
875
876 pub fn user(mut self, content: impl Into<MessageContent>) -> Self {
878 self.inner = self.inner.user(content);
879 self
880 }
881
882 pub fn assistant(mut self, content: impl Into<String>) -> Self {
884 self.inner = self.inner.assistant(content);
885 self
886 }
887
888 pub fn user_with_image(
890 mut self,
891 text: impl Into<String>,
892 image_url: impl Into<String>,
893 ) -> Self {
894 self.inner = self.inner.user_with_image(text, image_url);
895 self
896 }
897
898 pub fn messages(mut self, messages: Vec<Message>) -> Self {
900 self.inner = self.inner.messages(messages);
901 self
902 }
903
904 pub fn tool(mut self, tool: Tool) -> Self {
906 self.inner = self.inner.tool(tool);
907 self
908 }
909
910 pub fn tools(mut self, tools: Vec<Tool>) -> Self {
912 self.inner = self.inner.tools(tools);
913 self
914 }
915
916 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
918 self.inner = self.inner.tool_choice(choice);
919 self
920 }
921
922 pub fn temperature(mut self, temperature: f32) -> Self {
924 self.inner = self.inner.temperature(temperature);
925 self
926 }
927
928 pub fn top_p(mut self, top_p: f32) -> Self {
930 self.inner = self.inner.top_p(top_p);
931 self
932 }
933
934 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
936 self.inner = self.inner.max_tokens(max_tokens);
937 self
938 }
939
940 pub fn max_retries(mut self, max_retries: u32) -> Self {
942 self.inner = self.inner.max_retries(max_retries);
943 self
944 }
945
946 pub fn disable_retries(mut self) -> Self {
948 self.inner = self.inner.disable_retries();
949 self
950 }
951
952 pub fn retry_backoff(mut self, initial: Duration, max: Duration) -> Self {
954 self.inner = self.inner.retry_backoff(initial, max);
955 self
956 }
957
958 pub fn retry_jitter(mut self, factor: f64) -> Self {
960 self.inner = self.inner.retry_jitter(factor);
961 self
962 }
963
964 pub fn response_format(mut self, format: ResponseFormat) -> Self {
966 self.inner = self.inner.response_format(format);
967 self
968 }
969
970 pub fn json_output(mut self) -> Self {
972 self.inner = self.inner.json_output();
973 self
974 }
975
976 pub fn include(mut self, fields: Vec<String>) -> Self {
978 self.inner = self.inner.include(fields);
979 self
980 }
981
982 pub fn with_inline_citations(mut self) -> Self {
984 self.inner = self.inner.with_inline_citations();
985 self
986 }
987
988 pub fn with_verbose_streaming(mut self) -> Self {
990 self.inner = self.inner.with_verbose_streaming();
991 self
992 }
993
994 pub fn store(mut self, store: bool) -> Self {
996 self.inner = self.inner.store(store);
997 self
998 }
999
1000 pub fn send(self) -> Result<Response> {
1002 self.client.run(self.inner.send())
1003 }
1004}
1005
1006#[derive(Debug, Clone)]
1008pub struct SyncDeferredResponsePoller {
1009 client: SyncXaiClient,
1010 inner: DeferredResponsePoller,
1011}
1012
1013impl SyncDeferredResponsePoller {
1014 pub fn max_attempts(mut self, max_attempts: u32) -> Self {
1016 self.inner = self.inner.max_attempts(max_attempts);
1017 self
1018 }
1019
1020 pub fn poll_interval(mut self, interval: Duration) -> Self {
1022 self.inner = self.inner.poll_interval(interval);
1023 self
1024 }
1025
1026 pub fn poll_backoff(mut self, initial: Duration, max: Duration) -> Self {
1028 self.inner = self.inner.poll_backoff(initial, max);
1029 self
1030 }
1031
1032 pub fn wait(self) -> Result<Response> {
1034 self.client.run(self.inner.wait())
1035 }
1036}
1037
1038#[derive(Debug, Clone)]
1040pub struct SyncStatefulChat {
1041 client: SyncXaiClient,
1042 inner: StatefulChat,
1043}
1044
1045impl SyncStatefulChat {
1046 pub fn append(&mut self, role: Role, content: impl Into<MessageContent>) -> &mut Self {
1048 self.inner.append(role, content);
1049 self
1050 }
1051
1052 pub fn append_system(&mut self, content: impl Into<String>) -> &mut Self {
1054 self.inner.append_system(content);
1055 self
1056 }
1057
1058 pub fn append_user(&mut self, content: impl Into<MessageContent>) -> &mut Self {
1060 self.inner.append_user(content);
1061 self
1062 }
1063
1064 pub fn append_assistant(&mut self, content: impl Into<String>) -> &mut Self {
1066 self.inner.append_assistant(content);
1067 self
1068 }
1069
1070 pub fn append_tool_result(
1072 &mut self,
1073 tool_call_id: impl Into<String>,
1074 content: impl Into<String>,
1075 ) -> &mut Self {
1076 self.inner.append_tool_result(tool_call_id, content);
1077 self
1078 }
1079
1080 pub fn append_message(&mut self, message: Message) -> &mut Self {
1082 self.inner.append_message(message);
1083 self
1084 }
1085
1086 pub fn messages(&self) -> &[Message] {
1088 self.inner.messages()
1089 }
1090
1091 pub fn pending_tool_calls(&self) -> &[ToolCall] {
1093 self.inner.pending_tool_calls()
1094 }
1095
1096 pub fn take_pending_tool_calls(&mut self) -> Vec<ToolCall> {
1098 self.inner.take_pending_tool_calls()
1099 }
1100
1101 pub fn clear(&mut self) -> &mut Self {
1103 self.inner.clear();
1104 self
1105 }
1106
1107 pub fn sample(&self) -> Result<Response> {
1109 self.client.run(self.inner.sample())
1110 }
1111
1112 pub fn append_response_text(&mut self, response: &Response) -> &mut Self {
1114 self.inner.append_response_text(response);
1115 self
1116 }
1117
1118 pub fn append_response_semantics(&mut self, response: &Response) -> &mut Self {
1120 self.inner.append_response_semantics(response);
1121 self
1122 }
1123
1124 pub fn sample_and_append(&mut self) -> Result<Response> {
1126 self.client.run(self.inner.sample_and_append())
1127 }
1128
1129 pub fn sample_with_tool_loop<H>(&mut self, handler: H) -> Result<Response>
1131 where
1132 H: FnMut(ToolCall) -> Result<String>,
1133 {
1134 self.sample_with_tool_handler(DEFAULT_SYNC_TOOL_LOOP_MAX_ROUNDS, handler)
1135 }
1136
1137 pub fn sample_with_tool_handler<H>(
1139 &mut self,
1140 max_rounds: u32,
1141 mut handler: H,
1142 ) -> Result<Response>
1143 where
1144 H: FnMut(ToolCall) -> Result<String>,
1145 {
1146 self.client.run(
1147 self.inner
1148 .sample_with_tool_handler(max_rounds, move |call| {
1149 let next = handler(call);
1150 async move { next }
1151 }),
1152 )
1153 }
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use serde_json::json;
1160 use std::env;
1161 use std::sync::atomic::{AtomicUsize, Ordering};
1162 use wiremock::matchers::{body_partial_json, method, path};
1163 use wiremock::{Mock, MockServer, ResponseTemplate};
1164
1165 #[test]
1166 fn sync_responses_create_send_returns_text() {
1167 let server_rt = tokio::runtime::Runtime::new().unwrap();
1168 let server = server_rt.block_on(MockServer::start());
1169
1170 server_rt.block_on(async {
1171 Mock::given(method("POST"))
1172 .and(path("/responses"))
1173 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1174 "id": "resp_sync",
1175 "model": "grok-4",
1176 "output": [{
1177 "type": "message",
1178 "role": "assistant",
1179 "content": [{"type": "text", "text": "sync hello"}]
1180 }],
1181 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1182 })))
1183 .mount(&server)
1184 .await;
1185 });
1186
1187 let async_client = XaiClient::builder()
1188 .api_key("test-key")
1189 .base_url(server.uri())
1190 .build()
1191 .unwrap();
1192
1193 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1194 let response = client
1195 .responses()
1196 .create("grok-4")
1197 .user("hello")
1198 .send()
1199 .unwrap();
1200
1201 assert_eq!(response.output_text().as_deref(), Some("sync hello"));
1202 }
1203
1204 #[test]
1205 fn sync_responses_create_builder_forwards_extended_request_fields() {
1206 let server_rt = tokio::runtime::Runtime::new().unwrap();
1207 let server = server_rt.block_on(MockServer::start());
1208
1209 server_rt.block_on(async {
1210 Mock::given(method("POST"))
1211 .and(path("/responses"))
1212 .respond_with(move |req: &wiremock::Request| {
1213 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
1214
1215 assert_eq!(body["model"], "grok-4");
1216 assert_eq!(body["input"].as_array().unwrap().len(), 4);
1217 assert_eq!(body["input"][0]["role"], "system");
1218 assert_eq!(body["input"][1]["role"], "user");
1219 assert_eq!(body["input"][2]["role"], "assistant");
1220 assert_eq!(body["input"][3]["role"], "user");
1221 assert_eq!(body["input"][3]["content"][0]["type"], "text");
1222 assert_eq!(body["input"][3]["content"][1]["type"], "image_url");
1223 assert_eq!(body["top_p"], 1.0);
1224 assert_eq!(body["temperature"], 2.0);
1225 assert_eq!(body["max_tokens"], 64);
1226 assert_eq!(body["tool_choice"]["function"]["name"], "get_weather");
1227 assert_eq!(body["tools"].as_array().unwrap().len(), 2);
1228 assert_eq!(body["response_format"]["type"], "json_object");
1229 assert_eq!(body["store"], false);
1230 assert_eq!(
1231 body["include"],
1232 serde_json::json!(["custom", "inline_citations", "verbose_streaming"])
1233 );
1234
1235 ResponseTemplate::new(200).set_body_json(json!({
1236 "id": "resp_sync_extended",
1237 "model": "grok-4",
1238 "output": [{
1239 "type": "message",
1240 "role": "assistant",
1241 "content": [{"type": "text", "text": "extended sync"}]
1242 }],
1243 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1244 }))
1245 })
1246 .mount(&server)
1247 .await;
1248 });
1249
1250 let weather_tool = Tool::function(
1251 "get_weather",
1252 "Get weather",
1253 serde_json::json!({
1254 "type": "object",
1255 "properties": {"location": {"type": "string"}}
1256 }),
1257 );
1258 let map_tool = Tool::function(
1259 "get_map",
1260 "Get map",
1261 serde_json::json!({
1262 "type": "object",
1263 "properties": {"where": {"type": "string"}}
1264 }),
1265 );
1266
1267 let async_client = XaiClient::builder()
1268 .api_key("test-key")
1269 .base_url(server.uri())
1270 .build()
1271 .unwrap();
1272
1273 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1274 let response = client
1275 .responses()
1276 .create("grok-4")
1277 .system("You are a concise assistant.")
1278 .user("Hello")
1279 .assistant("Hi")
1280 .user_with_image("Look", "https://example.com/image.jpg")
1281 .tool(weather_tool)
1282 .tools(vec![map_tool])
1283 .tool_choice(ToolChoice::function("get_weather"))
1284 .temperature(2.4)
1285 .top_p(1.2)
1286 .max_tokens(64)
1287 .max_retries(2)
1288 .retry_backoff(Duration::from_millis(1), Duration::from_millis(2))
1289 .retry_jitter(0.25)
1290 .response_format(ResponseFormat::json_object())
1291 .json_output()
1292 .include(vec!["custom".to_string()])
1293 .with_inline_citations()
1294 .with_verbose_streaming()
1295 .store(false)
1296 .send()
1297 .unwrap();
1298
1299 assert_eq!(response.output_text().as_deref(), Some("extended sync"));
1300 }
1301
1302 #[test]
1303 fn sync_responses_create_builder_retry_controls_are_honored() {
1304 let server_rt = tokio::runtime::Runtime::new().unwrap();
1305 let server = server_rt.block_on(MockServer::start());
1306 let attempts = Arc::new(AtomicUsize::new(0));
1307 let attempts_for_responder = Arc::clone(&attempts);
1308
1309 server_rt.block_on(async {
1310 Mock::given(method("POST"))
1311 .and(path("/responses"))
1312 .respond_with(move |_req: &wiremock::Request| {
1313 let count = attempts_for_responder.fetch_add(1, Ordering::SeqCst);
1314 if count == 0 {
1315 ResponseTemplate::new(503).set_body_json(json!({
1316 "error": {"message": "temporary", "type": "server_error"}
1317 }))
1318 } else {
1319 ResponseTemplate::new(200).set_body_json(json!({
1320 "id": "resp_sync_retry",
1321 "model": "grok-4",
1322 "output": [{
1323 "type": "message",
1324 "role": "assistant",
1325 "content": [{"type": "text", "text": "retried"}]
1326 }],
1327 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1328 }))
1329 }
1330 })
1331 .mount(&server)
1332 .await;
1333 });
1334
1335 let async_client = XaiClient::builder()
1336 .api_key("test-key")
1337 .base_url(server.uri())
1338 .disable_retries()
1339 .retry_backoff(Duration::from_millis(0), Duration::from_millis(0))
1340 .build()
1341 .unwrap();
1342
1343 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1344 let response = client
1345 .responses()
1346 .create("grok-4")
1347 .user("hi")
1348 .max_retries(1)
1349 .retry_jitter(2.0)
1350 .send()
1351 .unwrap();
1352
1353 assert_eq!(response.output_text().as_deref(), Some("retried"));
1354 assert_eq!(attempts.load(Ordering::SeqCst), 2);
1355 }
1356
1357 #[test]
1358 fn sync_stateful_chat_builder_operations() {
1359 let server_rt = tokio::runtime::Runtime::new().unwrap();
1360 let server = server_rt.block_on(MockServer::start());
1361 let sample_calls = Arc::new(AtomicUsize::new(0));
1362 let sample_calls_for_responder = Arc::clone(&sample_calls);
1363
1364 server_rt.block_on(async {
1365 Mock::given(method("POST"))
1366 .and(path("/responses"))
1367 .and(body_partial_json(json!({"model": "grok-4"})))
1368 .respond_with(move |_req: &wiremock::Request| {
1369 let call_count = sample_calls_for_responder.fetch_add(1, Ordering::SeqCst);
1370 if call_count == 0 {
1371 ResponseTemplate::new(200).set_body_json(json!({
1372 "id": "resp_sync_state",
1373 "model": "grok-4",
1374 "output": [{
1375 "type": "message",
1376 "role": "assistant",
1377 "content": [{"type": "text", "text": "first"}]
1378 }],
1379 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1380 }))
1381 } else {
1382 ResponseTemplate::new(200).set_body_json(json!({
1383 "id": "resp_sync_state_tool",
1384 "model": "grok-4",
1385 "output": [{
1386 "type": "function_call",
1387 "id": "tool_1",
1388 "function": {
1389 "name": "echo",
1390 "arguments": "{\"text\":\"value\"}"
1391 }
1392 }],
1393 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1394 }))
1395 }
1396 })
1397 .mount(&server)
1398 .await;
1399 });
1400
1401 let async_client = XaiClient::builder()
1402 .api_key("test-key")
1403 .base_url(server.uri())
1404 .build()
1405 .unwrap();
1406
1407 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1408 let mut chat = client.responses().chat("grok-4");
1409
1410 chat.append_system("system prompt")
1411 .append_user("Hello")
1412 .append_assistant("seeded")
1413 .append_message(Message {
1414 role: Role::User,
1415 content: MessageContent::Text("injected".to_string()),
1416 name: None,
1417 tool_call_id: None,
1418 });
1419
1420 let first = chat.sample().unwrap();
1421 assert_eq!(first.output_text().as_deref(), Some("first"));
1422
1423 chat.append_response_text(&first);
1424 assert!(chat
1425 .messages()
1426 .iter()
1427 .any(|m| matches!(m.role, Role::Assistant)));
1428 assert!(chat.messages().len() >= 5);
1429
1430 let tool_response = chat.sample_and_append().unwrap();
1431 assert_eq!(tool_response.output_text().as_deref(), None);
1432 assert_eq!(chat.pending_tool_calls().len(), 1);
1433
1434 let pending = chat.take_pending_tool_calls();
1435 assert_eq!(pending.len(), 1);
1436 assert_eq!(pending[0].id, "tool_1");
1437
1438 chat.append_tool_result("tool_1", r#"{"text":"value"}"#);
1439 assert_eq!(chat.messages().len(), 6);
1440 assert_eq!(chat.messages().last().map(|m| &m.role), Some(&Role::Tool));
1441 }
1442
1443 #[test]
1444 fn sync_stateful_chat_tool_loop_runs_to_completion() {
1445 let server_rt = tokio::runtime::Runtime::new().unwrap();
1446 let server = server_rt.block_on(MockServer::start());
1447 let call_count = Arc::new(AtomicUsize::new(0));
1448 let call_count_for_responder = Arc::clone(&call_count);
1449
1450 server_rt.block_on(async {
1451 Mock::given(method("POST"))
1452 .and(path("/responses"))
1453 .respond_with(move |_req: &wiremock::Request| {
1454 let count = call_count_for_responder.fetch_add(1, Ordering::SeqCst);
1455 if count == 0 {
1456 ResponseTemplate::new(200).set_body_json(json!({
1457 "id": "resp_sync_loop_1",
1458 "model": "grok-4",
1459 "output": [{
1460 "type": "function_call",
1461 "id": "loop_tool",
1462 "function": {
1463 "name": "calc",
1464 "arguments": "{}"
1465 }
1466 }],
1467 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1468 }))
1469 } else {
1470 ResponseTemplate::new(200).set_body_json(json!( {
1471 "id": "resp_sync_loop_2",
1472 "model": "grok-4",
1473 "output": [{
1474 "type": "message",
1475 "role": "assistant",
1476 "content": [{"type": "text", "text": "looped"}]
1477 }],
1478 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1479 }))
1480 }
1481 })
1482 .mount(&server)
1483 .await;
1484 });
1485
1486 let async_client = XaiClient::builder()
1487 .api_key("test-key")
1488 .base_url(server.uri())
1489 .build()
1490 .unwrap();
1491
1492 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1493 let mut chat = client.responses().chat("grok-4");
1494 chat.append_system("loop test").append_user("run");
1495
1496 let response = chat
1497 .sample_with_tool_loop(|call| {
1498 if call.id == "loop_tool" {
1499 Ok(r#"{"result":"ok"}"#.to_string())
1500 } else {
1501 Ok("{}".to_string())
1502 }
1503 })
1504 .unwrap();
1505
1506 assert_eq!(response.output_text().as_deref(), Some("looped"));
1507 assert_eq!(call_count.load(Ordering::SeqCst), 2);
1508 assert_eq!(chat.messages().len(), 4);
1509 }
1510
1511 #[test]
1512 fn sync_deferred_response_poller_uses_polling_methods() {
1513 let server_rt = tokio::runtime::Runtime::new().unwrap();
1514 let server = server_rt.block_on(MockServer::start());
1515 let poll_count = Arc::new(AtomicUsize::new(0));
1516 let poll_count_clone = Arc::clone(&poll_count);
1517 let poll_backoff_count = Arc::clone(&poll_count);
1518
1519 server_rt.block_on(async {
1520 Mock::given(method("GET"))
1521 .and(path("/responses/resp_poll_interval"))
1522 .respond_with(move |_req: &wiremock::Request| {
1523 let count = poll_count_clone.fetch_add(1, Ordering::SeqCst);
1524 if count == 0 {
1525 ResponseTemplate::new(200).set_body_json(json!({
1526 "id": "resp_poll_interval",
1527 "model": "grok-4",
1528 "output": [],
1529 "usage": {"prompt_tokens": 1, "completion_tokens": 0, "total_tokens": 1}
1530 }))
1531 } else {
1532 ResponseTemplate::new(200).set_body_json(json!({
1533 "id": "resp_poll_interval",
1534 "model": "grok-4",
1535 "output": [{
1536 "type": "message",
1537 "role": "assistant",
1538 "content": [{"type": "text", "text": "done"}]
1539 }],
1540 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1541 }))
1542 }
1543 })
1544 .mount(&server)
1545 .await;
1546
1547 Mock::given(method("GET"))
1548 .and(path("/responses/resp_poll_backoff"))
1549 .respond_with(move |_req: &wiremock::Request| {
1550 let count = poll_backoff_count.fetch_add(1, Ordering::SeqCst);
1551 if count == 0 {
1552 ResponseTemplate::new(200).set_body_json(json!({
1553 "id": "resp_poll_backoff",
1554 "model": "grok-4",
1555 "output": [],
1556 "usage": {"prompt_tokens": 1, "completion_tokens": 0, "total_tokens": 1}
1557 }))
1558 } else {
1559 ResponseTemplate::new(200).set_body_json(json!({
1560 "id": "resp_poll_backoff",
1561 "model": "grok-4",
1562 "output": [{
1563 "type": "message",
1564 "role": "assistant",
1565 "content": [{"type": "text", "text": "ready"}]
1566 }],
1567 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1568 }))
1569 }
1570 })
1571 .mount(&server)
1572 .await;
1573 });
1574
1575 let async_client = XaiClient::builder()
1576 .api_key("test-key")
1577 .base_url(server.uri())
1578 .build()
1579 .unwrap();
1580
1581 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1582
1583 let poll_interval_response = client
1584 .responses()
1585 .deferred("resp_poll_interval")
1586 .max_attempts(3)
1587 .poll_interval(Duration::from_millis(0))
1588 .wait()
1589 .unwrap();
1590 assert_eq!(
1591 poll_interval_response.output_text().as_deref(),
1592 Some("done")
1593 );
1594
1595 let poll_backoff_response = client
1596 .responses()
1597 .deferred("resp_poll_backoff")
1598 .poll_backoff(Duration::from_millis(0), Duration::from_millis(0))
1599 .wait()
1600 .unwrap();
1601 assert_eq!(
1602 poll_backoff_response.output_text().as_deref(),
1603 Some("ready")
1604 );
1605 }
1606
1607 #[test]
1608 fn sync_client_new_and_from_env_cover_unused_entry_points() {
1609 let previous_key = env::var_os("XAI_API_KEY");
1610
1611 env::set_var("XAI_API_KEY", "sync-env-key");
1612 let from_env_client = SyncXaiClient::from_env().unwrap();
1613 let formatted = format!("{from_env_client:?}");
1614 assert!(formatted.contains("SyncXaiClient"));
1615 assert_eq!(from_env_client.base_url(), crate::config::DEFAULT_BASE_URL);
1616
1617 let new_client = SyncXaiClient::new("inline-test-key").unwrap();
1618 assert_eq!(new_client.base_url(), crate::config::DEFAULT_BASE_URL);
1619
1620 if let Some(previous_key) = previous_key {
1621 env::set_var("XAI_API_KEY", previous_key);
1622 } else {
1623 env::remove_var("XAI_API_KEY");
1624 }
1625 }
1626
1627 #[test]
1628 fn sync_models_and_tokenizer_wrappers_cover_missing_methods() {
1629 let server_rt = tokio::runtime::Runtime::new().unwrap();
1630 let server = server_rt.block_on(MockServer::start());
1631
1632 server_rt.block_on(async {
1633 Mock::given(method("GET"))
1634 .and(path("/models/grok-4"))
1635 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1636 "id": "grok-4",
1637 "object": "model",
1638 "owned_by": "xai"
1639 })))
1640 .mount(&server)
1641 .await;
1642
1643 Mock::given(method("POST"))
1644 .and(path("/tokenize-text"))
1645 .respond_with(move |req: &wiremock::Request| {
1646 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
1647 match body["text"].as_str().unwrap_or_default() {
1648 "sync tokenize" => ResponseTemplate::new(200).set_body_json(json!({
1649 "tokens": [9, 8, 7],
1650 "token_count": 3
1651 })),
1652 _ => ResponseTemplate::new(200).set_body_json(json!({
1653 "tokens": [1, 2],
1654 "token_count": 2
1655 })),
1656 }
1657 })
1658 .mount(&server)
1659 .await;
1660 });
1661
1662 let async_client = XaiClient::builder()
1663 .api_key("test-key")
1664 .base_url(server.uri())
1665 .build()
1666 .unwrap();
1667 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1668
1669 let model = client.models().get("grok-4").unwrap();
1670 assert_eq!(model.id, "grok-4");
1671
1672 let tokenized = client
1673 .tokenizer()
1674 .tokenize(TokenizeRequest::new("grok-4", "sync tokenize"))
1675 .unwrap();
1676 assert_eq!(tokenized.token_count, 3);
1677
1678 let tokenized_text = client
1679 .tokenizer()
1680 .tokenize_text("grok-4", "sync tokenize text")
1681 .unwrap();
1682 assert_eq!(tokenized_text.token_count, 2);
1683 }
1684
1685 #[test]
1686 fn sync_builder_wrappers_cover_uncovered_paths() {
1687 let server_rt = tokio::runtime::Runtime::new().unwrap();
1688 let server = server_rt.block_on(MockServer::start());
1689
1690 server_rt.block_on(async {
1691 Mock::given(method("POST"))
1692 .and(path("/responses"))
1693 .respond_with(move |req: &wiremock::Request| {
1694 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
1695 assert_eq!(body["input"][0]["role"], "user");
1696 assert_eq!(body["input"][1]["role"], "assistant");
1697 assert_eq!(body["response_format"]["type"], "json_object");
1698 ResponseTemplate::new(200).set_body_json(json!({
1699 "id": "resp_sync_wrappers",
1700 "model": "grok-4",
1701 "output": [{
1702 "type": "message",
1703 "role": "assistant",
1704 "content": [{"type": "text", "text": "wrapped"}]
1705 }],
1706 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
1707 }))
1708 })
1709 .mount(&server)
1710 .await;
1711
1712 Mock::given(method("POST"))
1713 .and(path("/chat/completions"))
1714 .respond_with(move |req: &wiremock::Request| {
1715 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
1716 assert_eq!(body["model"], "grok-4");
1717 assert_eq!(body["top_p"], 0.8);
1718 assert_eq!(body["response_format"]["type"], "json_object");
1719 assert_eq!(body["messages"].as_array().unwrap().len(), 2);
1720
1721 ResponseTemplate::new(200).set_body_json(json!( {
1722 "id": "chat_sync_wrappers",
1723 "object": "chat.completion",
1724 "created": 1700000000,
1725 "model": "grok-4",
1726 "choices": [{
1727 "index": 0,
1728 "message": {
1729 "role": "assistant",
1730 "content": "wrapped"
1731 },
1732 "finish_reason": "stop"
1733 }]
1734 }))
1735 })
1736 .mount(&server)
1737 .await;
1738
1739 Mock::given(method("POST"))
1740 .and(path("/images/generations"))
1741 .respond_with(move |req: &wiremock::Request| {
1742 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
1743 assert_eq!(body["response_format"], "url");
1744
1745 ResponseTemplate::new(200).set_body_json(json!({
1746 "created": 1700000000,
1747 "data": [{
1748 "url": "https://example.com/wrapped.png"
1749 }]
1750 }))
1751 })
1752 .mount(&server)
1753 .await;
1754 });
1755
1756 let async_client = XaiClient::builder()
1757 .api_key("test-key")
1758 .base_url(server.uri())
1759 .build()
1760 .unwrap();
1761 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1762
1763 client
1764 .responses()
1765 .create("grok-4")
1766 .message(Role::User, "hello")
1767 .messages(vec![Message::assistant("from messages")])
1768 .disable_retries()
1769 .response_format(ResponseFormat::json_object())
1770 .send()
1771 .unwrap();
1772
1773 client
1774 .chat()
1775 .create("grok-4")
1776 .messages(vec![Message::system("system"), Message::user("hello")])
1777 .top_p(0.8)
1778 .response_format(ResponseFormat::json_object())
1779 .send()
1780 .unwrap();
1781
1782 client
1783 .images()
1784 .generate("grok-2-image", "bridge at dusk")
1785 .response_format(ImageResponseFormat::B64Json)
1786 .url_format()
1787 .send()
1788 .unwrap();
1789 }
1790
1791 #[test]
1792 fn sync_batch_and_collection_wrappers_cover_remaining_paths() {
1793 let server_rt = tokio::runtime::Runtime::new().unwrap();
1794 let server = server_rt.block_on(MockServer::start());
1795
1796 server_rt.block_on(async {
1797 Mock::given(method("POST"))
1798 .and(path("/batches/batch_sync:cancel"))
1799 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1800 "id": "batch_sync",
1801 "name": "wrapped-batch",
1802 "status": "cancelled",
1803 "request_count": 0,
1804 "completed_count": 0,
1805 "failed_count": 0
1806 })))
1807 .mount(&server)
1808 .await;
1809
1810 Mock::given(method("POST"))
1811 .and(path("/batches/batch_sync/requests"))
1812 .respond_with(ResponseTemplate::new(200))
1813 .mount(&server)
1814 .await;
1815
1816 Mock::given(method("GET"))
1817 .and(path("/batches/batch_sync/requests"))
1818 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1819 "data": [{
1820 "id": "batch_req_1",
1821 "custom_id": "custom-1",
1822 "status": "completed"
1823 }],
1824 "next_token": "tok_1"
1825 })))
1826 .mount(&server)
1827 .await;
1828
1829 Mock::given(method("GET"))
1830 .and(path("/batches/batch_sync/results"))
1831 .respond_with(move |req: &wiremock::Request| {
1832 let query = req.url.query().unwrap_or("");
1833 if query.is_empty() {
1834 ResponseTemplate::new(200).set_body_json(json!({
1835 "data": [{
1836 "batch_request_id": "br_1",
1837 "custom_id": "custom-1",
1838 "error_code": 0,
1839 "response": {
1840 "id": "resp_1",
1841 "model": "grok-4",
1842 "output": [{
1843 "type": "message",
1844 "role": "assistant",
1845 "content": [{"type": "text", "text": "result"}]
1846 }]
1847 }
1848 }]
1849 }))
1850 } else {
1851 ResponseTemplate::new(200).set_body_json(json!({
1852 "data": [],
1853 "next_token": "tok_2"
1854 }))
1855 }
1856 })
1857 .mount(&server)
1858 .await;
1859
1860 Mock::given(method("GET"))
1861 .and(path("/collections"))
1862 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1863 "data": [{
1864 "id": "col_sync",
1865 "name": "Wrapped collection",
1866 "document_count": 2
1867 }]
1868 })))
1869 .mount(&server)
1870 .await;
1871
1872 Mock::given(method("GET"))
1873 .and(path("/collections/col_sync"))
1874 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1875 "id": "col_sync",
1876 "name": "Wrapped collection",
1877 "document_count": 2
1878 })))
1879 .mount(&server)
1880 .await;
1881
1882 Mock::given(method("POST"))
1883 .and(path("/collections/col_sync/documents"))
1884 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1885 "ids": ["doc_1", "doc_2"]
1886 })))
1887 .mount(&server)
1888 .await;
1889
1890 Mock::given(method("GET"))
1891 .and(path("/collections/col_sync/documents"))
1892 .respond_with(move |req: &wiremock::Request| {
1893 assert_eq!(req.url.query(), Some("limit=2&next_token=tok_doc"));
1894 ResponseTemplate::new(200).set_body_json(json!({
1895 "data": [
1896 {"id": "doc_1", "content": "seeded"}
1897 ]
1898 }))
1899 })
1900 .mount(&server)
1901 .await;
1902 });
1903
1904 let async_client = XaiClient::builder()
1905 .api_key("test-key")
1906 .base_url(server.uri())
1907 .build()
1908 .unwrap();
1909 let client = SyncXaiClient::with_async_client(async_client).unwrap();
1910
1911 let request =
1912 BatchRequest::new("custom-1", "grok-4").message(Message::user("batch payload"));
1913 client
1914 .batch()
1915 .add_requests("batch_sync", vec![request])
1916 .unwrap();
1917
1918 let cancelled = client.batch().cancel("batch_sync").unwrap();
1919 assert_eq!(
1920 cancelled.status,
1921 crate::models::batch::BatchStatus::Cancelled
1922 );
1923 assert_eq!(
1924 client
1925 .batch()
1926 .list_requests("batch_sync")
1927 .unwrap()
1928 .data
1929 .len(),
1930 1
1931 );
1932
1933 assert!(!client
1934 .batch()
1935 .list_results("batch_sync")
1936 .unwrap()
1937 .data
1938 .is_empty());
1939 assert_eq!(
1940 client
1941 .batch()
1942 .list_results_with_options("batch_sync", Some(2), Some("tok_2"))
1943 .unwrap()
1944 .next_token
1945 .as_deref(),
1946 Some("tok_2")
1947 );
1948
1949 let collection = client.collections().get("col_sync").unwrap();
1950 assert_eq!(collection.id, "col_sync");
1951 assert_eq!(client.collections().list().unwrap().data.len(), 1);
1952
1953 let added_documents = client
1954 .collections()
1955 .add_documents("col_sync", vec![Document::new("seeded")])
1956 .unwrap();
1957 assert_eq!(added_documents.ids[0], "doc_1");
1958
1959 let documents = client
1960 .collections()
1961 .list_documents_with_options("col_sync", Some(2), Some("tok_doc"))
1962 .unwrap();
1963 assert_eq!(documents.data[0].id.as_deref(), Some("doc_1"));
1964 }
1965
1966 #[test]
1967 fn sync_stateful_chat_append_helpers_cover_uncovered_methods() {
1968 let client = SyncXaiClient::new("test-key").unwrap();
1969 let mut chat = client.responses().chat("grok-4");
1970
1971 chat.append(Role::System, "system context");
1972 chat.append(Role::User, "hello");
1973 assert_eq!(chat.messages().len(), 2);
1974
1975 let sample_response: Response = serde_json::from_value(json!({
1976 "id": "resp_semantic",
1977 "model": "grok-4",
1978 "output": [{
1979 "type": "message",
1980 "role": "assistant",
1981 "content": [{"type": "text", "text": "semantic reply"}]
1982 }]
1983 }))
1984 .unwrap();
1985
1986 chat.append_response_semantics(&sample_response);
1987 assert_eq!(
1988 chat.messages().last().map(|m| &m.role),
1989 Some(&Role::Assistant)
1990 );
1991 chat.clear();
1992 assert!(chat.messages().is_empty());
1993 }
1994
1995 #[test]
1996 fn sync_models_list_returns_models() {
1997 let server_rt = tokio::runtime::Runtime::new().unwrap();
1998 let server = server_rt.block_on(MockServer::start());
1999
2000 server_rt.block_on(async {
2001 Mock::given(method("GET"))
2002 .and(path("/models"))
2003 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2004 "object": "list",
2005 "data": [
2006 {"id": "grok-4", "object": "model", "owned_by": "xai"}
2007 ]
2008 })))
2009 .mount(&server)
2010 .await;
2011 });
2012
2013 let async_client = XaiClient::builder()
2014 .api_key("test-key")
2015 .base_url(server.uri())
2016 .build()
2017 .unwrap();
2018
2019 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2020 let models = client.models().list().unwrap();
2021
2022 assert_eq!(models.object, "list");
2023 assert_eq!(models.data.len(), 1);
2024 assert_eq!(models.data[0].id, "grok-4");
2025 }
2026
2027 #[test]
2028 fn sync_tokenizer_count_tokens_returns_count() {
2029 let server_rt = tokio::runtime::Runtime::new().unwrap();
2030 let server = server_rt.block_on(MockServer::start());
2031
2032 server_rt.block_on(async {
2033 Mock::given(method("POST"))
2034 .and(path("/tokenize-text"))
2035 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2036 "tokens": [1, 2, 3]
2037 })))
2038 .mount(&server)
2039 .await;
2040 });
2041
2042 let async_client = XaiClient::builder()
2043 .api_key("test-key")
2044 .base_url(server.uri())
2045 .build()
2046 .unwrap();
2047
2048 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2049 let count = client
2050 .tokenizer()
2051 .count_tokens("grok-4", "count these tokens")
2052 .unwrap();
2053
2054 assert_eq!(count, 3);
2055 }
2056
2057 #[test]
2058 fn sync_chat_create_send_returns_text() {
2059 let server_rt = tokio::runtime::Runtime::new().unwrap();
2060 let server = server_rt.block_on(MockServer::start());
2061
2062 server_rt.block_on(async {
2063 Mock::given(method("POST"))
2064 .and(path("/chat/completions"))
2065 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2066 "id": "chatcmpl_sync",
2067 "object": "chat.completion",
2068 "created": 1700000000,
2069 "model": "grok-4",
2070 "choices": [{
2071 "index": 0,
2072 "message": {
2073 "role": "assistant",
2074 "content": "legacy sync hello"
2075 },
2076 "finish_reason": "stop"
2077 }]
2078 })))
2079 .mount(&server)
2080 .await;
2081 });
2082
2083 let async_client = XaiClient::builder()
2084 .api_key("test-key")
2085 .base_url(server.uri())
2086 .build()
2087 .unwrap();
2088
2089 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2090 let completion = client
2091 .chat()
2092 .create("grok-4")
2093 .message(Message::user("hello"))
2094 .send()
2095 .unwrap();
2096
2097 assert_eq!(completion.text(), Some("legacy sync hello"));
2098 }
2099
2100 #[test]
2101 fn sync_chat_create_forwards_options_and_tools() {
2102 let server_rt = tokio::runtime::Runtime::new().unwrap();
2103 let server = server_rt.block_on(MockServer::start());
2104
2105 server_rt.block_on(async {
2106 Mock::given(method("POST"))
2107 .and(path("/chat/completions"))
2108 .respond_with(move |req: &wiremock::Request| {
2109 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2110 assert_eq!(body["model"], "grok-4");
2111 assert_eq!(body["temperature"], 0.3);
2112 assert_eq!(body["max_tokens"], 42);
2113 assert_eq!(body["tool_choice"]["type"], "function");
2114 assert_eq!(body["tool_choice"]["function"]["name"], "get_weather");
2115 assert_eq!(body["tools"][0]["type"], "function");
2116 assert_eq!(body["tools"][0]["function"]["name"], "get_weather");
2117 ResponseTemplate::new(200).set_body_json(json!({
2118 "id": "chatcmpl_sync_opts",
2119 "object": "chat.completion",
2120 "created": 1700000000,
2121 "model": "grok-4",
2122 "choices": [{
2123 "index": 0,
2124 "message": {
2125 "role": "assistant",
2126 "content": "options forwarded"
2127 },
2128 "finish_reason": "stop"
2129 }]
2130 }))
2131 })
2132 .mount(&server)
2133 .await;
2134 });
2135
2136 let async_client = XaiClient::builder()
2137 .api_key("test-key")
2138 .base_url(server.uri())
2139 .build()
2140 .unwrap();
2141
2142 let weather_tool = Tool::function(
2143 "get_weather",
2144 "Get weather",
2145 serde_json::json!({
2146 "type": "object",
2147 "properties": {"location": {"type": "string"}}
2148 }),
2149 );
2150
2151 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2152 let completion = client
2153 .chat()
2154 .create("grok-4")
2155 .message(Message::user("Weather?"))
2156 .tools(vec![weather_tool])
2157 .tool_choice(ToolChoice::function("get_weather"))
2158 .temperature(0.3)
2159 .max_tokens(42)
2160 .send()
2161 .unwrap();
2162
2163 assert_eq!(completion.text(), Some("options forwarded"));
2164 }
2165
2166 #[test]
2167 fn sync_images_generate_send_returns_url() {
2168 let server_rt = tokio::runtime::Runtime::new().unwrap();
2169 let server = server_rt.block_on(MockServer::start());
2170
2171 server_rt.block_on(async {
2172 Mock::given(method("POST"))
2173 .and(path("/images/generations"))
2174 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2175 "created": 1700000000,
2176 "data": [{
2177 "url": "https://example.com/image.png"
2178 }]
2179 })))
2180 .mount(&server)
2181 .await;
2182 });
2183
2184 let async_client = XaiClient::builder()
2185 .api_key("test-key")
2186 .base_url(server.uri())
2187 .build()
2188 .unwrap();
2189
2190 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2191 let response = client
2192 .images()
2193 .generate("grok-2-image", "a lighthouse at dawn")
2194 .send()
2195 .unwrap();
2196
2197 assert_eq!(response.first_url(), Some("https://example.com/image.png"));
2198 }
2199
2200 #[test]
2201 fn sync_images_generate_base64_forwards_options() {
2202 let server_rt = tokio::runtime::Runtime::new().unwrap();
2203 let server = server_rt.block_on(MockServer::start());
2204
2205 server_rt.block_on(async {
2206 Mock::given(method("POST"))
2207 .and(path("/images/generations"))
2208 .respond_with(move |req: &wiremock::Request| {
2209 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2210 assert_eq!(body["model"], "grok-2-image");
2211 assert_eq!(body["prompt"], "draw");
2212 assert_eq!(body["n"], 2);
2213 assert_eq!(body["response_format"], "b64_json");
2214 ResponseTemplate::new(200).set_body_json(json!({
2215 "created": 1700000000,
2216 "data": [{
2217 "b64_json": "aGVsbG8="
2218 }]
2219 }))
2220 })
2221 .mount(&server)
2222 .await;
2223 });
2224
2225 let async_client = XaiClient::builder()
2226 .api_key("test-key")
2227 .base_url(server.uri())
2228 .build()
2229 .unwrap();
2230
2231 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2232 let response = client
2233 .images()
2234 .generate("grok-2-image", "draw")
2235 .n(2)
2236 .base64_format()
2237 .send()
2238 .unwrap();
2239
2240 assert_eq!(response.first_base64(), Some("aGVsbG8="));
2241 }
2242
2243 #[test]
2244 fn sync_batch_create_returns_batch() {
2245 let server_rt = tokio::runtime::Runtime::new().unwrap();
2246 let server = server_rt.block_on(MockServer::start());
2247
2248 server_rt.block_on(async {
2249 Mock::given(method("POST"))
2250 .and(path("/batches"))
2251 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2252 "id": "batch_sync_1",
2253 "name": "my-sync-batch",
2254 "status": "queued"
2255 })))
2256 .mount(&server)
2257 .await;
2258 });
2259
2260 let async_client = XaiClient::builder()
2261 .api_key("test-key")
2262 .base_url(server.uri())
2263 .build()
2264 .unwrap();
2265
2266 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2267 let batch = client.batch().create("my-sync-batch").unwrap();
2268
2269 assert_eq!(batch.id, "batch_sync_1");
2270 assert_eq!(batch.name, "my-sync-batch");
2271 }
2272
2273 #[test]
2274 fn sync_collections_create_named_returns_collection() {
2275 let server_rt = tokio::runtime::Runtime::new().unwrap();
2276 let server = server_rt.block_on(MockServer::start());
2277
2278 server_rt.block_on(async {
2279 Mock::given(method("POST"))
2280 .and(path("/collections"))
2281 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2282 "id": "col_sync_1",
2283 "name": "my-sync-collection",
2284 "document_count": 0
2285 })))
2286 .mount(&server)
2287 .await;
2288 });
2289
2290 let async_client = XaiClient::builder()
2291 .api_key("test-key")
2292 .base_url(server.uri())
2293 .build()
2294 .unwrap();
2295
2296 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2297 let collection = client
2298 .collections()
2299 .create_named("my-sync-collection")
2300 .unwrap();
2301
2302 assert_eq!(collection.id, "col_sync_1");
2303 assert_eq!(collection.name, "my-sync-collection");
2304 }
2305
2306 #[cfg(feature = "files")]
2307 #[test]
2308 fn sync_files_list_returns_files() {
2309 let server_rt = tokio::runtime::Runtime::new().unwrap();
2310 let server = server_rt.block_on(MockServer::start());
2311
2312 server_rt.block_on(async {
2313 Mock::given(method("GET"))
2314 .and(path("/files"))
2315 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2316 "object": "list",
2317 "data": [{
2318 "id": "file_sync_1",
2319 "filename": "notes.txt",
2320 "bytes": 42,
2321 "created_at": 1700000000,
2322 "purpose": "assistants",
2323 "object": "file"
2324 }]
2325 })))
2326 .mount(&server)
2327 .await;
2328 });
2329
2330 let async_client = XaiClient::builder()
2331 .api_key("test-key")
2332 .base_url(server.uri())
2333 .build()
2334 .unwrap();
2335
2336 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2337 let files = client.files().list().unwrap();
2338
2339 assert_eq!(files.object, "list");
2340 assert_eq!(files.data.len(), 1);
2341 assert_eq!(files.data[0].id, "file_sync_1");
2342 }
2343
2344 #[test]
2345 fn sync_batch_list_with_options_and_get_result_use_expected_paths() {
2346 let server_rt = tokio::runtime::Runtime::new().unwrap();
2347 let server = server_rt.block_on(MockServer::start());
2348
2349 server_rt.block_on(async {
2350 Mock::given(method("GET"))
2351 .and(path("/batches"))
2352 .respond_with(move |req: &wiremock::Request| {
2353 assert_eq!(req.url.query(), Some("limit=2&next_token=tok_1"));
2354 ResponseTemplate::new(200).set_body_json(json!({
2355 "data": [
2356 {"id": "batch_sync_2", "name": "batch-two", "status": "processing"}
2357 ],
2358 "next_token": "tok_2"
2359 }))
2360 })
2361 .mount(&server)
2362 .await;
2363
2364 Mock::given(method("GET"))
2365 .and(path("/batches/batch%2Fsync/results/req%201"))
2366 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2367 "batch_request_id": "br_1",
2368 "custom_id": "req 1",
2369 "error_code": 0,
2370 "response": {
2371 "id": "resp_sync_batch",
2372 "model": "grok-4",
2373 "output": [{
2374 "type": "message",
2375 "role": "assistant",
2376 "content": [{"type": "text", "text": "batch result"}]
2377 }]
2378 }
2379 })))
2380 .mount(&server)
2381 .await;
2382 });
2383
2384 let async_client = XaiClient::builder()
2385 .api_key("test-key")
2386 .base_url(server.uri())
2387 .build()
2388 .unwrap();
2389
2390 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2391
2392 let listed = client
2393 .batch()
2394 .list_with_options(Some(2), Some("tok_1"))
2395 .unwrap();
2396 assert_eq!(listed.data.len(), 1);
2397 assert_eq!(listed.next_token.as_deref(), Some("tok_2"));
2398
2399 let result = client.batch().get_result("batch/sync", "req 1").unwrap();
2400 assert!(result.is_success());
2401 assert_eq!(result.text().as_deref(), Some("batch result"));
2402 }
2403
2404 #[test]
2405 fn sync_batch_list_requests_with_options_forwards_query() {
2406 let server_rt = tokio::runtime::Runtime::new().unwrap();
2407 let server = server_rt.block_on(MockServer::start());
2408
2409 server_rt.block_on(async {
2410 Mock::given(method("GET"))
2411 .and(path("/batches/batch_sync/requests"))
2412 .respond_with(move |req: &wiremock::Request| {
2413 assert_eq!(req.url.query(), Some("limit=5&next_token=tok_req"));
2414 ResponseTemplate::new(200).set_body_json(json!({
2415 "data": [{
2416 "id": "br_1",
2417 "custom_id": "req-1",
2418 "status": "completed"
2419 }],
2420 "next_token": "tok_req_2"
2421 }))
2422 })
2423 .mount(&server)
2424 .await;
2425 });
2426
2427 let async_client = XaiClient::builder()
2428 .api_key("test-key")
2429 .base_url(server.uri())
2430 .build()
2431 .unwrap();
2432
2433 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2434 let listed = client
2435 .batch()
2436 .list_requests_with_options("batch_sync", Some(5), Some("tok_req"))
2437 .unwrap();
2438
2439 assert_eq!(listed.data.len(), 1);
2440 assert_eq!(listed.data[0].custom_id, "req-1");
2441 assert_eq!(listed.next_token.as_deref(), Some("tok_req_2"));
2442 }
2443
2444 #[test]
2445 fn sync_collections_document_and_search_paths_are_encoded() {
2446 let server_rt = tokio::runtime::Runtime::new().unwrap();
2447 let server = server_rt.block_on(MockServer::start());
2448
2449 server_rt.block_on(async {
2450 Mock::given(method("GET"))
2451 .and(path("/collections/col%2Fsync/documents/doc%201"))
2452 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2453 "id": "doc 1",
2454 "content": "retrieved content"
2455 })))
2456 .mount(&server)
2457 .await;
2458
2459 Mock::given(method("POST"))
2460 .and(path("/collections/col%2Fsync/search"))
2461 .respond_with(move |req: &wiremock::Request| {
2462 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2463 assert_eq!(body["query"], "needle");
2464 ResponseTemplate::new(200).set_body_json(json!({
2465 "results": [{
2466 "document": {"id": "doc 1", "content": "needle"},
2467 "score": 0.91
2468 }]
2469 }))
2470 })
2471 .mount(&server)
2472 .await;
2473 });
2474
2475 let async_client = XaiClient::builder()
2476 .api_key("test-key")
2477 .base_url(server.uri())
2478 .build()
2479 .unwrap();
2480
2481 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2482 let document = client
2483 .collections()
2484 .get_document("col/sync", "doc 1")
2485 .unwrap();
2486 assert_eq!(document.content, "retrieved content");
2487
2488 let search = client
2489 .collections()
2490 .search_query("col/sync", "needle")
2491 .unwrap();
2492 assert_eq!(search.results.len(), 1);
2493 assert_eq!(search.results[0].document.content, "needle");
2494 }
2495
2496 #[test]
2497 fn sync_collections_list_with_options_and_delete_document_forward_correctly() {
2498 let server_rt = tokio::runtime::Runtime::new().unwrap();
2499 let server = server_rt.block_on(MockServer::start());
2500
2501 server_rt.block_on(async {
2502 Mock::given(method("GET"))
2503 .and(path("/collections"))
2504 .respond_with(move |req: &wiremock::Request| {
2505 assert_eq!(req.url.query(), Some("limit=4&next_token=tok_col"));
2506 ResponseTemplate::new(200).set_body_json(json!({
2507 "data": [{"id": "col_1", "name": "a", "document_count": 1}],
2508 "next_token": "tok_col_2"
2509 }))
2510 })
2511 .mount(&server)
2512 .await;
2513
2514 Mock::given(method("DELETE"))
2515 .and(path("/collections/col%2Fsync/documents/doc%201"))
2516 .respond_with(ResponseTemplate::new(204))
2517 .mount(&server)
2518 .await;
2519 });
2520
2521 let async_client = XaiClient::builder()
2522 .api_key("test-key")
2523 .base_url(server.uri())
2524 .build()
2525 .unwrap();
2526
2527 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2528 let listed = client
2529 .collections()
2530 .list_with_options(Some(4), Some("tok_col"))
2531 .unwrap();
2532 assert_eq!(listed.data.len(), 1);
2533 assert_eq!(listed.next_token.as_deref(), Some("tok_col_2"));
2534
2535 client
2536 .collections()
2537 .delete_document("col/sync", "doc 1")
2538 .unwrap();
2539 }
2540
2541 #[cfg(feature = "files")]
2542 #[test]
2543 fn sync_files_upload_content_and_delete_work() {
2544 let server_rt = tokio::runtime::Runtime::new().unwrap();
2545 let server = server_rt.block_on(MockServer::start());
2546
2547 server_rt.block_on(async {
2548 Mock::given(method("POST"))
2549 .and(path("/files"))
2550 .respond_with(move |req: &wiremock::Request| {
2551 let body = String::from_utf8_lossy(&req.body);
2552 assert!(body.contains("name=\"purpose\""));
2553 assert!(body.contains("batch"));
2554 ResponseTemplate::new(200).set_body_json(json!({
2555 "id": "file_sync_uploaded",
2556 "filename": "upload.txt",
2557 "bytes": 11,
2558 "created_at": 1700000000,
2559 "purpose": "batch",
2560 "object": "file"
2561 }))
2562 })
2563 .mount(&server)
2564 .await;
2565
2566 Mock::given(method("GET"))
2567 .and(path("/files/file%2Fsync/content"))
2568 .respond_with(ResponseTemplate::new(200).set_body_bytes(b"hello world".to_vec()))
2569 .mount(&server)
2570 .await;
2571
2572 Mock::given(method("DELETE"))
2573 .and(path("/files/file%2Fsync"))
2574 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2575 "id": "file/sync",
2576 "deleted": true,
2577 "object": "file"
2578 })))
2579 .mount(&server)
2580 .await;
2581 });
2582
2583 let async_client = XaiClient::builder()
2584 .api_key("test-key")
2585 .base_url(server.uri())
2586 .build()
2587 .unwrap();
2588
2589 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2590
2591 let uploaded = client
2592 .files()
2593 .upload("upload.txt", b"hello world".to_vec())
2594 .purpose(FilePurpose::Batch)
2595 .send()
2596 .unwrap();
2597 assert_eq!(uploaded.id, "file_sync_uploaded");
2598 assert_eq!(uploaded.purpose, "batch");
2599
2600 let bytes = client.files().content("file/sync").unwrap();
2601 assert_eq!(bytes, b"hello world".to_vec());
2602
2603 let deleted = client.files().delete("file/sync").unwrap();
2604 assert!(deleted.deleted);
2605 assert_eq!(deleted.id, "file/sync");
2606 }
2607
2608 #[cfg(feature = "files")]
2609 #[test]
2610 fn sync_files_get_uses_encoded_path() {
2611 let server_rt = tokio::runtime::Runtime::new().unwrap();
2612 let server = server_rt.block_on(MockServer::start());
2613
2614 server_rt.block_on(async {
2615 Mock::given(method("GET"))
2616 .and(path("/files/file%2Fsync"))
2617 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2618 "id": "file/sync",
2619 "filename": "notes.txt",
2620 "bytes": 42,
2621 "created_at": 1700000000,
2622 "purpose": "assistants",
2623 "object": "file"
2624 })))
2625 .mount(&server)
2626 .await;
2627 });
2628
2629 let async_client = XaiClient::builder()
2630 .api_key("test-key")
2631 .base_url(server.uri())
2632 .build()
2633 .unwrap();
2634
2635 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2636 let file = client.files().get("file/sync").unwrap();
2637 assert_eq!(file.id, "file/sync");
2638 assert_eq!(file.filename, "notes.txt");
2639 }
2640
2641 #[test]
2642 fn sync_auth_gets_api_key_from_namespace() {
2643 let server_rt = tokio::runtime::Runtime::new().unwrap();
2644 let server = server_rt.block_on(MockServer::start());
2645
2646 server_rt.block_on(async {
2647 Mock::given(method("GET"))
2648 .and(path("/api-key"))
2649 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2650 "api_key": "k_123",
2651 "object": "api_key",
2652 "created_at": 1700000000
2653 })))
2654 .mount(&server)
2655 .await;
2656 });
2657
2658 let async_client = XaiClient::builder()
2659 .api_key("test-key")
2660 .base_url(server.uri())
2661 .build()
2662 .unwrap();
2663
2664 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2665 let info = client.auth().api_key().unwrap();
2666
2667 assert_eq!(info.api_key, "k_123");
2668 assert_eq!(info.created_at, Some(1700000000));
2669 }
2670
2671 #[test]
2672 fn sync_videos_get_uses_encoded_path() {
2673 let server_rt = tokio::runtime::Runtime::new().unwrap();
2674 let server = server_rt.block_on(MockServer::start());
2675
2676 server_rt.block_on(async {
2677 Mock::given(method("GET"))
2678 .and(path("/videos/video%2Fsync"))
2679 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2680 "id": "video/sync",
2681 "status": "ready",
2682 "url": "https://example.com/video.mp4"
2683 })))
2684 .mount(&server)
2685 .await;
2686 });
2687
2688 let async_client = XaiClient::builder()
2689 .api_key("test-key")
2690 .base_url(server.uri())
2691 .build()
2692 .unwrap();
2693
2694 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2695 let video = client.videos().get("video/sync").unwrap();
2696 assert_eq!(video.id, "video/sync");
2697 assert_eq!(video.url.as_deref(), Some("https://example.com/video.mp4"));
2698 }
2699
2700 #[cfg(feature = "files")]
2701 #[test]
2702 fn sync_files_advanced_endpoints_forward_expected_payloads() {
2703 let server_rt = tokio::runtime::Runtime::new().unwrap();
2704 let server = server_rt.block_on(MockServer::start());
2705
2706 server_rt.block_on(async {
2707 Mock::given(method("POST"))
2708 .and(path("/files:download"))
2709 .respond_with(move |req: &wiremock::Request| {
2710 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2711 assert_eq!(body["file_id"], "file-1");
2712 ResponseTemplate::new(200).set_body_bytes(b"downloaded-bytes".to_vec())
2713 })
2714 .mount(&server)
2715 .await;
2716
2717 Mock::given(method("POST"))
2718 .and(path("/files:initialize"))
2719 .respond_with(move |req: &wiremock::Request| {
2720 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2721 assert_eq!(body["filename"], "video.mp4");
2722 assert_eq!(body["purpose"], "assistants");
2723 ResponseTemplate::new(200).set_body_json(json!({
2724 "upload_id": "u-123",
2725 "object": "upload_session"
2726 }))
2727 })
2728 .mount(&server)
2729 .await;
2730
2731 Mock::given(method("POST"))
2732 .and(path("/files:uploadChunks"))
2733 .respond_with(move |req: &wiremock::Request| {
2734 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2735 assert_eq!(body["upload_id"], "u-123");
2736 assert_eq!(body["part"], 1);
2737 ResponseTemplate::new(200).set_body_json(json!({
2738 "upload_id": "u-123",
2739 "complete": true
2740 }))
2741 })
2742 .mount(&server)
2743 .await;
2744 });
2745
2746 let async_client = XaiClient::builder()
2747 .api_key("test-key")
2748 .base_url(server.uri())
2749 .build()
2750 .unwrap();
2751
2752 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2753 let bytes = client
2754 .files()
2755 .download(FileDownloadRequest {
2756 file_id: "file-1".to_string(),
2757 })
2758 .unwrap();
2759 assert_eq!(bytes, b"downloaded-bytes".to_vec());
2760
2761 let initialize = client
2762 .files()
2763 .initialize_upload(FileUploadInitializeRequest::new(
2764 "video.mp4",
2765 FilePurpose::Assistants,
2766 ))
2767 .unwrap();
2768 assert_eq!(initialize.upload_id, "u-123");
2769
2770 let chunk = client
2771 .files()
2772 .upload_chunks(FileUploadChunksRequest {
2773 upload_id: "u-123".to_string(),
2774 part: 1,
2775 chunk: "chunk-data".to_string(),
2776 checksum: None,
2777 })
2778 .unwrap();
2779 assert!(chunk.complete);
2780 }
2781
2782 #[test]
2783 fn sync_collections_update_and_upsert_wrap_async_features() {
2784 let server_rt = tokio::runtime::Runtime::new().unwrap();
2785 let server = server_rt.block_on(MockServer::start());
2786
2787 server_rt.block_on(async {
2788 Mock::given(method("PUT"))
2789 .and(path("/collections/col%2Fsync"))
2790 .respond_with(move |req: &wiremock::Request| {
2791 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2792 assert_eq!(body["name"], "updated");
2793 ResponseTemplate::new(200).set_body_json(json!({
2794 "id": "col/sync",
2795 "name": "updated",
2796 "document_count": 1
2797 }))
2798 })
2799 .mount(&server)
2800 .await;
2801
2802 Mock::given(method("PATCH"))
2803 .and(path("/collections/col%2Fsync/documents/doc%201"))
2804 .respond_with(move |req: &wiremock::Request| {
2805 let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
2806 assert_eq!(body["id"], "doc 1");
2807 ResponseTemplate::new(200).set_body_json(json!({
2808 "id": "doc 1",
2809 "content": "updated"
2810 }))
2811 })
2812 .mount(&server)
2813 .await;
2814
2815 Mock::given(method("GET"))
2816 .and(path("/collections/col%2Fsync/documents:batchGet"))
2817 .respond_with(move |req: &wiremock::Request| {
2818 assert_eq!(req.url.query(), Some("ids=d1&ids=d2"));
2819 ResponseTemplate::new(200).set_body_json(json!({
2820 "data": [
2821 {"id": "d1", "content": "one"},
2822 {"id": "d2", "content": "two"}
2823 ]
2824 }))
2825 })
2826 .mount(&server)
2827 .await;
2828 });
2829
2830 let async_client = XaiClient::builder()
2831 .api_key("test-key")
2832 .base_url(server.uri())
2833 .build()
2834 .unwrap();
2835
2836 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2837 let updated = client
2838 .collections()
2839 .update("col/sync", UpdateCollectionRequest::new().name("updated"))
2840 .unwrap();
2841 assert_eq!(updated.name, "updated");
2842
2843 let doc = client
2844 .collections()
2845 .upsert_document(
2846 "col/sync",
2847 Document {
2848 id: Some("doc 1".to_string()),
2849 content: "updated".to_string(),
2850 metadata: None,
2851 },
2852 )
2853 .unwrap();
2854 assert_eq!(doc.id.as_deref(), Some("doc 1"));
2855
2856 let docs = client
2857 .collections()
2858 .batch_get_documents(
2859 "col/sync",
2860 BatchGetDocumentsRequest::new(vec!["d1".into(), "d2".into()]),
2861 )
2862 .unwrap();
2863 assert_eq!(docs.data.len(), 2);
2864 }
2865
2866 #[test]
2867 fn sync_run_executes_unwrapped_future_outside_async_runtime() {
2868 let server_rt = tokio::runtime::Runtime::new().unwrap();
2869 let server = server_rt.block_on(MockServer::start());
2870
2871 server_rt.block_on(async {
2872 Mock::given(method("GET"))
2873 .and(path("/models"))
2874 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2875 "object": "list",
2876 "data": [{"id": "grok-4", "object": "model", "owned_by": "xai"}]
2877 })))
2878 .mount(&server)
2879 .await;
2880 });
2881
2882 let async_client = XaiClient::builder()
2883 .api_key("test-key")
2884 .base_url(server.uri())
2885 .build()
2886 .unwrap();
2887
2888 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2889 let models = client
2890 .run(client.as_async_client().models().list())
2891 .unwrap();
2892
2893 assert_eq!(models.data.len(), 1);
2894 assert_eq!(models.data[0].id, "grok-4");
2895 }
2896
2897 #[test]
2898 fn sync_models_list_propagates_api_error() {
2899 let server_rt = tokio::runtime::Runtime::new().unwrap();
2900 let server = server_rt.block_on(MockServer::start());
2901
2902 server_rt.block_on(async {
2903 Mock::given(method("GET"))
2904 .and(path("/models"))
2905 .respond_with(ResponseTemplate::new(503).set_body_json(json!({
2906 "error": {"message": "service unavailable", "type": "server_error"}
2907 })))
2908 .mount(&server)
2909 .await;
2910 });
2911
2912 let async_client = XaiClient::builder()
2913 .api_key("test-key")
2914 .base_url(server.uri())
2915 .build()
2916 .unwrap();
2917
2918 let client = SyncXaiClient::with_async_client(async_client).unwrap();
2919 let err = client.models().list().unwrap_err();
2920 assert!(matches!(err, Error::Api { status: 503, .. }));
2921 }
2922
2923 #[tokio::test]
2924 async fn sync_client_errors_inside_async_runtime() {
2925 let async_client = XaiClient::new("test-key").unwrap();
2926 let sync = SyncXaiClient::with_async_client(async_client).unwrap();
2927
2928 let err = sync.responses().get("resp_123").unwrap_err();
2929 assert!(
2930 matches!(err, Error::Config(msg) if msg.contains("cannot run inside an async runtime"))
2931 );
2932
2933 tokio::task::spawn_blocking(move || drop(sync))
2934 .await
2935 .unwrap();
2936 }
2937
2938 #[test]
2939 fn sync_responses_get_delete_and_poll_until_ready() {
2940 let server_rt = tokio::runtime::Runtime::new().unwrap();
2941 let server = server_rt.block_on(MockServer::start());
2942 let poll_count = std::sync::Arc::new(AtomicUsize::new(0));
2943 let poll_count_clone = std::sync::Arc::clone(&poll_count);
2944
2945 server_rt.block_on(async {
2946 Mock::given(method("GET"))
2947 .and(path("/responses/resp_sync"))
2948 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2949 "id": "resp_sync",
2950 "model": "grok-4",
2951 "output": [{
2952 "type": "message",
2953 "role": "assistant",
2954 "content": [{"type": "text", "text": "from_get" }]
2955 }],
2956 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
2957 })))
2958 .mount(&server)
2959 .await;
2960
2961 Mock::given(method("DELETE"))
2962 .and(path("/responses/resp_sync"))
2963 .respond_with(ResponseTemplate::new(204))
2964 .mount(&server)
2965 .await;
2966
2967 Mock::given(method("GET"))
2968 .and(path("/responses/poll"))
2969 .respond_with(move |_req: &wiremock::Request| {
2970 let attempts = poll_count_clone.fetch_add(1, Ordering::SeqCst);
2971 if attempts == 0 {
2972 ResponseTemplate::new(200).set_body_json(json!({
2973 "id": "poll",
2974 "model": "grok-4",
2975 "output": [],
2976 "usage": {"prompt_tokens": 1, "completion_tokens": 0, "total_tokens": 1}
2977 }))
2978 } else {
2979 ResponseTemplate::new(200).set_body_json(json!({
2980 "id": "poll",
2981 "model": "grok-4",
2982 "output": [{
2983 "type": "message",
2984 "role": "assistant",
2985 "content": [{"type": "text", "text": "ready" }]
2986 }],
2987 "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
2988 }))
2989 }
2990 })
2991 .mount(&server)
2992 .await;
2993 });
2994
2995 let async_client = XaiClient::builder()
2996 .api_key("test-key")
2997 .base_url(server.uri())
2998 .build()
2999 .unwrap();
3000
3001 let client = SyncXaiClient::with_async_client(async_client).unwrap();
3002
3003 let response = client.responses().get("resp_sync").unwrap();
3004 assert_eq!(response.output_text().as_deref(), Some("from_get"));
3005
3006 client.responses().delete("resp_sync").unwrap();
3007
3008 let polled = client.responses().poll_until_ready("poll", 2).unwrap();
3009 assert_eq!(polled.output_text().as_deref(), Some("ready"));
3010 assert_eq!(poll_count.load(Ordering::SeqCst), 2);
3011 }
3012
3013 #[test]
3014 fn sync_batch_and_collections_wrappers_cover_more_paths() {
3015 let server_rt = tokio::runtime::Runtime::new().unwrap();
3016 let server = server_rt.block_on(MockServer::start());
3017
3018 server_rt.block_on(async {
3019 Mock::given(method("GET"))
3020 .and(path("/batches/batch_sync"))
3021 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3022 "id": "batch_sync",
3023 "name": "my-batch",
3024 "status": "completed"
3025 })))
3026 .mount(&server)
3027 .await;
3028
3029 Mock::given(method("GET"))
3030 .and(path("/batches"))
3031 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3032 "data": [{
3033 "id": "batch_sync",
3034 "name": "my-batch",
3035 "status": "completed"
3036 }]
3037 })))
3038 .mount(&server)
3039 .await;
3040
3041 Mock::given(method("POST"))
3042 .and(path("/collections"))
3043 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3044 "id": "col_sync",
3045 "name": "my-collection",
3046 "document_count": 0
3047 })))
3048 .mount(&server)
3049 .await;
3050
3051 Mock::given(method("POST"))
3052 .and(path("/collections/col_sync/documents"))
3053 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3054 "ids": ["doc_sync"]
3055 })))
3056 .mount(&server)
3057 .await;
3058
3059 Mock::given(method("GET"))
3060 .and(path("/collections/col_sync/documents/doc_1"))
3061 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3062 "id": "doc_1",
3063 "content": "content"
3064 })))
3065 .mount(&server)
3066 .await;
3067
3068 Mock::given(method("GET"))
3069 .and(path("/collections/col_sync/documents"))
3070 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3071 "data": [{
3072 "id": "doc_1",
3073 "content": "content"
3074 }]
3075 })))
3076 .mount(&server)
3077 .await;
3078
3079 Mock::given(method("POST"))
3080 .and(path("/collections/col_sync/search"))
3081 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3082 "results": [{
3083 "document": {"id": "doc_1", "content": "content"},
3084 "score": 0.95
3085 }]
3086 })))
3087 .mount(&server)
3088 .await;
3089
3090 Mock::given(method("DELETE"))
3091 .and(path("/collections/col_sync"))
3092 .respond_with(ResponseTemplate::new(204))
3093 .mount(&server)
3094 .await;
3095
3096 Mock::given(method("GET"))
3097 .and(path("/collections/col_sync"))
3098 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3099 "id": "col_sync",
3100 "name": "my-collection",
3101 "document_count": 1
3102 })))
3103 .mount(&server)
3104 .await;
3105 });
3106
3107 let async_client = XaiClient::builder()
3108 .api_key("test-key")
3109 .base_url(server.uri())
3110 .build()
3111 .unwrap();
3112
3113 let client = SyncXaiClient::with_async_client(async_client).unwrap();
3114
3115 let batch = client.batch().get("batch_sync").unwrap();
3116 assert_eq!(batch.id, "batch_sync");
3117 assert_eq!(client.batch().list().unwrap().data.len(), 1);
3118
3119 let created = client
3120 .collections()
3121 .create(CreateCollectionRequest::new("my-collection"))
3122 .unwrap();
3123 assert_eq!(created.id, "col_sync");
3124
3125 let added = client
3126 .collections()
3127 .add_document("col_sync", Document::with_id("doc_sync", "content"))
3128 .unwrap();
3129 assert_eq!(added, "doc_sync");
3130
3131 let doc = client
3132 .collections()
3133 .get_document("col_sync", "doc_1")
3134 .unwrap();
3135 assert_eq!(doc.id.as_ref().unwrap(), "doc_1");
3136
3137 let docs = client.collections().list_documents("col_sync").unwrap();
3138 assert_eq!(docs.data.len(), 1);
3139
3140 let search = client
3141 .collections()
3142 .search("col_sync", SearchRequest::new("content").limit(1))
3143 .unwrap();
3144 assert_eq!(search.results[0].document.id.as_deref(), Some("doc_1"));
3145
3146 client.collections().delete("col_sync").unwrap();
3147 }
3148
3149 #[test]
3150 fn sync_chat_completion_builder_forwards_optional_fields() {
3151 let server_rt = tokio::runtime::Runtime::new().unwrap();
3152 let server = server_rt.block_on(MockServer::start());
3153
3154 server_rt.block_on(async {
3155 Mock::given(method("POST"))
3156 .and(path("/chat/completions"))
3157 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3158 "id": "chat_sync",
3159 "object": "chat.completion",
3160 "created": 1700000000,
3161 "model": "grok-4",
3162 "choices": [{
3163 "index": 0,
3164 "message": {
3165 "role": "assistant",
3166 "content": "done"
3167 },
3168 "finish_reason": "stop"
3169 }]
3170 })))
3171 .mount(&server)
3172 .await;
3173 });
3174
3175 let async_client = XaiClient::builder()
3176 .api_key("test-key")
3177 .base_url(server.uri())
3178 .build()
3179 .unwrap();
3180
3181 let client = SyncXaiClient::with_async_client(async_client).unwrap();
3182 let response = client
3183 .chat()
3184 .create("grok-4")
3185 .message(Message::user("hi"))
3186 .n(2)
3187 .stop(vec!["STOP".to_string()])
3188 .presence_penalty(0.2)
3189 .frequency_penalty(0.3)
3190 .message(Message::assistant("ignored"))
3191 .send()
3192 .unwrap();
3193
3194 assert_eq!(response.text(), Some("done"));
3195 }
3196}