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, 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    /// The underlying HTTP backend failed during builder construction.
49    #[error("reqwest error: {0}")]
50    HttpError(
51        #[from]
52        #[source]
53        reqwest::Error,
54    ),
55    /// A provider-specific builder property was invalid.
56    #[error("invalid property: {0}")]
57    InvalidProperty(&'static str),
58}
59
60/// Errors returned while constructing provider clients from environment variables or explicit input.
61///
62/// Provider-specific client constructors use this error for configuration problems that can be
63/// detected before any model request is sent, such as missing API keys, invalid environment
64/// values, or invalid builder configuration.
65#[derive(Debug, Error)]
66pub enum ProviderClientError {
67    /// A required or optional environment variable could not be read as valid Unicode.
68    ///
69    /// For required variables, this variant is also returned when the variable is not present.
70    #[error("environment variable `{name}` is not set or is invalid")]
71    EnvironmentVariable {
72        /// The environment variable name.
73        name: &'static str,
74        /// The underlying environment lookup error.
75        #[source]
76        source: VarError,
77    },
78    /// The underlying provider client builder failed while constructing HTTP configuration.
79    #[error(transparent)]
80    Http(#[from] http_client::Error),
81    /// The provider received an unsupported or incomplete configuration.
82    #[error("{0}")]
83    InvalidConfiguration(&'static str),
84}
85
86/// Result type returned by provider client construction helpers.
87pub type ProviderClientResult<T> = std::result::Result<T, ProviderClientError>;
88
89/// Read a required environment variable for provider client construction.
90///
91/// Returns [`ProviderClientError::EnvironmentVariable`] when the variable is missing or contains
92/// invalid Unicode.
93pub fn required_env_var(name: &'static str) -> ProviderClientResult<String> {
94    std::env::var(name).map_err(|source| ProviderClientError::EnvironmentVariable { name, source })
95}
96
97/// Read an optional environment variable for provider client construction.
98///
99/// Missing variables return `Ok(None)`. Variables containing invalid Unicode return
100/// [`ProviderClientError::EnvironmentVariable`].
101pub 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
109/// Abstracts over the ability to instantiate a client, either via environment variables or some
110/// `Self::Input`
111pub trait ProviderClient {
112    /// Input accepted by [`ProviderClient::from_val`].
113    type Input;
114    /// Error returned when client construction fails.
115    type Error;
116
117    /// Create a client from the process's environment.
118    fn from_env() -> Result<Self, Self::Error>
119    where
120        Self: Sized;
121
122    /// Create a client from an explicit provider-specific input value.
123    fn from_val(input: Self::Input) -> Result<Self, Self::Error>
124    where
125        Self: Sized;
126}
127
128/// A trait for API key inputs accepted by [`ClientBuilder::api_key`].
129///
130/// Returning `Some` inserts a header into the generic [`Client`]. Returning `None`
131/// lets the provider extension handle credentials itself.
132pub trait ApiKey: Sized {
133    /// Convert this key into a default request header, if the generic client
134    /// should own that authentication header.
135    fn into_header(self) -> Option<http_client::Result<(HeaderName, HeaderValue)>> {
136        None
137    }
138}
139
140/// An API key which will be inserted into a `Client`'s default headers as a bearer auth token
141pub 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/// A type containing nothing at all. For `Option`-like behavior on the type level, i.e. to describe
159/// the lack of a capability or field (an API key, for instance)
160#[derive(Debug, Default, Clone, Copy)]
161pub struct Nothing;
162
163impl ApiKey for Nothing {}
164
165#[derive(Clone)]
166/// Generic provider client shared by Rig provider integrations.
167///
168/// `Ext` stores provider-specific behavior such as URL construction, request
169/// customization, and capabilities. `H` is the HTTP backend and defaults to
170/// `reqwest::Client`.
171pub 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
178/// Provider extension hook for redacted [`Debug`] output.
179pub trait DebugExt: Debug {
180    /// Additional provider-specific fields to include in `Client` debug output.
181    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    /// Regular request/response HTTP transport.
221    Http,
222    /// Server-sent events streaming transport.
223    Sse,
224}
225
226/// An API provider extension, this abstracts over extensions which may be used in conjunction with
227/// the `Client<Ext, H>` struct to define the behavior of a provider with respect to networking,
228/// auth, instantiating models
229pub trait Provider: Sized {
230    /// The builder type that constructs this provider extension.
231    /// This associates extensions with their builders for type inference.
232    type Builder: ProviderBuilder;
233
234    /// Provider endpoint used by [`VerifyClient`] to validate credentials.
235    const VERIFY_PATH: &'static str;
236
237    /// Build a complete request URI for the given base URL, provider path, and transport.
238    fn build_uri(&self, base_url: &str, path: &str, _transport: Transport) -> String {
239        // Some providers (like Azure) have a blank base URL to allow users to input their own endpoints.
240        let base_url = if base_url.is_empty() || base_url.ends_with('/') {
241            base_url.to_string()
242        } else {
243            // Only add a slash to the base_url when it doesn't already end with a slash
244            base_url.to_string() + "/"
245        };
246
247        base_url + path.trim_start_matches('/')
248    }
249
250    /// Apply provider-specific request customization before sending.
251    fn with_custom(&self, req: http_client::Builder) -> http_client::Result<http_client::Builder> {
252        Ok(req)
253    }
254}
255
256/// A wrapper type providing runtime checks on a provider's capabilities via the [Capability] trait
257pub struct Capable<M>(PhantomData<M>);
258
259/// Type-level marker for whether a provider supports a capability.
260pub trait Capability {
261    /// Whether this marker represents a supported capability.
262    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
273/// The capabilities of a given provider, i.e. embeddings, audio transcriptions, text completion
274pub trait Capabilities<H = reqwest::Client> {
275    /// Completion model capability marker.
276    type Completion: Capability;
277    /// Embedding model capability marker.
278    type Embeddings: Capability;
279    /// Rerank model capability marker.
280    type Rerank: Capability;
281    /// Audio transcription model capability marker.
282    type Transcription: Capability;
283    /// Model listing capability marker.
284    type ModelListing: Capability;
285    #[cfg(feature = "image")]
286    /// Image generation model capability marker.
287    type ImageGeneration: Capability;
288    #[cfg(feature = "audio")]
289    /// Audio generation model capability marker.
290    type AudioGeneration: Capability;
291}
292
293/// An API provider extension *builder*, this abstracts over provider-specific builders which are
294/// able to configure and produce a given provider's extension type
295///
296/// See [Provider]
297pub trait ProviderBuilder: Sized + Default + Clone {
298    /// Provider extension type built for a concrete HTTP backend.
299    type Extension<H>: Provider
300    where
301        H: HttpClientExt;
302    /// API key input type accepted by the provider's client builder.
303    type ApiKey: ApiKey;
304
305    /// Default base URL for the provider.
306    const BASE_URL: &'static str;
307
308    /// Build the provider extension from the client builder configuration.
309    fn build<H>(
310        builder: &ClientBuilder<Self, Self::ApiKey, H>,
311    ) -> http_client::Result<Self::Extension<H>>
312    where
313        H: HttpClientExt;
314
315    /// This method can be used to customize the fields of `builder` before it is used to create
316    /// a client. For example, adding default headers
317    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
325// These implementations are declarations of associated types and constants,
326// so ordinary helper functions cannot express the repeated structure. Keeping
327// the variation points in one invocation makes each provider's configuration
328// visible without duplicating the generic builder plumbing.
329macro_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
369// A provider's Capabilities impl is a pure associated-type table where every
370// slot a provider does not support is `Nothing`. The named optional slots
371// keep each provider's invocation down to what it actually supports, and the
372// macro owns the feature gating on the image/audio slots.
373macro_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
402// ProviderClient is implemented for concrete client aliases, which likewise
403// cannot be factored into a function. The optional base-URL form captures the
404// only common construction variation without hiding provider-specific auth.
405macro_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
504/// `new` is pinned to `H = reqwest::Client` so the call site infers without an explicit `H`
505/// annotation. Callers who want a different backend should go through [`Client::builder`] and
506/// chain [`ClientBuilder::http_client`] before [`ClientBuilder::build`].
507impl<Ext> Client<Ext, reqwest::Client>
508where
509    Ext: Provider,
510    Ext::Builder: ProviderBuilder<Extension<reqwest::Client> = Ext> + Default,
511{
512    /// Construct a provider client using the default `reqwest::Client` backend.
513    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    /// Returns the configured provider base URL.
522    pub fn base_url(&self) -> &str {
523        &self.base_url
524    }
525
526    /// Returns default headers applied to outgoing provider requests.
527    pub fn headers(&self) -> &HeaderMap {
528        &self.headers
529    }
530
531    /// Returns the provider extension.
532    pub fn ext(&self) -> &Ext {
533        &self.ext
534    }
535
536    /// Reuse this client's base URL, headers, and HTTP backend with a different extension.
537    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
596/// `builder()` is anchored on `Client<Ext, reqwest::Client>` purely as an inference hook so that
597/// `provider::Client::builder()` resolves without a `H` annotation. The returned builder itself
598/// has `H = Missing`, accurately reflecting that no backend has been chosen yet; the eventual
599/// `Client` produced by `build()` may end up with any HTTP backend depending on whether
600/// [`ClientBuilder::http_client`] was called.
601impl<Ext> Client<Ext, reqwest::Client>
602where
603    Ext: Provider,
604    Ext::Builder: ProviderBuilder + Default,
605{
606    /// Start constructing a provider client.
607    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    /// Build a provider-customized POST request for a regular HTTP endpoint.
634    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    /// Build a provider-customized POST request for an SSE endpoint.
642    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    /// Build a provider-customized GET request for an SSE endpoint.
650    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    /// Build a provider-customized GET request for a regular HTTP endpoint.
658    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        // The reqwest transport reports non-success as an error before this
680        // status match can run (found live on rig#2315's error matrix: the
681        // 401/403 arms below were dead and every bogus key surfaced as a raw
682        // HttpError). Recover the status from the transport error so the
683        // documented VerifyError classification actually fires.
684        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            // The failed response's headers are preserved on every branch, so
702            // a caller can read rate-limit metadata such as `Retry-After` off
703            // a rejected verification (rig#2210).
704            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/// Type-state builder for [`Client`].
748///
749/// Each generic slot encodes a separate "has the user supplied this yet?" question:
750///
751/// - `ApiKey = Missing` means the caller has not yet called [`Self::api_key`]; transitioning to a
752///   concrete `ApiKey` type is required before [`Self::build`] is reachable.
753/// - `H = Missing` means the caller has not yet called [`Self::http_client`]; in that state
754///   `build()` substitutes the canonical `reqwest::Client` backend at construction time. Once a
755///   backend has been supplied, `H` is the concrete HTTP client type and `build()` uses it
756///   directly.
757///
758/// Keeping `Missing` as the *type-level* placeholder (rather than reusing `reqwest::Client`)
759/// means the builder's generics describe what the caller has actually provided, instead of
760/// pretending a default value is already present. It also avoids carrying an `Option<H>` whose
761/// `None` branch existed only to model the same "user hasn't picked a backend" state.
762#[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    /// Set the API key for this client. This *must* be done before the `build` method can be
788    /// called
789    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    /// Owned map over the ext field
805    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    /// Set the base URL for this client
829    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    /// Set the HTTP backend used in this client.
840    ///
841    /// Calling this advances the builder's `H` slot from whatever it was (typically `Missing`)
842    /// to the supplied client's type, which selects the H-generic [`Self::build`] impl below.
843    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    /// Set the HTTP headers used in this client
854    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    /// Returns the provider extension builder state.
875    pub fn ext(&self) -> &Ext {
876        &self.ext
877    }
878
879    /// Returns the configured base URL.
880    pub fn get_base_url(&self) -> &str {
881        &self.base_url
882    }
883}
884
885/// Default-backend `build`: when the caller never called [`ClientBuilder::http_client`], the
886/// builder's `H` slot is still `Missing`, and we substitute the canonical `reqwest::Client` at
887/// build time. This is the only place in the crate that knows about that default, and it is
888/// disjoint by trait bound from the H-generic `build` below (`Missing` does not implement
889/// [`HttpClientExt`]).
890impl<ExtBuilder, Key> ClientBuilder<ExtBuilder, Key, Missing>
891where
892    ExtBuilder: ProviderBuilder<ApiKey = Key>,
893    Key: ApiKey,
894{
895    /// Build a client using the default `reqwest::Client` backend.
896    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
903/// Concrete-backend `build`: the caller supplied an HTTP client via
904/// [`ClientBuilder::http_client`], so `H` is a real `HttpClientExt` type and we use it directly.
905impl<ExtBuilder, Key, H> ClientBuilder<ExtBuilder, Key, H>
906where
907    ExtBuilder: ProviderBuilder<ApiKey = Key>,
908    Key: ApiKey,
909    H: HttpClientExt,
910{
911    /// Build a client using the HTTP backend supplied with [`ClientBuilder::http_client`].
912    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
941// Every single-model capability client impl on `Client<Ext, H>` shares the
942// same shape: gate on the matching `Capabilities` slot, name the model type,
943// and construct it with `M::make`. The macro keeps the per-capability
944// variation (trait, slot, associated type, method, extra model bounds, and
945// feature gate) in one invocation each. `CompletionClient` (different
946// constructor protocol) and `EmbeddingsClient` (extra `_with_ndims` method)
947// stay hand-written below.
948macro_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    /// Type-level test that `Client::builder()` methods do not require annotation to determine
1159    /// backig HTTP client
1160    #[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}