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, ConstructCompletionModel};
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)]
47pub enum ClientBuilderError {
48 #[error("reqwest error: {0}")]
50 HttpError(
51 #[from]
52 #[source]
53 reqwest::Error,
54 ),
55 #[error("invalid property: {0}")]
57 InvalidProperty(&'static str),
58}
59
60#[derive(Debug, Error)]
66pub enum ProviderClientError {
67 #[error("environment variable `{name}` is not set or is invalid")]
71 EnvironmentVariable {
72 name: &'static str,
74 #[source]
76 source: VarError,
77 },
78 #[error(transparent)]
80 Http(#[from] http_client::Error),
81 #[error("{0}")]
83 InvalidConfiguration(&'static str),
84}
85
86pub type ProviderClientResult<T> = std::result::Result<T, ProviderClientError>;
88
89pub fn required_env_var(name: &'static str) -> ProviderClientResult<String> {
94 std::env::var(name).map_err(|source| ProviderClientError::EnvironmentVariable { name, source })
95}
96
97pub fn optional_env_var(name: &'static str) -> ProviderClientResult<Option<String>> {
102 match std::env::var(name) {
103 Ok(value) => Ok(Some(value)),
104 Err(VarError::NotPresent) => Ok(None),
105 Err(source) => Err(ProviderClientError::EnvironmentVariable { name, source }),
106 }
107}
108
109pub trait ProviderClient {
112 type Input;
114 type Error;
116
117 fn from_env() -> Result<Self, Self::Error>
119 where
120 Self: Sized;
121
122 fn from_val(input: Self::Input) -> Result<Self, Self::Error>
124 where
125 Self: Sized;
126}
127
128pub trait ApiKey: Sized {
133 fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
136 None
137 }
138}
139
140pub struct BearerAuth(String);
142
143impl ApiKey for BearerAuth {
144 fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
145 Some(make_auth_header(self.0))
146 }
147}
148
149impl<S> From<S> for BearerAuth
150where
151 S: Into<String>,
152{
153 fn from(value: S) -> Self {
154 Self(value.into())
155 }
156}
157
158#[derive(Debug, Default, Clone, Copy)]
161pub struct Nothing;
162
163impl ApiKey for Nothing {}
164
165#[derive(Clone)]
166pub struct Client<Ext = Nothing, H = reqwest::Client> {
172 base_url: Arc<str>,
173 headers: Arc<HeaderMap>,
174 http_client: H,
175 ext: Ext,
176}
177
178pub trait DebugExt: Debug {
180 fn fields(&self) -> impl Iterator<Item = (&'static str, &dyn Debug)> {
182 std::iter::empty()
183 }
184}
185
186impl<Ext, H> std::fmt::Debug for Client<Ext, H>
187where
188 Ext: DebugExt,
189 H: std::fmt::Debug,
190{
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 let mut d = &mut f.debug_struct("Client");
193
194 d = d
195 .field("base_url", &self.base_url)
196 .field(
197 "headers",
198 &self
199 .headers
200 .iter()
201 .filter_map(|(k, v)| {
202 if k == http::header::AUTHORIZATION || k.as_str().contains("api-key") {
203 None
204 } else {
205 Some((k, v))
206 }
207 })
208 .collect::<Vec<(&HeaderName, &HeaderValue)>>(),
209 )
210 .field("http_client", &self.http_client);
211
212 self.ext
213 .fields()
214 .fold(d, |d, (name, field)| d.field(name, field))
215 .finish()
216 }
217}
218
219pub enum Transport {
220 Http,
222 Sse,
224}
225
226pub trait Provider: Sized {
230 type Builder: ProviderBuilder;
233
234 const VERIFY_PATH: &'static str;
236
237 fn build_uri(&self, base_url: &str, path: &str, _transport: Transport) -> String {
239 let base_url = if base_url.is_empty() || base_url.ends_with('/') {
241 base_url.to_string()
242 } else {
243 base_url.to_string() + "/"
245 };
246
247 base_url + path.trim_start_matches('/')
248 }
249
250 fn with_custom(&self, req: http_client::Builder) -> http_client::Result<http_client::Builder> {
252 Ok(req)
253 }
254}
255
256pub struct Capable<M>(PhantomData<M>);
258
259pub trait Capability {
261 const CAPABLE: bool;
263}
264
265impl<M> Capability for Capable<M> {
266 const CAPABLE: bool = true;
267}
268
269impl Capability for Nothing {
270 const CAPABLE: bool = false;
271}
272
273pub trait Capabilities<H = reqwest::Client> {
275 type Completion: Capability;
277 type Embeddings: Capability;
279 type Rerank: Capability;
281 type Transcription: Capability;
283 type ModelListing: Capability;
285 #[cfg(feature = "image")]
286 type ImageGeneration: Capability;
288 #[cfg(feature = "audio")]
289 type AudioGeneration: Capability;
291}
292
293pub trait ProviderBuilder: Sized + Default + Clone {
298 type Extension<H>: Provider
300 where
301 H: HttpClientExt;
302 type ApiKey: ApiKey;
304
305 const BASE_URL: &'static str;
307
308 fn build<H>(
310 builder: &ClientBuilder<Self, Self::ApiKey, H>,
311 ) -> http_client::Result<Self::Extension<H>>
312 where
313 H: HttpClientExt;
314
315 fn finish<H>(
318 &self,
319 builder: ClientBuilder<Self, Self::ApiKey, H>,
320 ) -> http_client::Result<ClientBuilder<Self, Self::ApiKey, H>> {
321 Ok(builder)
322 }
323}
324
325macro_rules! impl_default_provider_builder {
330 (
331 $builder:ty => $extension:ty,
332 api_key = $api_key:ty,
333 base_url = $base_url:expr
334 $(, finish = $finish:path, state = $state:ident)? $(,)?
335 ) => {
336 impl $crate::client::ProviderBuilder for $builder {
337 type Extension<H>
338 = $extension
339 where
340 H: $crate::http_client::HttpClientExt;
341 type ApiKey = $api_key;
342
343 const BASE_URL: &'static str = $base_url;
344
345 fn build<H>(
346 _builder: &$crate::client::ClientBuilder<Self, Self::ApiKey, H>,
347 ) -> $crate::http_client::Result<Self::Extension<H>>
348 where
349 H: $crate::http_client::HttpClientExt,
350 {
351 Ok(<$extension>::default())
352 }
353
354 $(
355 fn finish<H>(
356 &self,
357 builder: $crate::client::ClientBuilder<Self, Self::ApiKey, H>,
358 ) -> $crate::http_client::Result<
359 $crate::client::ClientBuilder<Self, Self::ApiKey, H>,
360 > {
361 $finish(&self.$state, builder)
362 }
363 )?
364 }
365 };
366}
367pub(crate) use impl_default_provider_builder;
368
369macro_rules! impl_capabilities {
374 (
375 $ext:ty
376 $(, completion = $completion:ty)?
377 $(, embeddings = $embeddings:ty)?
378 $(, transcription = $transcription:ty)?
379 $(, model_listing = $model_listing:ty)?
380 $(, image_generation = $image_generation:ty)?
381 $(, audio_generation = $audio_generation:ty)?
382 $(, rerank = $rerank:ty)?
383 $(,)?
384 ) => {
385 impl<H> $crate::client::Capabilities<H> for $ext {
386 type Completion = $crate::client::impl_capabilities!(@slot $($completion)?);
387 type Embeddings = $crate::client::impl_capabilities!(@slot $($embeddings)?);
388 type Transcription = $crate::client::impl_capabilities!(@slot $($transcription)?);
389 type ModelListing = $crate::client::impl_capabilities!(@slot $($model_listing)?);
390 #[cfg(feature = "image")]
391 type ImageGeneration = $crate::client::impl_capabilities!(@slot $($image_generation)?);
392 #[cfg(feature = "audio")]
393 type AudioGeneration = $crate::client::impl_capabilities!(@slot $($audio_generation)?);
394 type Rerank = $crate::client::impl_capabilities!(@slot $($rerank)?);
395 }
396 };
397 (@slot $model:ty) => { $crate::client::Capable<$model> };
398 (@slot) => { $crate::client::Nothing };
399}
400pub(crate) use impl_capabilities;
401
402macro_rules! impl_provider_client {
406 (
407 $client:ty,
408 input = $input:ty,
409 api_key_env = $api_key_env:literal,
410 base_url_env_first = $base_url_env:literal $(,)?
411 ) => {
412 $crate::client::impl_provider_client!(@with_base
413 $client,
414 input = $input,
415 api_key_env = $api_key_env,
416 configuration = {
417 let base_url = $crate::client::optional_env_var($base_url_env)?;
418 let api_key = $crate::client::required_env_var($api_key_env)?;
419 (api_key, base_url)
420 }
421 );
422 };
423 (
424 $client:ty,
425 input = $input:ty,
426 api_key_env = $api_key_env:literal,
427 base_url_env = $base_url_env:literal $(,)?
428 ) => {
429 $crate::client::impl_provider_client!(@with_base
430 $client,
431 input = $input,
432 api_key_env = $api_key_env,
433 configuration = {
434 let api_key = $crate::client::required_env_var($api_key_env)?;
435 let base_url = $crate::client::optional_env_var($base_url_env)?;
436 (api_key, base_url)
437 }
438 );
439 };
440 (
441 $client:ty,
442 input = $input:ty,
443 api_key_env = $api_key_env:literal,
444 base_url = $base_url:expr $(,)?
445 ) => {
446 $crate::client::impl_provider_client!(@with_base
447 $client,
448 input = $input,
449 api_key_env = $api_key_env,
450 configuration = {
451 let api_key = $crate::client::required_env_var($api_key_env)?;
452 (api_key, $base_url)
453 }
454 );
455 };
456 (@with_base
457 $client:ty,
458 input = $input:ty,
459 api_key_env = $api_key_env:literal,
460 configuration = $configuration:block
461 ) => {
462 impl $crate::client::ProviderClient for $client {
463 type Input = $input;
464 type Error = $crate::client::ProviderClientError;
465
466 #[doc = concat!("Create this provider client from the `", $api_key_env, "` environment variable.")]
467 fn from_env() -> Result<Self, Self::Error> {
468 let (api_key, base_url) = $configuration;
469 let mut builder = Self::builder().api_key(api_key);
470 if let Some(base_url) = base_url {
471 builder = builder.base_url(base_url);
472 }
473 builder.build().map_err(Into::into)
474 }
475
476 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
477 Self::new(input).map_err(Into::into)
478 }
479 }
480 };
481 (
482 $client:ty,
483 input = $input:ty,
484 api_key_env = $api_key_env:literal $(,)?
485 ) => {
486 impl $crate::client::ProviderClient for $client {
487 type Input = $input;
488 type Error = $crate::client::ProviderClientError;
489
490 #[doc = concat!("Create this provider client from the `", $api_key_env, "` environment variable.")]
491 fn from_env() -> Result<Self, Self::Error> {
492 let api_key = $crate::client::required_env_var($api_key_env)?;
493 Self::new(api_key).map_err(Into::into)
494 }
495
496 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
497 Self::new(input).map_err(Into::into)
498 }
499 }
500 };
501}
502pub(crate) use impl_provider_client;
503
504impl<Ext> Client<Ext, reqwest::Client>
508where
509 Ext: Provider,
510 Ext::Builder: ProviderBuilder<Extension<reqwest::Client> = Ext> + Default,
511{
512 pub fn new(
514 api_key: impl Into<<Ext::Builder as ProviderBuilder>::ApiKey>,
515 ) -> http_client::Result<Self> {
516 Self::builder().api_key(api_key).build()
517 }
518}
519
520impl<Ext, H> Client<Ext, H> {
521 pub fn base_url(&self) -> &str {
523 &self.base_url
524 }
525
526 pub fn headers(&self) -> &HeaderMap {
528 &self.headers
529 }
530
531 pub fn ext(&self) -> &Ext {
533 &self.ext
534 }
535
536 pub fn with_ext<NewExt>(self, new_ext: NewExt) -> Client<NewExt, H> {
538 Client {
539 base_url: self.base_url,
540 headers: self.headers,
541 http_client: self.http_client,
542 ext: new_ext,
543 }
544 }
545}
546
547impl<Ext, H> HttpClientExt for Client<Ext, H>
548where
549 H: HttpClientExt + 'static,
550 Ext: WasmCompatSend + WasmCompatSync + 'static,
551{
552 fn send<T, U>(
553 &self,
554 mut req: Request<T>,
555 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
556 where
557 T: Into<Bytes> + WasmCompatSend,
558 U: From<Bytes>,
559 U: WasmCompatSend + 'static,
560 {
561 req.headers_mut().insert(
562 http::header::CONTENT_TYPE,
563 http::HeaderValue::from_static("application/json"),
564 );
565
566 self.http_client.send(req)
567 }
568
569 fn send_multipart<U>(
570 &self,
571 req: Request<MultipartForm>,
572 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
573 where
574 U: From<Bytes>,
575 U: WasmCompatSend + 'static,
576 {
577 self.http_client.send_multipart(req)
578 }
579
580 fn send_streaming<T>(
581 &self,
582 mut req: Request<T>,
583 ) -> impl Future<Output = http_client::Result<http_client::StreamingResponse>> + WasmCompatSend
584 where
585 T: Into<Bytes> + WasmCompatSend,
586 {
587 req.headers_mut().insert(
588 http::header::CONTENT_TYPE,
589 http::HeaderValue::from_static("application/json"),
590 );
591
592 self.http_client.send_streaming(req)
593 }
594}
595
596impl<Ext> Client<Ext, reqwest::Client>
602where
603 Ext: Provider,
604 Ext::Builder: ProviderBuilder + Default,
605{
606 pub fn builder() -> ClientBuilder<Ext::Builder, Missing, Missing> {
608 ClientBuilder::default()
609 }
610}
611
612impl<Ext, H> Client<Ext, H>
613where
614 Ext: Provider,
615{
616 fn request(
617 &self,
618 method: http::Method,
619 path: &str,
620 transport: Transport,
621 ) -> http_client::Result<Builder> {
622 let uri = self.ext.build_uri(&self.base_url, path, transport);
623
624 let mut req = Request::builder().method(method).uri(uri);
625
626 if let Some(hs) = req.headers_mut() {
627 hs.extend(self.headers.iter().map(|(k, v)| (k.clone(), v.clone())));
628 }
629
630 self.ext.with_custom(req)
631 }
632
633 pub fn post<S>(&self, path: S) -> http_client::Result<Builder>
635 where
636 S: AsRef<str>,
637 {
638 self.request(http::Method::POST, path.as_ref(), Transport::Http)
639 }
640
641 pub fn post_sse<S>(&self, path: S) -> http_client::Result<Builder>
643 where
644 S: AsRef<str>,
645 {
646 self.request(http::Method::POST, path.as_ref(), Transport::Sse)
647 }
648
649 pub fn get_sse<S>(&self, path: S) -> http_client::Result<Builder>
651 where
652 S: AsRef<str>,
653 {
654 self.request(http::Method::GET, path.as_ref(), Transport::Sse)
655 }
656
657 pub fn get<S>(&self, path: S) -> http_client::Result<Builder>
659 where
660 S: AsRef<str>,
661 {
662 self.request(http::Method::GET, path.as_ref(), Transport::Http)
663 }
664}
665
666impl<Ext, H> VerifyClient for Client<Ext, H>
667where
668 H: HttpClientExt,
669 Ext: DebugExt + Provider + WasmCompatSync,
670{
671 async fn verify(&self) -> Result<(), VerifyError> {
672 use http::StatusCode;
673
674 let req = self
675 .get(Ext::VERIFY_PATH)?
676 .body(http_client::NoBody)
677 .map_err(http_client::Error::from)?;
678
679 let response = match self.http_client.send(req).await {
685 Ok(response) => response,
686 Err(error) => {
687 return Err(match error.non_success_status() {
688 Some(StatusCode::UNAUTHORIZED) | Some(StatusCode::FORBIDDEN) => {
689 VerifyError::InvalidAuthentication
690 }
691 _ => VerifyError::HttpError(error),
692 });
693 }
694 };
695
696 match response.status() {
697 StatusCode::OK => Ok(()),
698 StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN => {
699 Err(VerifyError::InvalidAuthentication)
700 }
701 StatusCode::INTERNAL_SERVER_ERROR => {
705 let headers = Box::new(response.headers().clone());
706 let body = http_client::text(response).await?;
707 Err(VerifyError::HttpError(
708 http_client::Error::InvalidStatusCodeWithDetails {
709 status: StatusCode::INTERNAL_SERVER_ERROR,
710 body,
711 headers,
712 },
713 ))
714 }
715 status if status.as_u16() == 529 => {
716 let headers = Box::new(response.headers().clone());
717 let body = http_client::text(response).await?;
718 Err(VerifyError::HttpError(
719 http_client::Error::InvalidStatusCodeWithDetails {
720 status,
721 body,
722 headers,
723 },
724 ))
725 }
726 _ => {
727 let status = response.status();
728
729 if status.is_success() {
730 Ok(())
731 } else {
732 let headers = Box::new(response.headers().clone());
733 let body: String = String::from_utf8_lossy(&response.into_body().await?).into();
734 Err(VerifyError::HttpError(
735 http_client::Error::InvalidStatusCodeWithDetails {
736 status,
737 body,
738 headers,
739 },
740 ))
741 }
742 }
743 }
744 }
745}
746
747#[derive(Clone)]
763pub struct ClientBuilder<Ext, ApiKey = Missing, H = Missing> {
764 base_url: String,
765 api_key: ApiKey,
766 headers: HeaderMap,
767 http_client: H,
768 ext: Ext,
769}
770
771impl<ExtBuilder> Default for ClientBuilder<ExtBuilder, Missing, Missing>
772where
773 ExtBuilder: ProviderBuilder + Default,
774{
775 fn default() -> Self {
776 Self {
777 api_key: Missing,
778 headers: Default::default(),
779 base_url: ExtBuilder::BASE_URL.into(),
780 http_client: Missing,
781 ext: Default::default(),
782 }
783 }
784}
785
786impl<Ext, H> ClientBuilder<Ext, Missing, H> {
787 pub fn api_key<ApiKey>(self, api_key: impl Into<ApiKey>) -> ClientBuilder<Ext, ApiKey, H> {
790 ClientBuilder {
791 api_key: api_key.into(),
792 base_url: self.base_url,
793 headers: self.headers,
794 http_client: self.http_client,
795 ext: self.ext,
796 }
797 }
798}
799
800impl<Ext, ApiKey, H> ClientBuilder<Ext, ApiKey, H>
801where
802 Ext: Clone,
803{
804 pub(crate) fn over_ext<F, NewExt>(self, f: F) -> ClientBuilder<NewExt, ApiKey, H>
806 where
807 F: FnOnce(Ext) -> NewExt,
808 {
809 let ClientBuilder {
810 base_url,
811 api_key,
812 headers,
813 http_client,
814 ext,
815 } = self;
816
817 let new_ext = f(ext.clone());
818
819 ClientBuilder {
820 base_url,
821 api_key,
822 headers,
823 http_client,
824 ext: new_ext,
825 }
826 }
827
828 pub fn base_url<S>(self, base_url: S) -> Self
830 where
831 S: AsRef<str>,
832 {
833 Self {
834 base_url: base_url.as_ref().to_string(),
835 ..self
836 }
837 }
838
839 pub fn http_client<U>(self, http_client: U) -> ClientBuilder<Ext, ApiKey, U> {
844 ClientBuilder {
845 http_client,
846 base_url: self.base_url,
847 api_key: self.api_key,
848 headers: self.headers,
849 ext: self.ext,
850 }
851 }
852
853 pub fn http_headers(self, headers: HeaderMap) -> Self {
855 Self { headers, ..self }
856 }
857
858 pub(crate) fn headers_mut(&mut self) -> &mut HeaderMap {
859 &mut self.headers
860 }
861
862 pub(crate) fn ext_mut(&mut self) -> &mut Ext {
863 &mut self.ext
864 }
865}
866
867impl<Ext, ApiKey, H> ClientBuilder<Ext, ApiKey, H> {
868 pub(crate) fn get_api_key(&self) -> &ApiKey {
869 &self.api_key
870 }
871}
872
873impl<Ext, Key, H> ClientBuilder<Ext, Key, H> {
874 pub fn ext(&self) -> &Ext {
876 &self.ext
877 }
878
879 pub fn get_base_url(&self) -> &str {
881 &self.base_url
882 }
883}
884
885impl<ExtBuilder, Key> ClientBuilder<ExtBuilder, Key, Missing>
891where
892 ExtBuilder: ProviderBuilder<ApiKey = Key>,
893 Key: ApiKey,
894{
895 pub fn build(
897 self,
898 ) -> http_client::Result<Client<ExtBuilder::Extension<reqwest::Client>, reqwest::Client>> {
899 self.http_client(reqwest::Client::default()).build()
900 }
901}
902
903impl<ExtBuilder, Key, H> ClientBuilder<ExtBuilder, Key, H>
906where
907 ExtBuilder: ProviderBuilder<ApiKey = Key>,
908 Key: ApiKey,
909 H: HttpClientExt,
910{
911 pub fn build(mut self) -> http_client::Result<Client<ExtBuilder::Extension<H>, H>> {
913 let ext_builder = self.ext.clone();
914
915 self = ext_builder.finish(self)?;
916 let ext = ExtBuilder::build(&self)?;
917
918 let ClientBuilder {
919 http_client,
920 base_url,
921 mut headers,
922 api_key,
923 ..
924 } = self;
925
926 if let Some((k, v)) = api_key.into_header().transpose()?
927 && !headers.contains_key(&k)
928 {
929 headers.insert(k, v);
930 }
931
932 Ok(Client {
933 http_client,
934 base_url: Arc::from(base_url.as_str()),
935 headers: Arc::new(headers),
936 ext,
937 })
938 }
939}
940
941macro_rules! impl_capability_client {
949 (
950 $(#[cfg(feature = $feature:literal)])?
951 $client_trait:ident { $slot:ident, $assoc:ident, $method:ident, $model_trait:ident $(+ $extra:path)* }
952 ) => {
953 $(#[cfg(feature = $feature)])?
954 impl<M, Ext, H> $client_trait for Client<Ext, H>
955 where
956 Ext: Capabilities<H, $slot = Capable<M>>,
957 M: $model_trait<Client = Self> $(+ $extra)*,
958 {
959 type $assoc = M;
960
961 fn $method(&self, model: impl Into<String>) -> Self::$assoc {
962 M::make(self, model)
963 }
964 }
965 };
966}
967
968impl<M, Ext, H> CompletionClient for Client<Ext, H>
969where
970 Ext: Capabilities<H, Completion = Capable<M>>,
971 M: CompletionModel + ConstructCompletionModel<Self>,
972{
973 type CompletionModel = M;
974
975 fn completion_model(&self, model: impl Into<String>) -> Self::CompletionModel {
976 M::construct(self, model.into())
977 }
978}
979
980impl<M, Ext, H> EmbeddingsClient for Client<Ext, H>
981where
982 Ext: Capabilities<H, Embeddings = Capable<M>>,
983 M: EmbeddingModel<Client = Self>,
984{
985 type EmbeddingModel = M;
986
987 fn embedding_model(&self, model: impl Into<String>) -> Self::EmbeddingModel {
988 M::make(self, model, None)
989 }
990
991 fn embedding_model_with_ndims(
992 &self,
993 model: impl Into<String>,
994 ndims: usize,
995 ) -> Self::EmbeddingModel {
996 M::make(self, model, Some(ndims))
997 }
998}
999
1000impl_capability_client!(RerankingClient {
1001 Rerank,
1002 RerankModel,
1003 rerank_model,
1004 RerankModel
1005});
1006
1007impl_capability_client!(TranscriptionClient {
1008 Transcription,
1009 TranscriptionModel,
1010 transcription_model,
1011 TranscriptionModel + WasmCompatSend
1012});
1013
1014impl_capability_client!(
1015 #[cfg(feature = "image")]
1016 ImageGenerationClient {
1017 ImageGeneration,
1018 ImageGenerationModel,
1019 image_generation_model,
1020 ImageGenerationModel
1021 }
1022);
1023
1024impl_capability_client!(
1025 #[cfg(feature = "audio")]
1026 AudioGenerationClient {
1027 AudioGeneration,
1028 AudioGenerationModel,
1029 audio_generation_model,
1030 AudioGenerationModel
1031 }
1032);
1033
1034impl<M, Ext, H> ModelListingClient for Client<Ext, H>
1035where
1036 Ext: Capabilities<H, ModelListing = Capable<M>> + Clone,
1037 M: ModelLister<H, Client = Self> + WasmCompatSend + WasmCompatSync + Clone + 'static,
1038 H: WasmCompatSend + WasmCompatSync + Clone,
1039{
1040 fn list_models(
1041 &self,
1042 ) -> impl std::future::Future<
1043 Output = Result<crate::model::ModelList, crate::model::ModelListingError>,
1044 > + WasmCompatSend {
1045 let lister = M::new(self.clone());
1046 async move { lister.list_all().await }
1047 }
1048}
1049
1050#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1051mod wasm_model_listing_compile_checks {
1052 use super::{ModelListingClient, Nothing};
1053 use crate::{
1054 http_client::{self, HttpClientExt, LazyBody, MultipartForm, Request, Response},
1055 providers::{anthropic, deepseek, mistral, ollama, openai, openrouter},
1056 wasm_compat::WasmCompatSend,
1057 };
1058 use bytes::Bytes;
1059 use std::{
1060 future::{self, Future},
1061 marker::PhantomData,
1062 rc::Rc,
1063 };
1064
1065 #[derive(Clone, Default)]
1066 struct WasmOnlyHttpClient {
1067 _not_send_sync: PhantomData<Rc<()>>,
1068 }
1069
1070 impl HttpClientExt for WasmOnlyHttpClient {
1071 fn send<T, U>(
1072 &self,
1073 _req: Request<T>,
1074 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
1075 where
1076 T: Into<Bytes> + WasmCompatSend,
1077 U: From<Bytes> + WasmCompatSend + 'static,
1078 {
1079 future::ready(Err(http_client::Error::StreamEnded))
1080 }
1081
1082 fn send_multipart<U>(
1083 &self,
1084 _req: Request<MultipartForm>,
1085 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
1086 where
1087 U: From<Bytes> + WasmCompatSend + 'static,
1088 {
1089 future::ready(Err(http_client::Error::StreamEnded))
1090 }
1091
1092 fn send_streaming<T>(
1093 &self,
1094 _req: Request<T>,
1095 ) -> impl Future<Output = http_client::Result<http_client::StreamingResponse>> + WasmCompatSend
1096 where
1097 T: Into<Bytes> + WasmCompatSend,
1098 {
1099 future::ready(Err(http_client::Error::StreamEnded))
1100 }
1101 }
1102
1103 fn assert_model_listing_client<C>(client: C)
1104 where
1105 C: ModelListingClient,
1106 {
1107 let _ = client.list_models();
1108 }
1109
1110 fn assert_simple_model_listers_accept_wasm_only_http_clients() {
1111 let _ = openrouter::Client::builder()
1112 .api_key("dummy-key")
1113 .http_client(WasmOnlyHttpClient::default())
1114 .build()
1115 .map(assert_model_listing_client);
1116
1117 let _ = openai::Client::builder()
1118 .api_key("dummy-key")
1119 .http_client(WasmOnlyHttpClient::default())
1120 .build()
1121 .map(assert_model_listing_client);
1122
1123 let _ = mistral::Client::builder()
1124 .api_key("dummy-key")
1125 .http_client(WasmOnlyHttpClient::default())
1126 .build()
1127 .map(assert_model_listing_client);
1128
1129 let _ = anthropic::Client::builder()
1130 .api_key("dummy-key")
1131 .http_client(WasmOnlyHttpClient::default())
1132 .build()
1133 .map(assert_model_listing_client);
1134
1135 let _ = ollama::Client::builder()
1136 .api_key(Nothing)
1137 .http_client(WasmOnlyHttpClient::default())
1138 .build()
1139 .map(assert_model_listing_client);
1140
1141 let _ = deepseek::Client::builder()
1142 .api_key("dummy-key")
1143 .http_client(WasmOnlyHttpClient::default())
1144 .build()
1145 .map(assert_model_listing_client);
1146 }
1147
1148 #[allow(dead_code)]
1149 fn compile_assertions() {
1150 assert_simple_model_listers_accept_wasm_only_http_clients();
1151 }
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156 use crate::providers::anthropic;
1157
1158 #[test]
1161 fn ensures_client_builder_no_annotation() {
1162 let http_client = reqwest::Client::default();
1163 let _ = anthropic::Client::builder()
1164 .http_client(http_client)
1165 .api_key("Foo")
1166 .build()
1167 .unwrap();
1168 }
1169}