1pub mod audio_generation;
5pub mod completion;
6pub mod embeddings;
7pub mod image_generation;
8pub mod model_listing;
9pub mod rerank;
10pub mod transcription;
11pub mod verify;
12
13use bytes::Bytes;
14pub use completion::CompletionClient;
15pub use embeddings::EmbeddingsClient;
16use http::{HeaderMap, HeaderName, HeaderValue};
17pub use model_listing::{ModelLister, ModelListingClient};
18pub use rerank::RerankingClient;
19use std::{env::VarError, fmt::Debug, marker::PhantomData, sync::Arc};
20use thiserror::Error;
21pub use verify::{VerifyClient, VerifyError};
22
23#[cfg(feature = "image")]
24use crate::image_generation::ImageGenerationModel;
25#[cfg(feature = "image")]
26use image_generation::ImageGenerationClient;
27
28#[cfg(feature = "audio")]
29use crate::audio_generation::*;
30#[cfg(feature = "audio")]
31use audio_generation::*;
32
33use crate::{
34 completion::CompletionModel,
35 embeddings::EmbeddingModel,
36 http_client::{
37 self, Builder, HttpClientExt, LazyBody, MultipartForm, Request, Response, make_auth_header,
38 },
39 markers::Missing,
40 prelude::TranscriptionClient,
41 rerank::RerankModel,
42 transcription::TranscriptionModel,
43 wasm_compat::{WasmCompatSend, WasmCompatSync},
44};
45
46#[derive(Debug, Error)]
47#[non_exhaustive]
48pub enum ClientBuilderError {
49 #[error("reqwest error: {0}")]
51 HttpError(
52 #[from]
53 #[source]
54 reqwest::Error,
55 ),
56 #[error("invalid property: {0}")]
58 InvalidProperty(&'static str),
59}
60
61#[derive(Debug, Error)]
67#[non_exhaustive]
68pub enum ProviderClientError {
69 #[error("environment variable `{name}` is not set or is invalid")]
73 EnvironmentVariable {
74 name: &'static str,
76 #[source]
78 source: VarError,
79 },
80 #[error(transparent)]
82 Http(#[from] http_client::Error),
83 #[error("{0}")]
85 InvalidConfiguration(&'static str),
86}
87
88pub type ProviderClientResult<T> = std::result::Result<T, ProviderClientError>;
90
91pub fn required_env_var(name: &'static str) -> ProviderClientResult<String> {
96 std::env::var(name).map_err(|source| ProviderClientError::EnvironmentVariable { name, source })
97}
98
99pub fn optional_env_var(name: &'static str) -> ProviderClientResult<Option<String>> {
104 match std::env::var(name) {
105 Ok(value) => Ok(Some(value)),
106 Err(VarError::NotPresent) => Ok(None),
107 Err(source) => Err(ProviderClientError::EnvironmentVariable { name, source }),
108 }
109}
110
111pub trait ProviderClient {
114 type Input;
116 type Error;
118
119 fn from_env() -> Result<Self, Self::Error>
121 where
122 Self: Sized;
123
124 fn from_val(input: Self::Input) -> Result<Self, Self::Error>
126 where
127 Self: Sized;
128}
129
130pub trait ApiKey: Sized {
135 fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
138 None
139 }
140}
141
142pub struct BearerAuth(String);
144
145impl ApiKey for BearerAuth {
146 fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
147 Some(make_auth_header(self.0))
148 }
149}
150
151impl<S> From<S> for BearerAuth
152where
153 S: Into<String>,
154{
155 fn from(value: S) -> Self {
156 Self(value.into())
157 }
158}
159
160#[derive(Debug, Default, Clone, Copy)]
163pub struct Nothing;
164
165impl ApiKey for Nothing {}
166
167#[derive(Clone)]
168pub struct Client<Ext = Nothing, H = reqwest::Client> {
174 base_url: Arc<str>,
175 headers: Arc<HeaderMap>,
176 http_client: H,
177 ext: Ext,
178}
179
180pub trait DebugExt: Debug {
182 fn fields(&self) -> impl Iterator<Item = (&'static str, &dyn Debug)> {
184 std::iter::empty()
185 }
186}
187
188impl<Ext, H> std::fmt::Debug for Client<Ext, H>
189where
190 Ext: DebugExt,
191 H: std::fmt::Debug,
192{
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 let mut d = &mut f.debug_struct("Client");
195
196 d = d
197 .field("base_url", &self.base_url)
198 .field(
199 "headers",
200 &self
201 .headers
202 .iter()
203 .filter_map(|(k, v)| {
204 if k == http::header::AUTHORIZATION || k.as_str().contains("api-key") {
205 None
206 } else {
207 Some((k, v))
208 }
209 })
210 .collect::<Vec<(&HeaderName, &HeaderValue)>>(),
211 )
212 .field("http_client", &self.http_client);
213
214 self.ext
215 .fields()
216 .fold(d, |d, (name, field)| d.field(name, field))
217 .finish()
218 }
219}
220
221pub enum Transport {
222 Http,
224 Sse,
226}
227
228pub trait Provider: Sized {
232 type Builder: ProviderBuilder;
235
236 const VERIFY_PATH: &'static str;
238
239 fn build_uri(&self, base_url: &str, path: &str, _transport: Transport) -> String {
241 let base_url = if base_url.is_empty() || base_url.ends_with('/') {
243 base_url.to_string()
244 } else {
245 base_url.to_string() + "/"
247 };
248
249 base_url + path.trim_start_matches('/')
250 }
251
252 fn with_custom(&self, req: http_client::Builder) -> http_client::Result<http_client::Builder> {
254 Ok(req)
255 }
256}
257
258pub struct Capable<M>(PhantomData<M>);
260
261pub trait Capability {
263 const CAPABLE: bool;
265}
266
267impl<M> Capability for Capable<M> {
268 const CAPABLE: bool = true;
269}
270
271impl Capability for Nothing {
272 const CAPABLE: bool = false;
273}
274
275pub trait Capabilities<H = reqwest::Client> {
277 type Completion: Capability;
279 type Embeddings: Capability;
281 type Rerank: Capability;
283 type Transcription: Capability;
285 type ModelListing: Capability;
287 #[cfg(feature = "image")]
288 type ImageGeneration: Capability;
290 #[cfg(feature = "audio")]
291 type AudioGeneration: Capability;
293}
294
295pub trait ProviderBuilder: Sized + Default + Clone {
300 type Extension<H>: Provider
302 where
303 H: HttpClientExt;
304 type ApiKey: ApiKey;
306
307 const BASE_URL: &'static str;
309
310 fn build<H>(
312 builder: &ClientBuilder<Self, Self::ApiKey, H>,
313 ) -> http_client::Result<Self::Extension<H>>
314 where
315 H: HttpClientExt;
316
317 fn finish<H>(
320 &self,
321 builder: ClientBuilder<Self, Self::ApiKey, H>,
322 ) -> http_client::Result<ClientBuilder<Self, Self::ApiKey, H>> {
323 Ok(builder)
324 }
325}
326
327impl<Ext> Client<Ext, reqwest::Client>
331where
332 Ext: Provider,
333 Ext::Builder: ProviderBuilder<Extension<reqwest::Client> = Ext> + Default,
334{
335 pub fn new(
337 api_key: impl Into<<Ext::Builder as ProviderBuilder>::ApiKey>,
338 ) -> http_client::Result<Self> {
339 Self::builder().api_key(api_key).build()
340 }
341}
342
343impl<Ext, H> Client<Ext, H> {
344 pub fn base_url(&self) -> &str {
346 &self.base_url
347 }
348
349 pub fn headers(&self) -> &HeaderMap {
351 &self.headers
352 }
353
354 pub fn ext(&self) -> &Ext {
356 &self.ext
357 }
358
359 pub fn with_ext<NewExt>(self, new_ext: NewExt) -> Client<NewExt, H> {
361 Client {
362 base_url: self.base_url,
363 headers: self.headers,
364 http_client: self.http_client,
365 ext: new_ext,
366 }
367 }
368}
369
370impl<Ext, H> HttpClientExt for Client<Ext, H>
371where
372 H: HttpClientExt + 'static,
373 Ext: WasmCompatSend + WasmCompatSync + 'static,
374{
375 fn send<T, U>(
376 &self,
377 mut req: Request<T>,
378 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
379 where
380 T: Into<Bytes> + WasmCompatSend,
381 U: From<Bytes>,
382 U: WasmCompatSend + 'static,
383 {
384 req.headers_mut().insert(
385 http::header::CONTENT_TYPE,
386 http::HeaderValue::from_static("application/json"),
387 );
388
389 self.http_client.send(req)
390 }
391
392 fn send_multipart<U>(
393 &self,
394 req: Request<MultipartForm>,
395 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
396 where
397 U: From<Bytes>,
398 U: WasmCompatSend + 'static,
399 {
400 self.http_client.send_multipart(req)
401 }
402
403 fn send_streaming<T>(
404 &self,
405 mut req: Request<T>,
406 ) -> impl Future<Output = http_client::Result<http_client::StreamingResponse>> + WasmCompatSend
407 where
408 T: Into<Bytes> + WasmCompatSend,
409 {
410 req.headers_mut().insert(
411 http::header::CONTENT_TYPE,
412 http::HeaderValue::from_static("application/json"),
413 );
414
415 self.http_client.send_streaming(req)
416 }
417}
418
419impl<Ext> Client<Ext, reqwest::Client>
425where
426 Ext: Provider,
427 Ext::Builder: ProviderBuilder + Default,
428{
429 pub fn builder() -> ClientBuilder<Ext::Builder, Missing, Missing> {
431 ClientBuilder::default()
432 }
433}
434
435impl<Ext, H> Client<Ext, H>
436where
437 Ext: Provider,
438{
439 pub fn post<S>(&self, path: S) -> http_client::Result<Builder>
441 where
442 S: AsRef<str>,
443 {
444 let uri = self
445 .ext
446 .build_uri(&self.base_url, path.as_ref(), Transport::Http);
447
448 let mut req = Request::post(uri);
449
450 if let Some(hs) = req.headers_mut() {
451 hs.extend(self.headers.iter().map(|(k, v)| (k.clone(), v.clone())));
452 }
453
454 self.ext.with_custom(req)
455 }
456
457 pub fn post_sse<S>(&self, path: S) -> http_client::Result<Builder>
459 where
460 S: AsRef<str>,
461 {
462 let uri = self
463 .ext
464 .build_uri(&self.base_url, path.as_ref(), Transport::Sse);
465
466 let mut req = Request::post(uri);
467
468 if let Some(hs) = req.headers_mut() {
469 hs.extend(self.headers.iter().map(|(k, v)| (k.clone(), v.clone())));
470 }
471
472 self.ext.with_custom(req)
473 }
474
475 pub fn get_sse<S>(&self, path: S) -> http_client::Result<Builder>
477 where
478 S: AsRef<str>,
479 {
480 let uri = self
481 .ext
482 .build_uri(&self.base_url, path.as_ref(), Transport::Sse);
483
484 let mut req = Request::get(uri);
485
486 if let Some(hs) = req.headers_mut() {
487 hs.extend(self.headers.iter().map(|(k, v)| (k.clone(), v.clone())));
488 }
489
490 self.ext.with_custom(req)
491 }
492
493 pub fn get<S>(&self, path: S) -> http_client::Result<Builder>
495 where
496 S: AsRef<str>,
497 {
498 let uri = self
499 .ext
500 .build_uri(&self.base_url, path.as_ref(), Transport::Http);
501
502 let mut req = Request::get(uri);
503
504 if let Some(hs) = req.headers_mut() {
505 hs.extend(self.headers.iter().map(|(k, v)| (k.clone(), v.clone())));
506 }
507
508 self.ext.with_custom(req)
509 }
510}
511
512impl<Ext, H> VerifyClient for Client<Ext, H>
513where
514 H: HttpClientExt,
515 Ext: DebugExt + Provider + WasmCompatSync,
516{
517 async fn verify(&self) -> Result<(), VerifyError> {
518 use http::StatusCode;
519
520 let req = self
521 .get(Ext::VERIFY_PATH)?
522 .body(http_client::NoBody)
523 .map_err(http_client::Error::from)?;
524
525 let response = self.http_client.send(req).await?;
526
527 match response.status() {
528 StatusCode::OK => Ok(()),
529 StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN => {
530 Err(VerifyError::InvalidAuthentication)
531 }
532 StatusCode::INTERNAL_SERVER_ERROR => {
533 let text = http_client::text(response).await?;
534 Err(VerifyError::HttpError(
535 http_client::Error::InvalidStatusCodeWithMessage(
536 StatusCode::INTERNAL_SERVER_ERROR,
537 text,
538 ),
539 ))
540 }
541 status if status.as_u16() == 529 => {
542 let text = http_client::text(response).await?;
543 Err(VerifyError::HttpError(
544 http_client::Error::InvalidStatusCodeWithMessage(status, text),
545 ))
546 }
547 _ => {
548 let status = response.status();
549
550 if status.is_success() {
551 Ok(())
552 } else {
553 let text: String = String::from_utf8_lossy(&response.into_body().await?).into();
554 Err(VerifyError::HttpError(
555 http_client::Error::InvalidStatusCodeWithMessage(status, text),
556 ))
557 }
558 }
559 }
560 }
561}
562
563#[derive(Clone)]
579pub struct ClientBuilder<Ext, ApiKey = Missing, H = Missing> {
580 base_url: String,
581 api_key: ApiKey,
582 headers: HeaderMap,
583 http_client: H,
584 ext: Ext,
585}
586
587impl<ExtBuilder> Default for ClientBuilder<ExtBuilder, Missing, Missing>
588where
589 ExtBuilder: ProviderBuilder + Default,
590{
591 fn default() -> Self {
592 Self {
593 api_key: Missing,
594 headers: Default::default(),
595 base_url: ExtBuilder::BASE_URL.into(),
596 http_client: Missing,
597 ext: Default::default(),
598 }
599 }
600}
601
602impl<Ext, H> ClientBuilder<Ext, Missing, H> {
603 pub fn api_key<ApiKey>(self, api_key: impl Into<ApiKey>) -> ClientBuilder<Ext, ApiKey, H> {
606 ClientBuilder {
607 api_key: api_key.into(),
608 base_url: self.base_url,
609 headers: self.headers,
610 http_client: self.http_client,
611 ext: self.ext,
612 }
613 }
614}
615
616impl<Ext, ApiKey, H> ClientBuilder<Ext, ApiKey, H>
617where
618 Ext: Clone,
619{
620 pub(crate) fn over_ext<F, NewExt>(self, f: F) -> ClientBuilder<NewExt, ApiKey, H>
622 where
623 F: FnOnce(Ext) -> NewExt,
624 {
625 let ClientBuilder {
626 base_url,
627 api_key,
628 headers,
629 http_client,
630 ext,
631 } = self;
632
633 let new_ext = f(ext.clone());
634
635 ClientBuilder {
636 base_url,
637 api_key,
638 headers,
639 http_client,
640 ext: new_ext,
641 }
642 }
643
644 pub fn base_url<S>(self, base_url: S) -> Self
646 where
647 S: AsRef<str>,
648 {
649 Self {
650 base_url: base_url.as_ref().to_string(),
651 ..self
652 }
653 }
654
655 pub fn http_client<U>(self, http_client: U) -> ClientBuilder<Ext, ApiKey, U> {
660 ClientBuilder {
661 http_client,
662 base_url: self.base_url,
663 api_key: self.api_key,
664 headers: self.headers,
665 ext: self.ext,
666 }
667 }
668
669 pub fn http_headers(self, headers: HeaderMap) -> Self {
671 Self { headers, ..self }
672 }
673
674 pub(crate) fn headers_mut(&mut self) -> &mut HeaderMap {
675 &mut self.headers
676 }
677
678 pub(crate) fn ext_mut(&mut self) -> &mut Ext {
679 &mut self.ext
680 }
681}
682
683impl<Ext, ApiKey, H> ClientBuilder<Ext, ApiKey, H> {
684 pub(crate) fn get_api_key(&self) -> &ApiKey {
685 &self.api_key
686 }
687}
688
689impl<Ext, Key, H> ClientBuilder<Ext, Key, H> {
690 pub fn ext(&self) -> &Ext {
692 &self.ext
693 }
694
695 pub fn get_base_url(&self) -> &str {
697 &self.base_url
698 }
699}
700
701impl<ExtBuilder, Key> ClientBuilder<ExtBuilder, Key, Missing>
707where
708 ExtBuilder: ProviderBuilder<ApiKey = Key>,
709 Key: ApiKey,
710{
711 pub fn build(
713 self,
714 ) -> http_client::Result<Client<ExtBuilder::Extension<reqwest::Client>, reqwest::Client>> {
715 self.http_client(reqwest::Client::default()).build()
716 }
717}
718
719impl<ExtBuilder, Key, H> ClientBuilder<ExtBuilder, Key, H>
722where
723 ExtBuilder: ProviderBuilder<ApiKey = Key>,
724 Key: ApiKey,
725 H: HttpClientExt,
726{
727 pub fn build(mut self) -> http_client::Result<Client<ExtBuilder::Extension<H>, H>> {
729 let ext_builder = self.ext.clone();
730
731 self = ext_builder.finish(self)?;
732 let ext = ExtBuilder::build(&self)?;
733
734 let ClientBuilder {
735 http_client,
736 base_url,
737 mut headers,
738 api_key,
739 ..
740 } = self;
741
742 if let Some((k, v)) = api_key.into_header().transpose()?
743 && !headers.contains_key(&k)
744 {
745 headers.insert(k, v);
746 }
747
748 Ok(Client {
749 http_client,
750 base_url: Arc::from(base_url.as_str()),
751 headers: Arc::new(headers),
752 ext,
753 })
754 }
755}
756
757impl<M, Ext, H> CompletionClient for Client<Ext, H>
758where
759 Ext: Capabilities<H, Completion = Capable<M>>,
760 M: CompletionModel<Client = Self>,
761{
762 type CompletionModel = M;
763
764 fn completion_model(&self, model: impl Into<String>) -> Self::CompletionModel {
765 M::make(self, model)
766 }
767}
768
769impl<M, Ext, H> EmbeddingsClient for Client<Ext, H>
770where
771 Ext: Capabilities<H, Embeddings = Capable<M>>,
772 M: EmbeddingModel<Client = Self>,
773{
774 type EmbeddingModel = M;
775
776 fn embedding_model(&self, model: impl Into<String>) -> Self::EmbeddingModel {
777 M::make(self, model, None)
778 }
779
780 fn embedding_model_with_ndims(
781 &self,
782 model: impl Into<String>,
783 ndims: usize,
784 ) -> Self::EmbeddingModel {
785 M::make(self, model, Some(ndims))
786 }
787}
788
789impl<M, Ext, H> RerankingClient for Client<Ext, H>
790where
791 Ext: Capabilities<H, Rerank = Capable<M>>,
792 M: RerankModel<Client = Self>,
793{
794 type RerankModel = M;
795
796 fn rerank_model(&self, model: impl Into<String>) -> Self::RerankModel {
797 M::make(self, model)
798 }
799}
800
801impl<M, Ext, H> TranscriptionClient for Client<Ext, H>
802where
803 Ext: Capabilities<H, Transcription = Capable<M>>,
804 M: TranscriptionModel<Client = Self> + WasmCompatSend,
805{
806 type TranscriptionModel = M;
807
808 fn transcription_model(&self, model: impl Into<String>) -> Self::TranscriptionModel {
809 M::make(self, model)
810 }
811}
812
813#[cfg(feature = "image")]
814impl<M, Ext, H> ImageGenerationClient for Client<Ext, H>
815where
816 Ext: Capabilities<H, ImageGeneration = Capable<M>>,
817 M: ImageGenerationModel<Client = Self>,
818{
819 type ImageGenerationModel = M;
820
821 fn image_generation_model(&self, model: impl Into<String>) -> Self::ImageGenerationModel {
822 M::make(self, model)
823 }
824}
825
826#[cfg(feature = "audio")]
827impl<M, Ext, H> AudioGenerationClient for Client<Ext, H>
828where
829 Ext: Capabilities<H, AudioGeneration = Capable<M>>,
830 M: AudioGenerationModel<Client = Self>,
831{
832 type AudioGenerationModel = M;
833
834 fn audio_generation_model(&self, model: impl Into<String>) -> Self::AudioGenerationModel {
835 M::make(self, model)
836 }
837}
838
839impl<M, Ext, H> ModelListingClient for Client<Ext, H>
840where
841 Ext: Capabilities<H, ModelListing = Capable<M>> + Clone,
842 M: ModelLister<H, Client = Self> + WasmCompatSend + WasmCompatSync + Clone + 'static,
843 H: WasmCompatSend + WasmCompatSync + Clone,
844{
845 fn list_models(
846 &self,
847 ) -> impl std::future::Future<
848 Output = Result<crate::model::ModelList, crate::model::ModelListingError>,
849 > + WasmCompatSend {
850 let lister = M::new(self.clone());
851 async move { lister.list_all().await }
852 }
853}
854
855#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
856mod wasm_model_listing_compile_checks {
857 use super::{ModelListingClient, Nothing};
858 use crate::{
859 http_client::{self, HttpClientExt, LazyBody, MultipartForm, Request, Response},
860 providers::{anthropic, deepseek, mistral, ollama, openai, openrouter},
861 wasm_compat::WasmCompatSend,
862 };
863 use bytes::Bytes;
864 use std::{
865 future::{self, Future},
866 marker::PhantomData,
867 rc::Rc,
868 };
869
870 #[derive(Clone, Default)]
871 struct WasmOnlyHttpClient {
872 _not_send_sync: PhantomData<Rc<()>>,
873 }
874
875 impl HttpClientExt for WasmOnlyHttpClient {
876 fn send<T, U>(
877 &self,
878 _req: Request<T>,
879 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
880 where
881 T: Into<Bytes> + WasmCompatSend,
882 U: From<Bytes> + WasmCompatSend + 'static,
883 {
884 future::ready(Err(http_client::Error::StreamEnded))
885 }
886
887 fn send_multipart<U>(
888 &self,
889 _req: Request<MultipartForm>,
890 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
891 where
892 U: From<Bytes> + WasmCompatSend + 'static,
893 {
894 future::ready(Err(http_client::Error::StreamEnded))
895 }
896
897 fn send_streaming<T>(
898 &self,
899 _req: Request<T>,
900 ) -> impl Future<Output = http_client::Result<http_client::StreamingResponse>> + WasmCompatSend
901 where
902 T: Into<Bytes> + WasmCompatSend,
903 {
904 future::ready(Err(http_client::Error::StreamEnded))
905 }
906 }
907
908 fn assert_model_listing_client<C>(client: C)
909 where
910 C: ModelListingClient,
911 {
912 let _ = client.list_models();
913 }
914
915 fn assert_simple_model_listers_accept_wasm_only_http_clients() {
916 let _ = openrouter::Client::builder()
917 .api_key("dummy-key")
918 .http_client(WasmOnlyHttpClient::default())
919 .build()
920 .map(assert_model_listing_client);
921
922 let _ = openai::Client::builder()
923 .api_key("dummy-key")
924 .http_client(WasmOnlyHttpClient::default())
925 .build()
926 .map(assert_model_listing_client);
927
928 let _ = mistral::Client::builder()
929 .api_key("dummy-key")
930 .http_client(WasmOnlyHttpClient::default())
931 .build()
932 .map(assert_model_listing_client);
933
934 let _ = anthropic::Client::builder()
935 .api_key("dummy-key")
936 .http_client(WasmOnlyHttpClient::default())
937 .build()
938 .map(assert_model_listing_client);
939
940 let _ = ollama::Client::builder()
941 .api_key(Nothing)
942 .http_client(WasmOnlyHttpClient::default())
943 .build()
944 .map(assert_model_listing_client);
945
946 let _ = deepseek::Client::builder()
947 .api_key("dummy-key")
948 .http_client(WasmOnlyHttpClient::default())
949 .build()
950 .map(assert_model_listing_client);
951 }
952
953 #[allow(dead_code)]
954 fn compile_assertions() {
955 assert_simple_model_listers_accept_wasm_only_http_clients();
956 }
957}
958
959#[cfg(test)]
960mod tests {
961 use crate::providers::anthropic;
962
963 #[test]
966 fn ensures_client_builder_no_annotation() {
967 let http_client = reqwest::Client::default();
968 let _ = anthropic::Client::builder()
969 .http_client(http_client)
970 .api_key("Foo")
971 .build()
972 .unwrap();
973 }
974}