1use httpdate::parse_http_date;
4use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
5use reqwest::{RequestBuilder, Response, StatusCode};
6use std::sync::Arc;
7use std::time::Duration;
8use std::time::{SystemTime, UNIX_EPOCH};
9use tokio::time::sleep;
10#[cfg(feature = "telemetry")]
11use tracing::{debug, warn};
12
13#[cfg(feature = "files")]
14use crate::api::FilesApi;
15#[cfg(feature = "realtime")]
16use crate::api::RealtimeApi;
17use crate::api::{
18 AuthApi, BatchApi, ChatApi, CollectionsApi, DocumentsApi, EmbeddingsApi, ImagesApi, ModelsApi,
19 ResponsesApi, TokenizerApi, VideosApi,
20};
21use crate::config::{ClientConfig, RetryPolicy, XaiClientBuilder};
22use crate::sync::SyncXaiClient;
23use crate::{Error, Result};
24
25const SDK_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
26const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
27
28#[derive(Debug, Clone)]
53pub struct XaiClient {
54 inner: Arc<ClientInner>,
55}
56
57#[derive(Debug)]
58struct ClientInner {
59 config: ClientConfig,
60 http: reqwest::Client,
61 retry_policy: RetryPolicy,
62}
63
64impl XaiClient {
65 pub fn new(api_key: impl Into<String>) -> Result<Self> {
74 let config = ClientConfig::new(api_key);
75 Self::with_config(config)
76 }
77
78 pub fn from_env() -> Result<Self> {
80 let config = ClientConfig::from_env()
81 .map_err(|_| Error::Config("XAI_API_KEY environment variable not set".to_string()))?;
82 Self::with_config(config)
83 }
84
85 pub fn builder() -> XaiClientBuilder {
87 XaiClientBuilder::new()
88 }
89
90 pub fn sync(&self) -> Result<SyncXaiClient> {
95 SyncXaiClient::with_async_client(self.clone())
96 }
97
98 pub fn into_sync(self) -> Result<SyncXaiClient> {
103 SyncXaiClient::with_async_client(self)
104 }
105
106 pub fn with_config(config: ClientConfig) -> Result<Self> {
108 Self::with_config_and_retry(config, RetryPolicy::default())
109 }
110
111 pub(crate) fn with_config_and_retry(
112 config: ClientConfig,
113 retry_policy: RetryPolicy,
114 ) -> Result<Self> {
115 let mut headers = HeaderMap::new();
116 headers.insert(
117 AUTHORIZATION,
118 HeaderValue::from_str(&format!("Bearer {}", config.api_key.expose()))
119 .map_err(|_| Error::Config("Invalid API key format".to_string()))?,
120 );
121 headers.insert(USER_AGENT, HeaderValue::from_static(SDK_USER_AGENT));
122
123 let http = reqwest::Client::builder()
124 .default_headers(headers)
125 .timeout(config.timeout)
126 .build()?;
127
128 Ok(Self {
129 inner: Arc::new(ClientInner {
130 config,
131 http,
132 retry_policy,
133 }),
134 })
135 }
136
137 pub fn base_url(&self) -> &str {
139 &self.inner.config.base_url
140 }
141
142 pub(crate) fn http(&self) -> &reqwest::Client {
144 &self.inner.http
145 }
146
147 pub(crate) fn retry_policy(&self) -> RetryPolicy {
148 self.inner.retry_policy
149 }
150
151 pub(crate) async fn send(&self, request: RequestBuilder) -> Result<Response> {
152 self.send_with_retry_policy(request, None).await
153 }
154
155 pub(crate) async fn send_with_retry_policy(
156 &self,
157 request: RequestBuilder,
158 retry_policy_override: Option<RetryPolicy>,
159 ) -> Result<Response> {
160 let retry_policy = retry_policy_override.unwrap_or(self.inner.retry_policy);
161
162 #[cfg(feature = "telemetry")]
163 let (request_method, request_url) = request_telemetry_fields(&request);
164
165 let mut current = Some(request);
166
167 for attempt in 0..=retry_policy.max_retries {
168 #[cfg(feature = "telemetry")]
169 debug!(
170 attempt,
171 max_retries = retry_policy.max_retries,
172 method = %request_method,
173 url = %request_url,
174 "xai-rust request attempt"
175 );
176
177 let req = current
178 .take()
179 .ok_or_else(|| Error::Config("Request cannot be retried".to_string()))?;
180 let retry_clone = req.try_clone();
181
182 match req.send().await {
183 Ok(response) => {
184 if attempt < retry_policy.max_retries
185 && is_retryable_status(response.status())
186 && retry_clone.is_some()
187 {
188 let delay = retry_delay_from_response(
189 &retry_policy,
190 attempt,
191 response.status(),
192 response.headers().get("retry-after"),
193 );
194 let delay = apply_jitter(delay, retry_policy.jitter_factor);
195 #[cfg(feature = "telemetry")]
196 warn!(
197 attempt,
198 status = response.status().as_u16(),
199 delay_ms = delay.as_millis() as u64,
200 method = %request_method,
201 url = %request_url,
202 "xai-rust retrying request after retryable status"
203 );
204 sleep(delay).await;
205 current = retry_clone;
206 continue;
207 }
208 return Ok(response);
209 }
210 Err(err) => {
211 if attempt < retry_policy.max_retries && is_retryable_transport_error(&err) {
212 if let Some(next) = retry_clone {
213 let delay = exponential_backoff_delay(&retry_policy, attempt);
214 let delay = apply_jitter(delay, retry_policy.jitter_factor);
215 #[cfg(feature = "telemetry")]
216 warn!(
217 attempt,
218 delay_ms = delay.as_millis() as u64,
219 error = %err,
220 method = %request_method,
221 url = %request_url,
222 "xai-rust retrying request after transport error"
223 );
224 sleep(delay).await;
225 current = Some(next);
226 continue;
227 }
228 }
229 #[cfg(feature = "telemetry")]
230 warn!(
231 error = %err,
232 method = %request_method,
233 url = %request_url,
234 "xai-rust request failed"
235 );
236 return Err(Error::Http(err));
237 }
238 }
239 }
240
241 Err(Error::Timeout)
242 }
243
244 #[cfg(any(feature = "realtime", test))]
246 pub(crate) fn api_key(&self) -> &str {
247 self.inner.config.api_key.expose()
248 }
249
250 pub(crate) fn encode_path(segment: &str) -> String {
252 if is_unreserved_path_segment(segment.as_bytes()) {
253 return segment.to_owned();
254 }
255 encode_path_segment_slow(segment.as_bytes())
256 }
257
258 pub fn responses(&self) -> ResponsesApi {
281 ResponsesApi::new(self.clone())
282 }
283
284 pub fn auth(&self) -> AuthApi {
299 AuthApi::new(self.clone())
300 }
301
302 pub fn chat(&self) -> ChatApi {
306 ChatApi::new(self.clone())
307 }
308
309 pub fn images(&self) -> ImagesApi {
327 ImagesApi::new(self.clone())
328 }
329
330 pub fn videos(&self) -> VideosApi {
345 VideosApi::new(self.clone())
346 }
347
348 #[cfg(feature = "files")]
363 pub fn files(&self) -> FilesApi {
364 FilesApi::new(self.clone())
365 }
366
367 pub fn models(&self) -> ModelsApi {
382 ModelsApi::new(self.clone())
383 }
384
385 #[cfg(feature = "realtime")]
405 pub fn realtime(&self) -> RealtimeApi {
406 RealtimeApi::new(self.clone())
407 }
408
409 pub fn batch(&self) -> BatchApi {
425 BatchApi::new(self.clone())
426 }
427
428 pub fn collections(&self) -> CollectionsApi {
444 CollectionsApi::new(self.clone())
445 }
446
447 pub fn documents(&self) -> DocumentsApi {
463 DocumentsApi::new(self.clone())
464 }
465
466 pub fn embeddings(&self) -> EmbeddingsApi {
487 EmbeddingsApi::new(self.clone())
488 }
489
490 pub fn tokenizer(&self) -> TokenizerApi {
509 TokenizerApi::new(self.clone())
510 }
511}
512
513fn is_retryable_status(status: StatusCode) -> bool {
514 matches!(status.as_u16(), 408 | 429 | 500 | 502 | 503 | 504)
515}
516
517fn is_retryable_transport_error(err: &reqwest::Error) -> bool {
518 err.is_timeout() || err.is_connect() || err.is_request()
519}
520
521fn exponential_backoff_delay(policy: &RetryPolicy, attempt: u32) -> Duration {
522 let max_millis = policy.max_backoff.as_millis();
523 let initial_millis = policy.initial_backoff.as_millis();
524 if max_millis == 0 || initial_millis == 0 {
525 return Duration::ZERO;
526 }
527
528 let factor_shift = attempt.min(16);
529 let factor = 1u128 << factor_shift;
530 let delayed = initial_millis.saturating_mul(factor).min(max_millis);
531 Duration::from_millis(delayed as u64)
532}
533
534fn retry_delay_from_response(
535 policy: &RetryPolicy,
536 attempt: u32,
537 status: StatusCode,
538 retry_after: Option<&HeaderValue>,
539) -> Duration {
540 if is_retryable_status(status) {
541 if let Some(delay) = retry_after.and_then(parse_retry_after_delay) {
542 return delay.min(policy.max_backoff);
543 }
544 }
545
546 exponential_backoff_delay(policy, attempt)
547}
548
549fn parse_retry_after_delay(retry_after: &HeaderValue) -> Option<Duration> {
550 let value = retry_after.to_str().ok()?.trim();
551 if value.is_empty() {
552 return None;
553 }
554
555 if let Ok(seconds) = value.parse::<u64>() {
556 return Some(Duration::from_secs(seconds));
557 }
558
559 let retry_at = parse_http_date(value).ok()?;
560 Some(
561 retry_at
562 .duration_since(SystemTime::now())
563 .unwrap_or(Duration::ZERO),
564 )
565}
566
567fn apply_jitter(delay: Duration, factor: f64) -> Duration {
568 if delay.is_zero() || factor <= 0.0 {
569 return delay;
570 }
571
572 let clamped_factor = factor.clamp(0.0, 1.0);
573 let delay_secs = delay.as_secs_f64();
574 let half_range = delay_secs * clamped_factor * 0.5;
575 if half_range <= f64::EPSILON {
576 return delay;
577 }
578
579 let min = (delay_secs - half_range).max(0.0);
580 let max = delay_secs + half_range;
581 let sample = SystemTime::now()
582 .duration_since(UNIX_EPOCH)
583 .map(|d| d.subsec_nanos() as f64 / 1_000_000_000.0)
584 .unwrap_or(0.5);
585 Duration::from_secs_f64(min + (max - min) * sample)
586}
587
588#[inline]
589fn is_unreserved_path_segment(segment: &[u8]) -> bool {
590 let mut i = 0;
591 while i < segment.len() {
592 let b = segment[i];
593 if !matches!(
594 b,
595 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
596 ) {
597 return false;
598 }
599 i += 1;
600 }
601 true
602}
603
604#[inline]
605fn encode_path_segment_slow(segment: &[u8]) -> String {
606 let mut encoded = String::with_capacity(segment.len() + 8);
607 for &b in segment {
608 if should_percent_encode_path_byte(b) {
609 encoded.push('%');
610 encoded.push(HEX_UPPER[(b >> 4) as usize] as char);
611 encoded.push(HEX_UPPER[(b & 0x0F) as usize] as char);
612 } else {
613 encoded.push(b as char);
614 }
615 }
616 encoded
617}
618
619#[inline]
620fn should_percent_encode_path_byte(b: u8) -> bool {
621 b >= 0x80
622 || matches!(
623 b,
624 0x00..=0x20
625 | b'"'
626 | b'#'
627 | b'%'
628 | b'/'
629 | b'<'
630 | b'>'
631 | b'?'
632 | b'['
633 | b'\\'
634 | b']'
635 | b'^'
636 | b'`'
637 | b'{'
638 | b'|'
639 | b'}'
640 )
641}
642
643#[cfg(feature = "opt-bench-internals")]
644#[doc(hidden)]
645pub fn __opt_bench_encode_path(segment: &str) -> String {
646 XaiClient::encode_path(segment)
647}
648
649#[cfg(feature = "telemetry")]
650fn request_telemetry_fields(request: &RequestBuilder) -> (String, String) {
651 request
652 .try_clone()
653 .and_then(|builder| builder.build().ok())
654 .map(|built| (built.method().to_string(), built.url().to_string()))
655 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()))
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::config::DEFAULT_BASE_URL;
662 use httpdate::fmt_http_date;
663 use serde_json::json;
664 use std::sync::{
665 atomic::{AtomicUsize, Ordering},
666 Arc,
667 };
668 use wiremock::matchers::{header, method, path};
669 use wiremock::{Mock, MockServer, ResponseTemplate};
670
671 #[test]
674 fn client_new_returns_ok() {
675 let client = XaiClient::new("test-api-key-1234");
676 assert!(client.is_ok());
677 }
678
679 #[test]
680 fn client_new_sets_base_url() {
681 let client = XaiClient::new("test-key").unwrap();
682 assert_eq!(client.base_url(), DEFAULT_BASE_URL);
683 }
684
685 #[test]
686 fn client_new_preserves_api_key() {
687 let client = XaiClient::new("my-secret-key").unwrap();
688 assert_eq!(client.api_key(), "my-secret-key");
689 }
690
691 #[test]
692 fn client_clone_shares_inner() {
693 let client1 = XaiClient::new("key").unwrap();
694 let client2 = client1.clone();
695 assert_eq!(client1.base_url(), client2.base_url());
696 assert_eq!(client1.api_key(), client2.api_key());
697 }
698
699 #[test]
702 fn client_builder_returns_builder() {
703 let builder = XaiClient::builder();
704 let debug = format!("{:?}", builder);
706 assert!(debug.contains("XaiClientBuilder"));
707 }
708
709 #[test]
710 fn client_from_builder() {
711 let client = XaiClient::builder().api_key("builder-key").build().unwrap();
712 assert_eq!(client.api_key(), "builder-key");
713 assert_eq!(client.base_url(), DEFAULT_BASE_URL);
714 }
715
716 #[test]
717 fn client_from_builder_custom_url() {
718 let client = XaiClient::builder()
719 .api_key("key")
720 .base_url("https://custom.api.com/v2")
721 .build()
722 .unwrap();
723 assert_eq!(client.base_url(), "https://custom.api.com/v2");
724 }
725
726 #[test]
727 fn client_from_builder_custom_url_trims_trailing_slash() {
728 let client = XaiClient::builder()
729 .api_key("key")
730 .base_url("https://custom.api.com/v2/")
731 .build()
732 .unwrap();
733 assert_eq!(client.base_url(), "https://custom.api.com/v2");
734 }
735
736 #[test]
739 fn client_with_config() {
740 let config = ClientConfig::new("config-key");
741 let client = XaiClient::with_config(config).unwrap();
742 assert_eq!(client.api_key(), "config-key");
743 }
744
745 #[tokio::test]
746 async fn client_sets_default_user_agent_header() {
747 let server = MockServer::start().await;
748 Mock::given(method("GET"))
749 .and(path("/models"))
750 .and(header("user-agent", SDK_USER_AGENT))
751 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
752 "object": "list",
753 "data": []
754 })))
755 .expect(1)
756 .mount(&server)
757 .await;
758
759 let client = XaiClient::builder()
760 .api_key("test-key")
761 .base_url(server.uri())
762 .build()
763 .unwrap();
764
765 let response = client.models().list().await.unwrap();
766 assert_eq!(response.object, "list");
767 assert!(response.data.is_empty());
768 }
769
770 #[tokio::test]
771 async fn client_retries_transient_http_statuses() {
772 let server = MockServer::start().await;
773 let call_count = Arc::new(AtomicUsize::new(0));
774 let responder_count = Arc::clone(&call_count);
775
776 Mock::given(method("GET"))
777 .and(path("/models"))
778 .respond_with(move |_req: &wiremock::Request| {
779 let count = responder_count.fetch_add(1, Ordering::SeqCst);
780 if count == 0 {
781 ResponseTemplate::new(503).set_body_json(json!({
782 "error": {"message": "temporary", "type": "server_error"}
783 }))
784 } else {
785 ResponseTemplate::new(200).set_body_json(json!({
786 "object": "list",
787 "data": []
788 }))
789 }
790 })
791 .mount(&server)
792 .await;
793
794 let client = XaiClient::builder()
795 .api_key("test-key")
796 .base_url(server.uri())
797 .max_retries(1)
798 .retry_backoff(Duration::ZERO, Duration::ZERO)
799 .build()
800 .unwrap();
801
802 let list = client.models().list().await.unwrap();
803 assert_eq!(list.object, "list");
804 assert_eq!(call_count.load(Ordering::SeqCst), 2);
805 }
806
807 #[tokio::test]
808 async fn client_disable_retries_returns_first_error_response() {
809 let server = MockServer::start().await;
810
811 Mock::given(method("GET"))
812 .and(path("/models"))
813 .respond_with(ResponseTemplate::new(503).set_body_json(json!({
814 "error": {"message": "service unavailable", "type": "server_error"}
815 })))
816 .expect(1)
817 .mount(&server)
818 .await;
819
820 let client = XaiClient::builder()
821 .api_key("test-key")
822 .base_url(server.uri())
823 .disable_retries()
824 .build()
825 .unwrap();
826
827 let err = client.models().list().await.unwrap_err();
828 assert!(matches!(err, Error::Api { status: 503, .. }));
829 }
830
831 #[test]
834 fn encode_path_no_special_chars() {
835 let encoded = XaiClient::encode_path("simple-path");
836 assert_eq!(encoded, "simple-path");
837 }
838
839 #[test]
840 fn encode_path_with_spaces() {
841 let encoded = XaiClient::encode_path("path with spaces");
842 assert_eq!(encoded, "path%20with%20spaces");
843 }
844
845 #[test]
846 fn encode_path_with_slashes() {
847 let encoded = XaiClient::encode_path("a/b/c");
848 assert_eq!(encoded, "a%2Fb%2Fc");
849 }
850
851 #[test]
852 fn encode_path_with_special_chars() {
853 let encoded = XaiClient::encode_path("file@name#1");
854 assert!(encoded.contains("%40") || encoded.contains("@")); assert!(encoded.contains("%23")); }
857
858 #[test]
859 fn encode_path_with_unicode() {
860 let encoded = XaiClient::encode_path("héllo world");
861 assert_eq!(encoded, "h%C3%A9llo%20world");
862 }
863
864 #[test]
865 fn encode_path_empty_string() {
866 let encoded = XaiClient::encode_path("");
867 assert_eq!(encoded, "");
868 }
869
870 #[test]
871 fn encode_path_with_percent() {
872 let encoded = XaiClient::encode_path("100%done");
873 assert!(encoded.contains("%25")); }
875
876 #[test]
877 fn apply_jitter_zero_factor_returns_same_delay() {
878 let delay = Duration::from_millis(800);
879 assert_eq!(apply_jitter(delay, 0.0), delay);
880 }
881
882 #[test]
883 fn apply_jitter_stays_within_expected_bounds() {
884 let delay = Duration::from_millis(1000);
885 let jittered = apply_jitter(delay, 0.2);
886 assert!(jittered >= Duration::from_millis(900));
887 assert!(jittered <= Duration::from_millis(1100));
888 }
889
890 #[test]
891 fn retry_delay_from_response_uses_retry_after_seconds_for_retryable_status() {
892 let policy = RetryPolicy {
893 max_retries: 2,
894 initial_backoff: Duration::from_millis(200),
895 max_backoff: Duration::from_secs(10),
896 jitter_factor: 0.0,
897 };
898 let retry_after = HeaderValue::from_static("7");
899
900 let delay = retry_delay_from_response(
901 &policy,
902 0,
903 StatusCode::SERVICE_UNAVAILABLE,
904 Some(&retry_after),
905 );
906 assert_eq!(delay, Duration::from_secs(7));
907 }
908
909 #[test]
910 fn retry_delay_from_response_parses_http_date_retry_after() {
911 let policy = RetryPolicy {
912 max_retries: 2,
913 initial_backoff: Duration::from_millis(200),
914 max_backoff: Duration::from_secs(5),
915 jitter_factor: 0.0,
916 };
917 let retry_after =
918 HeaderValue::from_str(&fmt_http_date(SystemTime::now() + Duration::from_secs(600)))
919 .unwrap();
920
921 let delay = retry_delay_from_response(
922 &policy,
923 0,
924 StatusCode::TOO_MANY_REQUESTS,
925 Some(&retry_after),
926 );
927 assert_eq!(delay, policy.max_backoff);
928 }
929
930 #[test]
931 fn retry_delay_from_response_with_past_http_date_returns_zero() {
932 let policy = RetryPolicy {
933 max_retries: 2,
934 initial_backoff: Duration::from_millis(200),
935 max_backoff: Duration::from_secs(5),
936 jitter_factor: 0.0,
937 };
938 let retry_after = HeaderValue::from_str(&fmt_http_date(UNIX_EPOCH)).unwrap();
939
940 let delay = retry_delay_from_response(
941 &policy,
942 1,
943 StatusCode::TOO_MANY_REQUESTS,
944 Some(&retry_after),
945 );
946 assert_eq!(delay, Duration::ZERO);
947 }
948
949 #[test]
950 fn retry_delay_from_response_invalid_retry_after_falls_back_to_exponential() {
951 let policy = RetryPolicy {
952 max_retries: 2,
953 initial_backoff: Duration::from_millis(250),
954 max_backoff: Duration::from_secs(10),
955 jitter_factor: 0.0,
956 };
957 let retry_after = HeaderValue::from_static("invalid");
958
959 let delay = retry_delay_from_response(
960 &policy,
961 2,
962 StatusCode::TOO_MANY_REQUESTS,
963 Some(&retry_after),
964 );
965 assert_eq!(delay, Duration::from_millis(1000));
966 }
967
968 #[cfg(feature = "telemetry")]
969 #[test]
970 fn request_telemetry_fields_extract_method_and_url() {
971 let http = reqwest::Client::new();
972 let request = http.post("https://example.com/v1/test");
973 let (method, url) = request_telemetry_fields(&request);
974 assert_eq!(method, "POST");
975 assert_eq!(url, "https://example.com/v1/test");
976 }
977
978 #[test]
981 fn client_responses_api() {
982 let client = XaiClient::new("key").unwrap();
983 let _api = client.responses(); }
985
986 #[test]
987 fn client_auth_api() {
988 let client = XaiClient::new("key").unwrap();
989 let _api = client.auth();
990 }
991
992 #[test]
993 fn client_sync_facade_from_ref() {
994 let client = XaiClient::new("key").unwrap();
995 let sync = client.sync().unwrap();
996 assert_eq!(sync.base_url(), client.base_url());
997 }
998
999 #[test]
1000 fn client_into_sync_facade() {
1001 let client = XaiClient::new("key").unwrap();
1002 let sync = client.into_sync().unwrap();
1003 assert_eq!(sync.base_url(), DEFAULT_BASE_URL);
1004 }
1005
1006 #[test]
1007 fn client_chat_api() {
1008 let client = XaiClient::new("key").unwrap();
1009 let _api = client.chat();
1010 }
1011
1012 #[test]
1013 fn client_images_api() {
1014 let client = XaiClient::new("key").unwrap();
1015 let _api = client.images();
1016 }
1017
1018 #[test]
1019 fn client_videos_api() {
1020 let client = XaiClient::new("key").unwrap();
1021 let _api = client.videos();
1022 }
1023
1024 #[test]
1025 fn client_models_api() {
1026 let client = XaiClient::new("key").unwrap();
1027 let _api = client.models();
1028 }
1029
1030 #[test]
1031 fn client_batch_api() {
1032 let client = XaiClient::new("key").unwrap();
1033 let _api = client.batch();
1034 }
1035
1036 #[test]
1037 fn client_collections_api() {
1038 let client = XaiClient::new("key").unwrap();
1039 let _api = client.collections();
1040 }
1041
1042 #[test]
1043 fn client_documents_api() {
1044 let client = XaiClient::new("key").unwrap();
1045 let _api = client.documents();
1046 }
1047
1048 #[test]
1049 fn client_embeddings_api() {
1050 let client = XaiClient::new("key").unwrap();
1051 let _api = client.embeddings();
1052 }
1053
1054 #[test]
1055 fn client_tokenizer_api() {
1056 let client = XaiClient::new("key").unwrap();
1057 let _api = client.tokenizer();
1058 }
1059
1060 #[cfg(feature = "files")]
1061 #[test]
1062 fn client_files_api() {
1063 let client = XaiClient::new("key").unwrap();
1064 let _api = client.files();
1065 }
1066
1067 #[cfg(feature = "realtime")]
1068 #[test]
1069 fn client_realtime_api() {
1070 let client = XaiClient::new("key").unwrap();
1071 let _api = client.realtime();
1072 }
1073}