Skip to main content

rig_core/client/
mod.rs

1//! This module provides traits for defining and creating provider clients.
2//! Clients are used to create models for completion, embeddings, etc.
3
4pub 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    /// The underlying HTTP backend failed during builder construction.
50    #[error("reqwest error: {0}")]
51    HttpError(
52        #[from]
53        #[source]
54        reqwest::Error,
55    ),
56    /// A provider-specific builder property was invalid.
57    #[error("invalid property: {0}")]
58    InvalidProperty(&'static str),
59}
60
61/// Errors returned while constructing provider clients from environment variables or explicit input.
62///
63/// Provider-specific client constructors use this error for configuration problems that can be
64/// detected before any model request is sent, such as missing API keys, invalid environment
65/// values, or invalid builder configuration.
66#[derive(Debug, Error)]
67#[non_exhaustive]
68pub enum ProviderClientError {
69    /// A required or optional environment variable could not be read as valid Unicode.
70    ///
71    /// For required variables, this variant is also returned when the variable is not present.
72    #[error("environment variable `{name}` is not set or is invalid")]
73    EnvironmentVariable {
74        /// The environment variable name.
75        name: &'static str,
76        /// The underlying environment lookup error.
77        #[source]
78        source: VarError,
79    },
80    /// The underlying provider client builder failed while constructing HTTP configuration.
81    #[error(transparent)]
82    Http(#[from] http_client::Error),
83    /// The provider received an unsupported or incomplete configuration.
84    #[error("{0}")]
85    InvalidConfiguration(&'static str),
86}
87
88/// Result type returned by provider client construction helpers.
89pub type ProviderClientResult<T> = std::result::Result<T, ProviderClientError>;
90
91/// Read a required environment variable for provider client construction.
92///
93/// Returns [`ProviderClientError::EnvironmentVariable`] when the variable is missing or contains
94/// invalid Unicode.
95pub fn required_env_var(name: &'static str) -> ProviderClientResult<String> {
96    std::env::var(name).map_err(|source| ProviderClientError::EnvironmentVariable { name, source })
97}
98
99/// Read an optional environment variable for provider client construction.
100///
101/// Missing variables return `Ok(None)`. Variables containing invalid Unicode return
102/// [`ProviderClientError::EnvironmentVariable`].
103pub 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
111/// Abstracts over the ability to instantiate a client, either via environment variables or some
112/// `Self::Input`
113pub trait ProviderClient {
114    /// Input accepted by [`ProviderClient::from_val`].
115    type Input;
116    /// Error returned when client construction fails.
117    type Error;
118
119    /// Create a client from the process's environment.
120    fn from_env() -> Result<Self, Self::Error>
121    where
122        Self: Sized;
123
124    /// Create a client from an explicit provider-specific input value.
125    fn from_val(input: Self::Input) -> Result<Self, Self::Error>
126    where
127        Self: Sized;
128}
129
130/// A trait for API key inputs accepted by [`ClientBuilder::api_key`].
131///
132/// Returning `Some` inserts a header into the generic [`Client`]. Returning `None`
133/// lets the provider extension handle credentials itself.
134pub trait ApiKey: Sized {
135    /// Convert this key into a default request header, if the generic client
136    /// should own that authentication header.
137    fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
138        None
139    }
140}
141
142/// An API key which will be inserted into a `Client`'s default headers as a bearer auth token
143pub 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/// A type containing nothing at all. For `Option`-like behavior on the type level, i.e. to describe
161/// the lack of a capability or field (an API key, for instance)
162#[derive(Debug, Default, Clone, Copy)]
163pub struct Nothing;
164
165impl ApiKey for Nothing {}
166
167#[derive(Clone)]
168/// Generic provider client shared by Rig provider integrations.
169///
170/// `Ext` stores provider-specific behavior such as URL construction, request
171/// customization, and capabilities. `H` is the HTTP backend and defaults to
172/// `reqwest::Client`.
173pub 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
180/// Provider extension hook for redacted [`Debug`] output.
181pub trait DebugExt: Debug {
182    /// Additional provider-specific fields to include in `Client` debug output.
183    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    /// Regular request/response HTTP transport.
223    Http,
224    /// Server-sent events streaming transport.
225    Sse,
226}
227
228/// An API provider extension, this abstracts over extensions which may be used in conjunction with
229/// the `Client<Ext, H>` struct to define the behavior of a provider with respect to networking,
230/// auth, instantiating models
231pub trait Provider: Sized {
232    /// The builder type that constructs this provider extension.
233    /// This associates extensions with their builders for type inference.
234    type Builder: ProviderBuilder;
235
236    /// Provider endpoint used by [`VerifyClient`] to validate credentials.
237    const VERIFY_PATH: &'static str;
238
239    /// Build a complete request URI for the given base URL, provider path, and transport.
240    fn build_uri(&self, base_url: &str, path: &str, _transport: Transport) -> String {
241        // Some providers (like Azure) have a blank base URL to allow users to input their own endpoints.
242        let base_url = if base_url.is_empty() || base_url.ends_with('/') {
243            base_url.to_string()
244        } else {
245            // Only add a slash to the base_url when it doesn't already end with a slash
246            base_url.to_string() + "/"
247        };
248
249        base_url + path.trim_start_matches('/')
250    }
251
252    /// Apply provider-specific request customization before sending.
253    fn with_custom(&self, req: http_client::Builder) -> http_client::Result<http_client::Builder> {
254        Ok(req)
255    }
256}
257
258/// A wrapper type providing runtime checks on a provider's capabilities via the [Capability] trait
259pub struct Capable<M>(PhantomData<M>);
260
261/// Type-level marker for whether a provider supports a capability.
262pub trait Capability {
263    /// Whether this marker represents a supported capability.
264    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
275/// The capabilities of a given provider, i.e. embeddings, audio transcriptions, text completion
276pub trait Capabilities<H = reqwest::Client> {
277    /// Completion model capability marker.
278    type Completion: Capability;
279    /// Embedding model capability marker.
280    type Embeddings: Capability;
281    /// Rerank model capability marker.
282    type Rerank: Capability;
283    /// Audio transcription model capability marker.
284    type Transcription: Capability;
285    /// Model listing capability marker.
286    type ModelListing: Capability;
287    #[cfg(feature = "image")]
288    /// Image generation model capability marker.
289    type ImageGeneration: Capability;
290    #[cfg(feature = "audio")]
291    /// Audio generation model capability marker.
292    type AudioGeneration: Capability;
293}
294
295/// An API provider extension *builder*, this abstracts over provider-specific builders which are
296/// able to configure and produce a given provider's extension type
297///
298/// See [Provider]
299pub trait ProviderBuilder: Sized + Default + Clone {
300    /// Provider extension type built for a concrete HTTP backend.
301    type Extension<H>: Provider
302    where
303        H: HttpClientExt;
304    /// API key input type accepted by the provider's client builder.
305    type ApiKey: ApiKey;
306
307    /// Default base URL for the provider.
308    const BASE_URL: &'static str;
309
310    /// Build the provider extension from the client builder configuration.
311    fn build<H>(
312        builder: &ClientBuilder<Self, Self::ApiKey, H>,
313    ) -> http_client::Result<Self::Extension<H>>
314    where
315        H: HttpClientExt;
316
317    /// This method can be used to customize the fields of `builder` before it is used to create
318    /// a client. For example, adding default headers
319    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
327/// `new` is pinned to `H = reqwest::Client` so the call site infers without an explicit `H`
328/// annotation. Callers who want a different backend should go through [`Client::builder`] and
329/// chain [`ClientBuilder::http_client`] before [`ClientBuilder::build`].
330impl<Ext> Client<Ext, reqwest::Client>
331where
332    Ext: Provider,
333    Ext::Builder: ProviderBuilder<Extension<reqwest::Client> = Ext> + Default,
334{
335    /// Construct a provider client using the default `reqwest::Client` backend.
336    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    /// Returns the configured provider base URL.
345    pub fn base_url(&self) -> &str {
346        &self.base_url
347    }
348
349    /// Returns default headers applied to outgoing provider requests.
350    pub fn headers(&self) -> &HeaderMap {
351        &self.headers
352    }
353
354    /// Returns the provider extension.
355    pub fn ext(&self) -> &Ext {
356        &self.ext
357    }
358
359    /// Reuse this client's base URL, headers, and HTTP backend with a different extension.
360    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
419/// `builder()` is anchored on `Client<Ext, reqwest::Client>` purely as an inference hook so that
420/// `provider::Client::builder()` resolves without a `H` annotation. The returned builder itself
421/// has `H = Missing`, accurately reflecting that no backend has been chosen yet; the eventual
422/// `Client` produced by `build()` may end up with any HTTP backend depending on whether
423/// [`ClientBuilder::http_client`] was called.
424impl<Ext> Client<Ext, reqwest::Client>
425where
426    Ext: Provider,
427    Ext::Builder: ProviderBuilder + Default,
428{
429    /// Start constructing a provider client.
430    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    /// Build a provider-customized POST request for a regular HTTP endpoint.
440    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    /// Build a provider-customized POST request for an SSE endpoint.
458    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    /// Build a provider-customized GET request for an SSE endpoint.
476    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    /// Build a provider-customized GET request for a regular HTTP endpoint.
494    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/// Type-state builder for [`Client`].
564///
565/// Each generic slot encodes a separate "has the user supplied this yet?" question:
566///
567/// - `ApiKey = Missing` means the caller has not yet called [`Self::api_key`]; transitioning to a
568///   concrete `ApiKey` type is required before [`Self::build`] is reachable.
569/// - `H = Missing` means the caller has not yet called [`Self::http_client`]; in that state
570///   `build()` substitutes the canonical `reqwest::Client` backend at construction time. Once a
571///   backend has been supplied, `H` is the concrete HTTP client type and `build()` uses it
572///   directly.
573///
574/// Keeping `Missing` as the *type-level* placeholder (rather than reusing `reqwest::Client`)
575/// means the builder's generics describe what the caller has actually provided, instead of
576/// pretending a default value is already present. It also avoids carrying an `Option<H>` whose
577/// `None` branch existed only to model the same "user hasn't picked a backend" state.
578#[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    /// Set the API key for this client. This *must* be done before the `build` method can be
604    /// called
605    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    /// Owned map over the ext field
621    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    /// Set the base URL for this client
645    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    /// Set the HTTP backend used in this client.
656    ///
657    /// Calling this advances the builder's `H` slot from whatever it was (typically `Missing`)
658    /// to the supplied client's type, which selects the H-generic [`Self::build`] impl below.
659    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    /// Set the HTTP headers used in this client
670    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    /// Returns the provider extension builder state.
691    pub fn ext(&self) -> &Ext {
692        &self.ext
693    }
694
695    /// Returns the configured base URL.
696    pub fn get_base_url(&self) -> &str {
697        &self.base_url
698    }
699}
700
701/// Default-backend `build`: when the caller never called [`ClientBuilder::http_client`], the
702/// builder's `H` slot is still `Missing`, and we substitute the canonical `reqwest::Client` at
703/// build time. This is the only place in the crate that knows about that default, and it is
704/// disjoint by trait bound from the H-generic `build` below (`Missing` does not implement
705/// [`HttpClientExt`]).
706impl<ExtBuilder, Key> ClientBuilder<ExtBuilder, Key, Missing>
707where
708    ExtBuilder: ProviderBuilder<ApiKey = Key>,
709    Key: ApiKey,
710{
711    /// Build a client using the default `reqwest::Client` backend.
712    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
719/// Concrete-backend `build`: the caller supplied an HTTP client via
720/// [`ClientBuilder::http_client`], so `H` is a real `HttpClientExt` type and we use it directly.
721impl<ExtBuilder, Key, H> ClientBuilder<ExtBuilder, Key, H>
722where
723    ExtBuilder: ProviderBuilder<ApiKey = Key>,
724    Key: ApiKey,
725    H: HttpClientExt,
726{
727    /// Build a client using the HTTP backend supplied with [`ClientBuilder::http_client`].
728    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    /// Type-level test that `Client::builder()` methods do not require annotation to determine
964    /// backig HTTP client
965    #[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}