Skip to main content

xai_rust/
sync.rs

1//! Blocking (sync-style) client facade built on top of the async SDK.
2//!
3//! This module is intended for applications that cannot use `async/await`.
4//! It runs SDK futures on an internal Tokio runtime and exposes blocking
5//! request methods across the SDK API namespaces.
6
7use 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/// Blocking/sync facade for the xAI SDK.
47#[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    /// Create a blocking client from an API key.
63    pub fn new(api_key: impl Into<String>) -> Result<Self> {
64        Self::with_async_client(XaiClient::new(api_key)?)
65    }
66
67    /// Create a blocking client from the `XAI_API_KEY` environment variable.
68    pub fn from_env() -> Result<Self> {
69        Self::with_async_client(XaiClient::from_env()?)
70    }
71
72    /// Create a blocking facade from an existing async client.
73    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    /// Get the wrapped async client.
82    pub fn as_async_client(&self) -> &XaiClient {
83        &self.client
84    }
85
86    /// Get the configured API base URL.
87    pub fn base_url(&self) -> &str {
88        self.client.base_url()
89    }
90
91    /// Access blocking Responses API helpers.
92    pub fn responses(&self) -> SyncResponsesApi {
93        SyncResponsesApi {
94            client: self.clone(),
95        }
96    }
97
98    /// Access blocking Auth API helpers.
99    pub fn auth(&self) -> SyncAuthApi {
100        SyncAuthApi {
101            client: self.clone(),
102        }
103    }
104
105    /// Access blocking Models API helpers.
106    pub fn models(&self) -> SyncModelsApi {
107        SyncModelsApi {
108            client: self.clone(),
109        }
110    }
111
112    /// Access blocking legacy Chat Completions API helpers.
113    pub fn chat(&self) -> SyncChatApi {
114        SyncChatApi {
115            client: self.clone(),
116        }
117    }
118
119    /// Access blocking Images API helpers.
120    pub fn images(&self) -> SyncImagesApi {
121        SyncImagesApi {
122            client: self.clone(),
123        }
124    }
125
126    /// Access blocking Videos API helpers.
127    pub fn videos(&self) -> SyncVideosApi {
128        SyncVideosApi {
129            client: self.clone(),
130        }
131    }
132
133    /// Access blocking Batch API helpers.
134    pub fn batch(&self) -> SyncBatchApi {
135        SyncBatchApi {
136            client: self.clone(),
137        }
138    }
139
140    /// Access blocking Collections API helpers.
141    pub fn collections(&self) -> SyncCollectionsApi {
142        SyncCollectionsApi {
143            client: self.clone(),
144        }
145    }
146
147    /// Access blocking Files API helpers.
148    #[cfg(feature = "files")]
149    pub fn files(&self) -> SyncFilesApi {
150        SyncFilesApi {
151            client: self.clone(),
152        }
153    }
154
155    /// Access blocking Tokenizer API helpers.
156    pub fn tokenizer(&self) -> SyncTokenizerApi {
157        SyncTokenizerApi {
158            client: self.clone(),
159        }
160    }
161
162    /// Run an SDK future to completion on the internal runtime.
163    ///
164    /// This can be used for endpoints that do not yet have dedicated blocking wrappers.
165    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/// Blocking wrapper for `ResponsesApi`.
177#[derive(Debug, Clone)]
178pub struct SyncResponsesApi {
179    client: SyncXaiClient,
180}
181
182impl SyncResponsesApi {
183    /// Create a blocking response request builder.
184    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    /// Create a blocking deferred-response poller.
192    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    /// Create a blocking stateful chat handle.
200    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    /// Get a response by ID.
208    pub fn get(&self, response_id: &str) -> Result<Response> {
209        self.client
210            .run(self.client.client.responses().get(response_id))
211    }
212
213    /// Delete a response by ID.
214    pub fn delete(&self, response_id: &str) -> Result<()> {
215        self.client
216            .run(self.client.client.responses().delete(response_id))
217    }
218
219    /// Poll until output is available or attempts are exhausted.
220    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/// Blocking wrapper for `ModelsApi`.
231#[derive(Debug, Clone)]
232pub struct SyncModelsApi {
233    client: SyncXaiClient,
234}
235
236impl SyncModelsApi {
237    /// List available models.
238    pub fn list(&self) -> Result<ModelListResponse> {
239        self.client.run(self.client.client.models().list())
240    }
241
242    /// Get a specific model by ID.
243    pub fn get(&self, model_id: &str) -> Result<Model> {
244        self.client.run(self.client.client.models().get(model_id))
245    }
246}
247
248/// Blocking wrapper for `AuthApi`.
249#[derive(Debug, Clone)]
250pub struct SyncAuthApi {
251    client: SyncXaiClient,
252}
253
254impl SyncAuthApi {
255    /// Retrieve API key info.
256    pub fn api_key(&self) -> Result<ApiKeyInfo> {
257        self.client.run(self.client.client.auth().api_key())
258    }
259}
260
261/// Blocking wrapper for `VideosApi`.
262#[derive(Debug, Clone)]
263pub struct SyncVideosApi {
264    client: SyncXaiClient,
265}
266
267impl SyncVideosApi {
268    /// Get a video by ID.
269    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/// Blocking wrapper for `TokenizerApi`.
276#[derive(Debug, Clone)]
277pub struct SyncTokenizerApi {
278    client: SyncXaiClient,
279}
280
281impl SyncTokenizerApi {
282    /// Tokenize using a full request object.
283    pub fn tokenize(&self, request: TokenizeRequest) -> Result<TokenizeResponse> {
284        self.client
285            .run(self.client.client.tokenizer().tokenize(request))
286    }
287
288    /// Tokenize using model and text values.
289    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    /// Count tokens for model and text.
299    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/// Blocking wrapper for the legacy `ChatApi`.
306#[derive(Debug, Clone)]
307pub struct SyncChatApi {
308    client: SyncXaiClient,
309}
310
311impl SyncChatApi {
312    /// Create a blocking chat completion request builder.
313    #[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/// Blocking wrapper for `ChatCompletionBuilder`.
323#[derive(Debug)]
324pub struct SyncChatCompletionBuilder {
325    client: SyncXaiClient,
326    inner: ChatCompletionBuilder,
327}
328
329impl SyncChatCompletionBuilder {
330    /// Add messages to the conversation.
331    pub fn messages(mut self, messages: Vec<Message>) -> Self {
332        self.inner = self.inner.messages(messages);
333        self
334    }
335
336    /// Add a single message.
337    pub fn message(mut self, message: Message) -> Self {
338        self.inner = self.inner.message(message);
339        self
340    }
341
342    /// Add tools.
343    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
344        self.inner = self.inner.tools(tools);
345        self
346    }
347
348    /// Set tool choice.
349    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
350        self.inner = self.inner.tool_choice(choice);
351        self
352    }
353
354    /// Set temperature.
355    pub fn temperature(mut self, temperature: f32) -> Self {
356        self.inner = self.inner.temperature(temperature);
357        self
358    }
359
360    /// Set top-p.
361    pub fn top_p(mut self, top_p: f32) -> Self {
362        self.inner = self.inner.top_p(top_p);
363        self
364    }
365
366    /// Set max tokens.
367    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
368        self.inner = self.inner.max_tokens(max_tokens);
369        self
370    }
371
372    /// Set response format.
373    pub fn response_format(mut self, format: ResponseFormat) -> Self {
374        self.inner = self.inner.response_format(format);
375        self
376    }
377
378    /// Set number of completions.
379    pub fn n(mut self, n: u32) -> Self {
380        self.inner = self.inner.n(n);
381        self
382    }
383
384    /// Set stop sequences.
385    pub fn stop(mut self, stop: Vec<String>) -> Self {
386        self.inner = self.inner.stop(stop);
387        self
388    }
389
390    /// Set presence penalty.
391    pub fn presence_penalty(mut self, penalty: f32) -> Self {
392        self.inner = self.inner.presence_penalty(penalty);
393        self
394    }
395
396    /// Set frequency penalty.
397    pub fn frequency_penalty(mut self, penalty: f32) -> Self {
398        self.inner = self.inner.frequency_penalty(penalty);
399        self
400    }
401
402    /// Send the request and return a chat completion.
403    pub fn send(self) -> Result<ChatCompletion> {
404        self.client.run(self.inner.send())
405    }
406}
407
408/// Blocking wrapper for `ImagesApi`.
409#[derive(Debug, Clone)]
410pub struct SyncImagesApi {
411    client: SyncXaiClient,
412}
413
414impl SyncImagesApi {
415    /// Create a blocking image generation request builder.
416    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/// Blocking wrapper for `ImageGenerationBuilder`.
429#[derive(Debug)]
430pub struct SyncImageGenerationBuilder {
431    client: SyncXaiClient,
432    inner: ImageGenerationBuilder,
433}
434
435impl SyncImageGenerationBuilder {
436    /// Set number of images (1-10).
437    pub fn n(mut self, n: u8) -> Self {
438        self.inner = self.inner.n(n);
439        self
440    }
441
442    /// Set response format.
443    pub fn response_format(mut self, format: ImageResponseFormat) -> Self {
444        self.inner = self.inner.response_format(format);
445        self
446    }
447
448    /// Request URL format.
449    pub fn url_format(mut self) -> Self {
450        self.inner = self.inner.url_format();
451        self
452    }
453
454    /// Request base64 format.
455    pub fn base64_format(mut self) -> Self {
456        self.inner = self.inner.base64_format();
457        self
458    }
459
460    /// Send the request and return generated images.
461    pub fn send(self) -> Result<ImageGenerationResponse> {
462        self.client.run(self.inner.send())
463    }
464}
465
466/// Blocking wrapper for `BatchApi`.
467#[derive(Debug, Clone)]
468pub struct SyncBatchApi {
469    client: SyncXaiClient,
470}
471
472impl SyncBatchApi {
473    /// Create a batch.
474    pub fn create(&self, name: impl Into<String>) -> Result<Batch> {
475        self.client.run(self.client.client.batch().create(name))
476    }
477
478    /// Get batch metadata by ID.
479    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    /// List batches.
484    pub fn list(&self) -> Result<BatchListResponse> {
485        self.client.run(self.client.client.batch().list())
486    }
487
488    /// List batches with pagination options.
489    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    /// Cancel a batch.
503    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    /// Add requests to a batch.
508    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    /// List requests in a batch.
518    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    /// List requests in a batch with pagination options.
524    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    /// List results for a batch.
539    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    /// List results for a batch with pagination options.
545    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    /// Get a specific request result.
560    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/// Blocking wrapper for `CollectionsApi`.
571#[derive(Debug, Clone)]
572pub struct SyncCollectionsApi {
573    client: SyncXaiClient,
574}
575
576impl SyncCollectionsApi {
577    /// Create a collection from a request object.
578    pub fn create(&self, request: CreateCollectionRequest) -> Result<Collection> {
579        self.client
580            .run(self.client.client.collections().create(request))
581    }
582
583    /// Create a collection with only a name.
584    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    /// Get a collection by ID.
590    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    /// List collections.
596    pub fn list(&self) -> Result<CollectionListResponse> {
597        self.client.run(self.client.client.collections().list())
598    }
599
600    /// List collections with pagination options.
601    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    /// Delete a collection.
615    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    /// Add documents to a collection.
621    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    /// Add a single document to a collection.
635    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    /// Update a collection.
649    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    /// Add or replace a document by ID.
663    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    /// Get multiple documents by IDs.
677    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    /// List documents in a collection.
691    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    /// List documents in a collection with pagination options.
701    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    /// Get a document by ID.
716    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    /// Delete a document from a collection.
730    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    /// Search a collection with a request object.
744    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    /// Search a collection with a query string.
758    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/// Blocking wrapper for `FilesApi`.
773#[cfg(feature = "files")]
774#[derive(Debug, Clone)]
775pub struct SyncFilesApi {
776    client: SyncXaiClient,
777}
778
779#[cfg(feature = "files")]
780impl SyncFilesApi {
781    /// List uploaded files.
782    pub fn list(&self) -> Result<FileListResponse> {
783        self.client.run(self.client.client.files().list())
784    }
785
786    /// Get file metadata by ID.
787    pub fn get(&self, file_id: &str) -> Result<FileObject> {
788        self.client.run(self.client.client.files().get(file_id))
789    }
790
791    /// Get raw file bytes by ID.
792    pub fn content(&self, file_id: &str) -> Result<Vec<u8>> {
793        self.client.run(self.client.client.files().content(file_id))
794    }
795
796    /// Delete a file by ID.
797    pub fn delete(&self, file_id: &str) -> Result<DeleteFileResponse> {
798        self.client.run(self.client.client.files().delete(file_id))
799    }
800
801    /// Create a blocking file upload request builder.
802    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    /// Download file bytes with the explicit download endpoint.
810    pub fn download(&self, request: FileDownloadRequest) -> Result<Vec<u8>> {
811        self.client
812            .run(self.client.client.files().download(request))
813    }
814
815    /// Initialize a multi-part upload session.
816    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    /// Upload a file chunk.
825    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/// Blocking wrapper for `UploadFileBuilder`.
835#[cfg(feature = "files")]
836#[derive(Debug)]
837pub struct SyncUploadFileBuilder {
838    client: SyncXaiClient,
839    inner: UploadFileBuilder,
840}
841
842#[cfg(feature = "files")]
843impl SyncUploadFileBuilder {
844    /// Set the file purpose.
845    pub fn purpose(mut self, purpose: FilePurpose) -> Self {
846        self.inner = self.inner.purpose(purpose);
847        self
848    }
849
850    /// Send the upload request and return file metadata.
851    pub fn send(self) -> Result<FileObject> {
852        self.client.run(self.inner.send())
853    }
854}
855
856/// Blocking wrapper for `CreateResponseBuilder`.
857#[derive(Debug)]
858pub struct SyncCreateResponseBuilder {
859    client: SyncXaiClient,
860    inner: CreateResponseBuilder,
861}
862
863impl SyncCreateResponseBuilder {
864    /// Add a message.
865    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    /// Add a system message.
871    pub fn system(mut self, content: impl Into<String>) -> Self {
872        self.inner = self.inner.system(content);
873        self
874    }
875
876    /// Add a user message.
877    pub fn user(mut self, content: impl Into<MessageContent>) -> Self {
878        self.inner = self.inner.user(content);
879        self
880    }
881
882    /// Add an assistant message.
883    pub fn assistant(mut self, content: impl Into<String>) -> Self {
884        self.inner = self.inner.assistant(content);
885        self
886    }
887
888    /// Add a user message with image content.
889    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    /// Add pre-built messages.
899    pub fn messages(mut self, messages: Vec<Message>) -> Self {
900        self.inner = self.inner.messages(messages);
901        self
902    }
903
904    /// Add a tool.
905    pub fn tool(mut self, tool: Tool) -> Self {
906        self.inner = self.inner.tool(tool);
907        self
908    }
909
910    /// Add multiple tools.
911    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
912        self.inner = self.inner.tools(tools);
913        self
914    }
915
916    /// Set tool choice.
917    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
918        self.inner = self.inner.tool_choice(choice);
919        self
920    }
921
922    /// Set temperature.
923    pub fn temperature(mut self, temperature: f32) -> Self {
924        self.inner = self.inner.temperature(temperature);
925        self
926    }
927
928    /// Set top-p.
929    pub fn top_p(mut self, top_p: f32) -> Self {
930        self.inner = self.inner.top_p(top_p);
931        self
932    }
933
934    /// Set max tokens.
935    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
936        self.inner = self.inner.max_tokens(max_tokens);
937        self
938    }
939
940    /// Override retry attempts for this request.
941    pub fn max_retries(mut self, max_retries: u32) -> Self {
942        self.inner = self.inner.max_retries(max_retries);
943        self
944    }
945
946    /// Disable retries for this request.
947    pub fn disable_retries(mut self) -> Self {
948        self.inner = self.inner.disable_retries();
949        self
950    }
951
952    /// Override retry backoff for this request.
953    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    /// Override retry jitter for this request.
959    pub fn retry_jitter(mut self, factor: f64) -> Self {
960        self.inner = self.inner.retry_jitter(factor);
961        self
962    }
963
964    /// Set response format.
965    pub fn response_format(mut self, format: ResponseFormat) -> Self {
966        self.inner = self.inner.response_format(format);
967        self
968    }
969
970    /// Request JSON output.
971    pub fn json_output(mut self) -> Self {
972        self.inner = self.inner.json_output();
973        self
974    }
975
976    /// Include additional response fields.
977    pub fn include(mut self, fields: Vec<String>) -> Self {
978        self.inner = self.inner.include(fields);
979        self
980    }
981
982    /// Include inline citations.
983    pub fn with_inline_citations(mut self) -> Self {
984        self.inner = self.inner.with_inline_citations();
985        self
986    }
987
988    /// Include verbose streaming metadata.
989    pub fn with_verbose_streaming(mut self) -> Self {
990        self.inner = self.inner.with_verbose_streaming();
991        self
992    }
993
994    /// Set response storage behavior.
995    pub fn store(mut self, store: bool) -> Self {
996        self.inner = self.inner.store(store);
997        self
998    }
999
1000    /// Send the request and return a response.
1001    pub fn send(self) -> Result<Response> {
1002        self.client.run(self.inner.send())
1003    }
1004}
1005
1006/// Blocking wrapper for `DeferredResponsePoller`.
1007#[derive(Debug, Clone)]
1008pub struct SyncDeferredResponsePoller {
1009    client: SyncXaiClient,
1010    inner: DeferredResponsePoller,
1011}
1012
1013impl SyncDeferredResponsePoller {
1014    /// Set max polling attempts.
1015    pub fn max_attempts(mut self, max_attempts: u32) -> Self {
1016        self.inner = self.inner.max_attempts(max_attempts);
1017        self
1018    }
1019
1020    /// Set a fixed polling interval.
1021    pub fn poll_interval(mut self, interval: Duration) -> Self {
1022        self.inner = self.inner.poll_interval(interval);
1023        self
1024    }
1025
1026    /// Set exponential polling backoff bounds.
1027    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    /// Wait until response output becomes available.
1033    pub fn wait(self) -> Result<Response> {
1034        self.client.run(self.inner.wait())
1035    }
1036}
1037
1038/// Blocking wrapper for stateful chat interactions.
1039#[derive(Debug, Clone)]
1040pub struct SyncStatefulChat {
1041    client: SyncXaiClient,
1042    inner: StatefulChat,
1043}
1044
1045impl SyncStatefulChat {
1046    /// Append a message to local state.
1047    pub fn append(&mut self, role: Role, content: impl Into<MessageContent>) -> &mut Self {
1048        self.inner.append(role, content);
1049        self
1050    }
1051
1052    /// Append a system message.
1053    pub fn append_system(&mut self, content: impl Into<String>) -> &mut Self {
1054        self.inner.append_system(content);
1055        self
1056    }
1057
1058    /// Append a user message.
1059    pub fn append_user(&mut self, content: impl Into<MessageContent>) -> &mut Self {
1060        self.inner.append_user(content);
1061        self
1062    }
1063
1064    /// Append an assistant message.
1065    pub fn append_assistant(&mut self, content: impl Into<String>) -> &mut Self {
1066        self.inner.append_assistant(content);
1067        self
1068    }
1069
1070    /// Append a tool result message.
1071    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    /// Append a pre-built message.
1081    pub fn append_message(&mut self, message: Message) -> &mut Self {
1082        self.inner.append_message(message);
1083        self
1084    }
1085
1086    /// Get local message history.
1087    pub fn messages(&self) -> &[Message] {
1088        self.inner.messages()
1089    }
1090
1091    /// Get pending tool calls from the latest sampled response.
1092    pub fn pending_tool_calls(&self) -> &[ToolCall] {
1093        self.inner.pending_tool_calls()
1094    }
1095
1096    /// Take and clear pending tool calls.
1097    pub fn take_pending_tool_calls(&mut self) -> Vec<ToolCall> {
1098        self.inner.take_pending_tool_calls()
1099    }
1100
1101    /// Clear local chat state.
1102    pub fn clear(&mut self) -> &mut Self {
1103        self.inner.clear();
1104        self
1105    }
1106
1107    /// Sample with current local message history.
1108    pub fn sample(&self) -> Result<Response> {
1109        self.client.run(self.inner.sample())
1110    }
1111
1112    /// Append assistant text from a sampled response.
1113    pub fn append_response_text(&mut self, response: &Response) -> &mut Self {
1114        self.inner.append_response_text(response);
1115        self
1116    }
1117
1118    /// Append response semantics and pending tool calls to local state.
1119    pub fn append_response_semantics(&mut self, response: &Response) -> &mut Self {
1120        self.inner.append_response_semantics(response);
1121        self
1122    }
1123
1124    /// Sample and append response semantics.
1125    pub fn sample_and_append(&mut self) -> Result<Response> {
1126        self.client.run(self.inner.sample_and_append())
1127    }
1128
1129    /// Run a blocking tool loop with default max rounds.
1130    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    /// Run a blocking tool loop with explicit max rounds.
1138    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}