Skip to main content

liter_llm/client/
mod.rs

1pub mod config;
2pub mod config_file;
3#[cfg(all(feature = "native-http", feature = "tower"))]
4pub mod managed;
5
6use std::future::Future;
7use std::pin::Pin;
8#[cfg(any(feature = "native-http", feature = "wasm-http"))]
9use std::sync::Arc;
10
11use futures_core::Stream;
12
13use crate::error::Result;
14use crate::types::audio::{CreateSpeechRequest, CreateTranscriptionRequest, TranscriptionResponse};
15use crate::types::batch::{BatchListQuery, BatchListResponse, BatchObject, CreateBatchRequest};
16use crate::types::files::{CreateFileRequest, DeleteResponse, FileListQuery, FileListResponse, FileObject};
17use crate::types::image::{CreateImageRequest, ImagesResponse};
18use crate::types::moderation::{ModerationRequest, ModerationResponse};
19use crate::types::ocr::{OcrRequest, OcrResponse};
20use crate::types::raw::{RawExchange, RawStreamExchange};
21use crate::types::rerank::{RerankRequest, RerankResponse};
22use crate::types::responses::{CreateResponseRequest, ResponseObject};
23use crate::types::search::{SearchRequest, SearchResponse};
24use crate::types::{
25    ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, EmbeddingRequest, EmbeddingResponse,
26    ModelsListResponse,
27};
28
29// DefaultClient and its LlmClient impl require reqwest + tokio.
30#[cfg(any(feature = "native-http", feature = "wasm-http"))]
31use crate::auth::Credential;
32#[cfg(any(feature = "native-http", feature = "wasm-http"))]
33use crate::error::LiterLlmError;
34#[cfg(any(feature = "native-http", feature = "wasm-http"))]
35use crate::http;
36#[cfg(any(feature = "native-http", feature = "wasm-http"))]
37use crate::provider::{self, OpenAiCompatibleProvider, OpenAiProvider, Provider};
38#[cfg(any(feature = "native-http", feature = "wasm-http"))]
39use secrecy::ExposeSecret;
40
41pub use config::{ClientConfig, ClientConfigBuilder};
42pub use config_file::FileConfig;
43
44/// A boxed future returning `T`.
45#[cfg(not(target_arch = "wasm32"))]
46pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
47
48/// A boxed future returning `T` (WASM variant — not `Send` because JS is single-threaded).
49#[cfg(target_arch = "wasm32")]
50pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
51
52/// A boxed stream of `T`.
53#[cfg(not(target_arch = "wasm32"))]
54pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
55
56/// A boxed stream of `T` (WASM variant — not `Send` because JS is single-threaded).
57#[cfg(target_arch = "wasm32")]
58pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + 'a>>;
59
60/// Result of [`DefaultClient::prepare_request`].
61///
62/// The body is pre-serialized into `bytes::Bytes` so it is serialized exactly
63/// once — the same bytes are used for signing headers and for the HTTP request
64/// body.  On retry, cloning `Bytes` is a zero-copy ref-count bump.
65///
66/// `body_json` is the pre-serialization JSON value, retained so that
67/// [`Provider::dynamic_headers`] can inspect request fields without
68/// re-parsing.
69///
70/// The `provider` is the resolved provider for this specific request — it may
71/// differ from `self.provider` when the model prefix identifies a different
72/// provider.
73#[cfg(any(feature = "native-http", feature = "wasm-http"))]
74struct PreparedRequest {
75    url: String,
76    provider: Arc<dyn Provider>,
77    body_json: serde_json::Value,
78    body_bytes: bytes::Bytes,
79}
80
81/// Convert an owned `(String, String)` auth header pair to `(&str, &str)` borrows.
82///
83/// Centralises the four identical `map(|(n, v)| (n.as_str(), v.as_str()))` expressions
84/// that appear wherever we hand headers to the HTTP layer.
85#[cfg(any(feature = "native-http", feature = "wasm-http"))]
86fn str_pair(pair: &(String, String)) -> (&str, &str) {
87    (pair.0.as_str(), pair.1.as_str())
88}
89
90/// Core LLM client trait.
91#[cfg(not(target_arch = "wasm32"))]
92pub trait LlmClient: Send + Sync {
93    /// Send a chat completion request.
94    fn chat(&self, req: ChatCompletionRequest) -> BoxFuture<'_, Result<ChatCompletionResponse>>;
95
96    /// Send a streaming chat completion request.
97    fn chat_stream(
98        &self,
99        req: ChatCompletionRequest,
100    ) -> BoxFuture<'_, Result<BoxStream<'static, Result<ChatCompletionChunk>>>>;
101
102    /// Send an embedding request.
103    fn embed(&self, req: EmbeddingRequest) -> BoxFuture<'_, Result<EmbeddingResponse>>;
104
105    /// List available models.
106    fn list_models(&self) -> BoxFuture<'_, Result<ModelsListResponse>>;
107
108    /// Generate an image.
109    fn image_generate(&self, req: CreateImageRequest) -> BoxFuture<'_, Result<ImagesResponse>>;
110
111    /// Generate speech audio from text.
112    fn speech(&self, req: CreateSpeechRequest) -> BoxFuture<'_, Result<bytes::Bytes>>;
113
114    /// Transcribe audio to text.
115    fn transcribe(&self, req: CreateTranscriptionRequest) -> BoxFuture<'_, Result<TranscriptionResponse>>;
116
117    /// Check content against moderation policies.
118    fn moderate(&self, req: ModerationRequest) -> BoxFuture<'_, Result<ModerationResponse>>;
119
120    /// Rerank documents by relevance to a query.
121    fn rerank(&self, req: RerankRequest) -> BoxFuture<'_, Result<RerankResponse>>;
122
123    /// Perform a web/document search.
124    fn search(&self, req: SearchRequest) -> BoxFuture<'_, Result<SearchResponse>>;
125
126    /// Extract text from a document via OCR.
127    fn ocr(&self, req: OcrRequest) -> BoxFuture<'_, Result<OcrResponse>>;
128}
129
130/// Core LLM client trait (WASM variant — no `Send + Sync` because JS is single-threaded).
131#[cfg(target_arch = "wasm32")]
132pub trait LlmClient {
133    /// Send a chat completion request.
134    fn chat(&self, req: ChatCompletionRequest) -> BoxFuture<'_, Result<ChatCompletionResponse>>;
135
136    /// Send a streaming chat completion request.
137    fn chat_stream(
138        &self,
139        req: ChatCompletionRequest,
140    ) -> BoxFuture<'_, Result<BoxStream<'static, Result<ChatCompletionChunk>>>>;
141
142    /// Send an embedding request.
143    fn embed(&self, req: EmbeddingRequest) -> BoxFuture<'_, Result<EmbeddingResponse>>;
144
145    /// List available models.
146    fn list_models(&self) -> BoxFuture<'_, Result<ModelsListResponse>>;
147
148    /// Generate an image.
149    fn image_generate(&self, req: CreateImageRequest) -> BoxFuture<'_, Result<ImagesResponse>>;
150
151    /// Generate speech audio from text.
152    fn speech(&self, req: CreateSpeechRequest) -> BoxFuture<'_, Result<bytes::Bytes>>;
153
154    /// Transcribe audio to text.
155    fn transcribe(&self, req: CreateTranscriptionRequest) -> BoxFuture<'_, Result<TranscriptionResponse>>;
156
157    /// Check content against moderation policies.
158    fn moderate(&self, req: ModerationRequest) -> BoxFuture<'_, Result<ModerationResponse>>;
159
160    /// Rerank documents by relevance to a query.
161    fn rerank(&self, req: RerankRequest) -> BoxFuture<'_, Result<RerankResponse>>;
162
163    /// Perform a web/document search.
164    fn search(&self, req: SearchRequest) -> BoxFuture<'_, Result<SearchResponse>>;
165
166    /// Extract text from a document via OCR.
167    fn ocr(&self, req: OcrRequest) -> BoxFuture<'_, Result<OcrResponse>>;
168}
169
170/// Extension of [`LlmClient`] that returns raw request/response data
171/// alongside the typed response.
172///
173/// Every `_raw` method mirrors its counterpart on [`LlmClient`] but wraps the
174/// result in a [`RawExchange`] that exposes the final request body (after
175/// `transform_request`) and the raw provider response (before
176/// `transform_response`). This is useful for debugging provider-specific
177/// transformations, capturing wire-level data, or implementing custom parsing.
178pub trait LlmClientRaw: LlmClient {
179    /// Send a chat completion request and return the raw exchange.
180    ///
181    /// The `raw_request` field contains the final JSON body sent to the
182    /// provider; `raw_response` contains the provider JSON before
183    /// normalization.
184    fn chat_raw(&self, req: ChatCompletionRequest) -> BoxFuture<'_, Result<RawExchange<ChatCompletionResponse>>>;
185
186    /// Send a streaming chat completion request and return the raw exchange.
187    ///
188    /// Only `raw_request` is available upfront — the stream itself is
189    /// returned in `stream` and consumed incrementally.
190    fn chat_stream_raw(
191        &self,
192        req: ChatCompletionRequest,
193    ) -> BoxFuture<'_, Result<RawStreamExchange<BoxStream<'static, Result<ChatCompletionChunk>>>>>;
194
195    /// Send an embedding request and return the raw exchange.
196    fn embed_raw(&self, req: EmbeddingRequest) -> BoxFuture<'_, Result<RawExchange<EmbeddingResponse>>>;
197
198    /// Generate an image and return the raw exchange.
199    fn image_generate_raw(&self, req: CreateImageRequest) -> BoxFuture<'_, Result<RawExchange<ImagesResponse>>>;
200
201    /// Transcribe audio to text and return the raw exchange.
202    fn transcribe_raw(
203        &self,
204        req: CreateTranscriptionRequest,
205    ) -> BoxFuture<'_, Result<RawExchange<TranscriptionResponse>>>;
206
207    /// Check content against moderation policies and return the raw exchange.
208    fn moderate_raw(&self, req: ModerationRequest) -> BoxFuture<'_, Result<RawExchange<ModerationResponse>>>;
209
210    /// Rerank documents by relevance to a query and return the raw exchange.
211    fn rerank_raw(&self, req: RerankRequest) -> BoxFuture<'_, Result<RawExchange<RerankResponse>>>;
212
213    /// Perform a web/document search and return the raw exchange.
214    fn search_raw(&self, req: SearchRequest) -> BoxFuture<'_, Result<RawExchange<SearchResponse>>>;
215
216    /// Extract text from a document via OCR and return the raw exchange.
217    fn ocr_raw(&self, req: OcrRequest) -> BoxFuture<'_, Result<RawExchange<OcrResponse>>>;
218}
219
220/// File management operations (upload, list, retrieve, delete).
221#[cfg(not(target_arch = "wasm32"))]
222pub trait FileClient: Send + Sync {
223    /// Upload a file.
224    fn create_file(&self, req: CreateFileRequest) -> BoxFuture<'_, Result<FileObject>>;
225
226    /// Retrieve metadata for a file.
227    fn retrieve_file(&self, file_id: &str) -> BoxFuture<'_, Result<FileObject>>;
228
229    /// Delete a file.
230    fn delete_file(&self, file_id: &str) -> BoxFuture<'_, Result<DeleteResponse>>;
231
232    /// List files, optionally filtered by query parameters.
233    fn list_files(&self, query: Option<FileListQuery>) -> BoxFuture<'_, Result<FileListResponse>>;
234
235    /// Retrieve the raw content of a file.
236    fn file_content(&self, file_id: &str) -> BoxFuture<'_, Result<bytes::Bytes>>;
237}
238
239/// File management operations (upload, list, retrieve, delete) (WASM variant).
240#[cfg(target_arch = "wasm32")]
241pub trait FileClient {
242    /// Upload a file.
243    fn create_file(&self, req: CreateFileRequest) -> BoxFuture<'_, Result<FileObject>>;
244
245    /// Retrieve metadata for a file.
246    fn retrieve_file(&self, file_id: &str) -> BoxFuture<'_, Result<FileObject>>;
247
248    /// Delete a file.
249    fn delete_file(&self, file_id: &str) -> BoxFuture<'_, Result<DeleteResponse>>;
250
251    /// List files, optionally filtered by query parameters.
252    fn list_files(&self, query: Option<FileListQuery>) -> BoxFuture<'_, Result<FileListResponse>>;
253
254    /// Retrieve the raw content of a file.
255    fn file_content(&self, file_id: &str) -> BoxFuture<'_, Result<bytes::Bytes>>;
256}
257
258/// Batch processing operations (create, list, retrieve, cancel).
259#[cfg(not(target_arch = "wasm32"))]
260pub trait BatchClient: Send + Sync {
261    /// Create a new batch job.
262    fn create_batch(&self, req: CreateBatchRequest) -> BoxFuture<'_, Result<BatchObject>>;
263
264    /// Retrieve a batch by ID.
265    fn retrieve_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>>;
266
267    /// List batches, optionally filtered by query parameters.
268    fn list_batches(&self, query: Option<BatchListQuery>) -> BoxFuture<'_, Result<BatchListResponse>>;
269
270    /// Cancel an in-progress batch.
271    fn cancel_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>>;
272}
273
274/// Batch processing operations (create, list, retrieve, cancel) (WASM variant).
275#[cfg(target_arch = "wasm32")]
276pub trait BatchClient {
277    /// Create a new batch job.
278    fn create_batch(&self, req: CreateBatchRequest) -> BoxFuture<'_, Result<BatchObject>>;
279
280    /// Retrieve a batch by ID.
281    fn retrieve_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>>;
282
283    /// List batches, optionally filtered by query parameters.
284    fn list_batches(&self, query: Option<BatchListQuery>) -> BoxFuture<'_, Result<BatchListResponse>>;
285
286    /// Cancel an in-progress batch.
287    fn cancel_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>>;
288}
289
290/// Responses API operations (create, retrieve, cancel).
291#[cfg(not(target_arch = "wasm32"))]
292pub trait ResponseClient: Send + Sync {
293    /// Create a new response.
294    fn create_response(&self, req: CreateResponseRequest) -> BoxFuture<'_, Result<ResponseObject>>;
295
296    /// Retrieve a response by ID.
297    fn retrieve_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>>;
298
299    /// Cancel an in-progress response.
300    fn cancel_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>>;
301}
302
303/// Responses API operations (create, retrieve, cancel) (WASM variant).
304#[cfg(target_arch = "wasm32")]
305pub trait ResponseClient {
306    /// Create a new response.
307    fn create_response(&self, req: CreateResponseRequest) -> BoxFuture<'_, Result<ResponseObject>>;
308
309    /// Retrieve a response by ID.
310    fn retrieve_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>>;
311
312    /// Cancel an in-progress response.
313    fn cancel_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>>;
314}
315
316/// Default client implementation backed by `reqwest`.
317///
318/// The provider is resolved at construction time from `model_hint` (or
319/// defaults to OpenAI). However, individual requests can override the
320/// provider when their model string contains a prefix that clearly
321/// identifies a different provider (e.g. `"anthropic/claude-3"` will
322/// route to Anthropic even if the client was built without a hint).
323///
324/// When the model prefix does not match any known provider, the
325/// construction-time provider is used as the fallback.
326///
327/// The provider is stored behind an [`Arc`] so it can be shared cheaply into
328/// async closures and streaming tasks that must be `'static`.
329#[cfg(any(feature = "native-http", feature = "wasm-http"))]
330#[derive(Clone)]
331pub struct DefaultClient {
332    config: ClientConfig,
333    http: reqwest::Client,
334    /// Provider resolved at construction; shared via Arc so streaming closures
335    /// can capture an owned reference without requiring `unsafe`.
336    provider: Arc<dyn Provider>,
337    /// Pre-computed auth header `(name, value)` — avoids `format!("Bearer {key}")`
338    /// on every request.  `None` when the provider requires no authentication.
339    cached_auth_header: Option<(String, String)>,
340    /// Pre-computed static extra headers — avoids converting `&'static str` pairs
341    /// to `(String, String)` on every request.
342    cached_extra_headers: Vec<(String, String)>,
343}
344
345#[cfg(any(feature = "native-http", feature = "wasm-http"))]
346impl DefaultClient {
347    /// Build a client.
348    ///
349    /// `model_hint` guides provider auto-detection when no explicit
350    /// `base_url` override is present in the config.  For example, passing
351    /// `Some("groq/llama3-70b")` selects the Groq provider.  Pass `None` to
352    /// default to OpenAI.
353    ///
354    /// # Errors
355    ///
356    /// Returns a wrapped [`reqwest::Error`] if the underlying HTTP client
357    /// cannot be constructed.  Header names and values are pre-validated by
358    /// [`ClientConfigBuilder::header`], so they are inserted directly here.
359    pub fn new(mut config: ClientConfig, model_hint: Option<&str>) -> Result<Self> {
360        let provider = build_provider(&config, model_hint);
361        // Validate configuration eagerly so callers get a clear error at
362        // construction time rather than on the first request.
363        provider.validate()?;
364
365        // Auto-load the API key from the environment when no explicit key was
366        // provided and `load_env` is enabled.  Skipped on WASM where
367        // `std::env::var` is unavailable.
368        #[cfg(not(target_arch = "wasm32"))]
369        if config.load_env
370            && config.api_key.expose_secret().is_empty()
371            && let Some(env_var_name) = provider.env_var()
372        {
373            match std::env::var(env_var_name) {
374                Ok(val) if !val.is_empty() => {
375                    config.api_key = secrecy::SecretString::from(val);
376                }
377                _ => {
378                    return Err(LiterLlmError::Authentication {
379                        message: format!("no API key provided and environment variable {env_var_name} is not set"),
380                    });
381                }
382            }
383        }
384
385        // Build the header map from pre-validated headers stored in the config.
386        // The builder already validated each header name/value, so these
387        // conversions are expected to succeed; return a proper error if they
388        // somehow fail rather than panicking.
389        let mut header_map = reqwest::header::HeaderMap::new();
390        for (k, v) in config.headers() {
391            let name =
392                reqwest::header::HeaderName::from_bytes(k.as_bytes()).map_err(|_| LiterLlmError::InvalidHeader {
393                    name: k.clone(),
394                    reason: "pre-validated header name became invalid".into(),
395                })?;
396            let val = reqwest::header::HeaderValue::from_str(v).map_err(|_| LiterLlmError::InvalidHeader {
397                name: k.clone(),
398                reason: "pre-validated header value became invalid".into(),
399            })?;
400            header_map.insert(name, val);
401        }
402
403        let http = {
404            let builder = reqwest::Client::builder().default_headers(header_map);
405            // reqwest's WASM backend uses the browser fetch API and does not
406            // support per-client timeout configuration.
407            #[cfg(not(target_arch = "wasm32"))]
408            let builder = builder.timeout(config.timeout);
409            builder.build().map_err(LiterLlmError::from)?
410        };
411
412        // Pre-compute the auth header once at construction time to avoid
413        // `format!("Bearer {key}")` on every request.
414        let cached_auth_header = provider
415            .auth_header(config.api_key.expose_secret())
416            .map(|(name, value)| (name.into_owned(), value.into_owned()));
417
418        // Pre-compute static extra headers once to avoid `&'static str` ->
419        // `String` conversion on every request.
420        let cached_extra_headers = provider
421            .extra_headers()
422            .iter()
423            .map(|&(name, value)| (name.to_owned(), value.to_owned()))
424            .collect();
425
426        Ok(Self {
427            config,
428            http,
429            provider,
430            cached_auth_header,
431            cached_extra_headers,
432        })
433    }
434
435    /// Resolve the provider for a specific request based on the model string.
436    ///
437    /// If the model prefix clearly identifies a provider that differs from the
438    /// construction-time default, the detected provider is returned.  Otherwise
439    /// the construction-time provider is reused (zero allocation).
440    fn resolve_provider_for_model(&self, model: &str) -> Arc<dyn Provider> {
441        // When a base_url override is set, always use the construction-time
442        // provider — the user explicitly pointed the client at a specific
443        // endpoint (e.g. a mock server or custom proxy).
444        if self.config.base_url.is_some() {
445            return Arc::clone(&self.provider);
446        }
447        // If the construction-time provider already matches this model, keep it.
448        if self.provider.matches_model(model) {
449            return Arc::clone(&self.provider);
450        }
451        // Attempt per-request detection from the model prefix.
452        if let Some(detected) = provider::detect_provider(model) {
453            return Arc::from(detected);
454        }
455        // Fall back to the construction-time provider.
456        Arc::clone(&self.provider)
457    }
458
459    /// Compute the auth header for a given provider (potentially different from
460    /// the construction-time cached one).
461    async fn resolve_auth_header_for_provider(&self, prov: &dyn Provider) -> Result<Option<(String, String)>> {
462        if let Some(ref cp) = self.config.credential_provider {
463            let credential = cp.resolve().await?;
464            match credential {
465                Credential::BearerToken(token) => Ok(Some((
466                    "Authorization".to_owned(),
467                    format!("Bearer {}", token.expose_secret()),
468                ))),
469                Credential::AwsCredentials { .. } => Ok(None),
470            }
471        } else {
472            // Re-compute auth header for the resolved provider.
473            Ok(prov
474                .auth_header(self.config.api_key.expose_secret())
475                .map(|(name, value)| (name.into_owned(), value.into_owned())))
476        }
477    }
478
479    /// Build the combined header list for a request using a specific provider.
480    fn all_headers_for_provider(
481        &self,
482        prov: &dyn Provider,
483        method: &str,
484        url: &str,
485        body_json: &serde_json::Value,
486        body_bytes: &[u8],
487    ) -> Vec<(String, String)> {
488        let mut headers = prov.signing_headers(method, url, body_bytes);
489        headers.extend(
490            prov.extra_headers()
491                .iter()
492                .map(|&(name, value)| (name.to_owned(), value.to_owned())),
493        );
494        headers.extend(prov.dynamic_headers(body_json));
495        headers
496    }
497
498    /// Shared helper: resolve the per-request provider, build the URL, strip
499    /// model prefix from the request body, set the `stream` flag, apply provider
500    /// transform, and return everything needed to fire a request.
501    ///
502    /// `endpoint_fn` receives the resolved provider and returns the endpoint
503    /// path (e.g. `|p| p.chat_completions_path()`), ensuring the path comes
504    /// from the correct provider when per-request routing overrides the default.
505    ///
506    /// `stream` is inserted into the body **before** `transform_request` runs,
507    /// so providers can inspect the final body state in one pass.
508    fn prepare_request(
509        &self,
510        serializable: &impl serde::Serialize,
511        endpoint_fn: impl FnOnce(&dyn Provider) -> &str,
512        model: &str,
513        stream: Option<bool>,
514    ) -> Result<PreparedRequest> {
515        if model.is_empty() {
516            return Err(LiterLlmError::BadRequest {
517                message: "model must not be empty".into(),
518            });
519        }
520
521        let prov = self.resolve_provider_for_model(model);
522        let bare_model = prov.strip_model_prefix(model).to_owned();
523        // Use build_url so providers like Azure and Bedrock can embed the model
524        // name or deployment identifier into the URL.
525        let endpoint_path = endpoint_fn(prov.as_ref());
526        let url = prov.build_url(endpoint_path, &bare_model);
527
528        let mut body = serde_json::to_value(serializable)?;
529        if let Some(obj) = body.as_object_mut() {
530            obj.insert("model".into(), serde_json::Value::String(bare_model));
531            if let Some(s) = stream {
532                obj.insert("stream".into(), serde_json::Value::Bool(s));
533            }
534        }
535        prov.transform_request(&mut body)?;
536
537        // Serialize exactly once — the same bytes are used for signing and for
538        // the HTTP request body.  `Bytes` is reference-counted, so cloning on
539        // retry is a zero-copy bump.
540        let body_bytes = bytes::Bytes::from(serde_json::to_vec(&body)?);
541
542        Ok(PreparedRequest {
543            url,
544            provider: prov,
545            body_json: body,
546            body_bytes,
547        })
548    }
549
550    /// Resolve the auth header for a request using the construction-time provider.
551    ///
552    /// Uses the pre-computed cached auth header for efficiency.  When a
553    /// [`CredentialProvider`] is configured, it is called to obtain a fresh
554    /// credential which overrides the cached header.
555    async fn resolve_auth_header(&self) -> Result<Option<(String, String)>> {
556        if let Some(ref cp) = self.config.credential_provider {
557            let credential = cp.resolve().await?;
558            match credential {
559                Credential::BearerToken(token) => Ok(Some((
560                    "Authorization".to_owned(),
561                    format!("Bearer {}", token.expose_secret()),
562                ))),
563                Credential::AwsCredentials { .. } => Ok(None),
564            }
565        } else {
566            Ok(self.cached_auth_header.clone())
567        }
568    }
569
570    /// Build the combined header list using the construction-time provider.
571    ///
572    /// Uses pre-computed cached extra headers for efficiency.
573    fn all_headers(
574        &self,
575        method: &str,
576        url: &str,
577        body_json: &serde_json::Value,
578        body_bytes: &[u8],
579    ) -> Vec<(String, String)> {
580        let mut headers = self.provider.signing_headers(method, url, body_bytes);
581        headers.extend(self.cached_extra_headers.iter().cloned());
582        headers.extend(self.provider.dynamic_headers(body_json));
583        headers
584    }
585}
586
587#[cfg(any(feature = "native-http", feature = "wasm-http"))]
588/// Resolve the provider to use for all requests on this client.
589///
590/// Priority:
591/// 1. Explicit `base_url` in config -> custom OpenAI-compatible provider.
592/// 2. `model_hint` -> auto-detect by model name prefix.
593/// 3. Default -> OpenAI.
594fn build_provider(config: &ClientConfig, model_hint: Option<&str>) -> Arc<dyn Provider> {
595    if let Some(ref base_url) = config.base_url {
596        return Arc::new(OpenAiCompatibleProvider {
597            name: "custom".into(),
598            base_url: base_url.clone(),
599            env_var: None,
600            model_prefixes: vec![],
601        });
602    }
603
604    if let Some(model) = model_hint
605        && let Some(p) = provider::detect_provider(model)
606    {
607        // detect_provider returns Box<dyn Provider>; convert to Arc.
608        return Arc::from(p);
609    }
610
611    Arc::new(OpenAiProvider)
612}
613
614#[cfg(any(feature = "native-http", feature = "wasm-http"))]
615impl LlmClient for DefaultClient {
616    fn chat(&self, req: ChatCompletionRequest) -> BoxFuture<'_, Result<ChatCompletionResponse>> {
617        Box::pin(async move {
618            // Pass stream=false so providers can inspect the flag in transform_request.
619            let prepared = self.prepare_request(&req, |p| p.chat_completions_path(), &req.model, Some(false))?;
620
621            let auth_header = self
622                .resolve_auth_header_for_provider(prepared.provider.as_ref())
623                .await?;
624            let all_headers = self.all_headers_for_provider(
625                prepared.provider.as_ref(),
626                "POST",
627                &prepared.url,
628                &prepared.body_json,
629                &prepared.body_bytes,
630            );
631            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
632
633            let auth = auth_header.as_ref().map(str_pair);
634            let mut raw = http::request::post_json_raw(
635                &self.http,
636                &prepared.url,
637                auth,
638                &extra,
639                prepared.body_bytes,
640                self.config.max_retries,
641            )
642            .await?;
643            prepared.provider.transform_response(&mut raw)?;
644            serde_json::from_value::<ChatCompletionResponse>(raw).map_err(LiterLlmError::from)
645        })
646    }
647
648    fn chat_stream(
649        &self,
650        req: ChatCompletionRequest,
651    ) -> BoxFuture<'_, Result<BoxStream<'static, Result<ChatCompletionChunk>>>> {
652        Box::pin(async move {
653            // Use prepare_request for validation, model-prefix stripping, and
654            // transform_request — then override the URL via build_stream_url.
655            let prepared = self.prepare_request(&req, |p| p.chat_completions_path(), &req.model, Some(true))?;
656
657            // Always use build_stream_url for the streaming endpoint.
658            let bare_model = prepared.provider.strip_model_prefix(&req.model);
659            let url = prepared
660                .provider
661                .build_stream_url(prepared.provider.chat_completions_path(), bare_model);
662
663            let auth_header = self
664                .resolve_auth_header_for_provider(prepared.provider.as_ref())
665                .await?;
666            let all_headers = self.all_headers_for_provider(
667                prepared.provider.as_ref(),
668                "POST",
669                &url,
670                &prepared.body_json,
671                &prepared.body_bytes,
672            );
673            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
674            let auth = auth_header.as_ref().map(str_pair);
675
676            match prepared.provider.stream_format() {
677                provider::StreamFormat::Sse => {
678                    let provider = Arc::clone(&prepared.provider);
679                    let parse_event = move |data: &str| provider.parse_stream_event(data);
680                    let stream = http::streaming::post_stream(
681                        &self.http,
682                        &url,
683                        auth,
684                        &extra,
685                        prepared.body_bytes,
686                        self.config.max_retries,
687                        parse_event,
688                    )
689                    .await?;
690                    Ok(stream)
691                }
692                provider::StreamFormat::AwsEventStream => {
693                    let stream = http::eventstream::post_eventstream(
694                        &self.http,
695                        &url,
696                        auth,
697                        &extra,
698                        prepared.body_bytes,
699                        self.config.max_retries,
700                        provider::bedrock::parse_bedrock_stream_event,
701                    )
702                    .await?;
703                    Ok(stream)
704                }
705            }
706        })
707    }
708
709    fn embed(&self, req: EmbeddingRequest) -> BoxFuture<'_, Result<EmbeddingResponse>> {
710        Box::pin(async move {
711            // Embeddings have no stream flag; pass None so it is not inserted.
712            let prepared = self.prepare_request(&req, |p| p.embeddings_path(), &req.model, None)?;
713
714            let auth_header = self
715                .resolve_auth_header_for_provider(prepared.provider.as_ref())
716                .await?;
717            let all_headers = self.all_headers_for_provider(
718                prepared.provider.as_ref(),
719                "POST",
720                &prepared.url,
721                &prepared.body_json,
722                &prepared.body_bytes,
723            );
724            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
725
726            let auth = auth_header.as_ref().map(str_pair);
727            let mut raw = http::request::post_json_raw(
728                &self.http,
729                &prepared.url,
730                auth,
731                &extra,
732                prepared.body_bytes,
733                self.config.max_retries,
734            )
735            .await?;
736            prepared.provider.transform_response(&mut raw)?;
737            serde_json::from_value::<EmbeddingResponse>(raw).map_err(LiterLlmError::from)
738        })
739    }
740
741    fn list_models(&self) -> BoxFuture<'_, Result<ModelsListResponse>> {
742        Box::pin(async move {
743            // list_models has no model string — use the construction-time provider.
744            let url = self.provider.build_url(self.provider.models_path(), "");
745            let auth_header = self.resolve_auth_header().await?;
746            let auth = auth_header.as_ref().map(str_pair);
747            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
748            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
749
750            let mut raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
751            self.provider.transform_response(&mut raw)?;
752            serde_json::from_value::<ModelsListResponse>(raw).map_err(LiterLlmError::from)
753        })
754    }
755
756    fn image_generate(&self, req: CreateImageRequest) -> BoxFuture<'_, Result<ImagesResponse>> {
757        Box::pin(async move {
758            let model = req.model.as_deref().unwrap_or_default();
759            let prepared = self.prepare_request(&req, |p| p.image_generations_path(), model, None)?;
760
761            let auth_header = self
762                .resolve_auth_header_for_provider(prepared.provider.as_ref())
763                .await?;
764            let all_headers = self.all_headers_for_provider(
765                prepared.provider.as_ref(),
766                "POST",
767                &prepared.url,
768                &prepared.body_json,
769                &prepared.body_bytes,
770            );
771            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
772
773            let auth = auth_header.as_ref().map(str_pair);
774            let mut raw = http::request::post_json_raw(
775                &self.http,
776                &prepared.url,
777                auth,
778                &extra,
779                prepared.body_bytes,
780                self.config.max_retries,
781            )
782            .await?;
783            prepared.provider.transform_response(&mut raw)?;
784            serde_json::from_value::<ImagesResponse>(raw).map_err(LiterLlmError::from)
785        })
786    }
787
788    fn speech(&self, req: CreateSpeechRequest) -> BoxFuture<'_, Result<bytes::Bytes>> {
789        Box::pin(async move {
790            let prepared = self.prepare_request(&req, |p| p.audio_speech_path(), &req.model, None)?;
791
792            let auth_header = self
793                .resolve_auth_header_for_provider(prepared.provider.as_ref())
794                .await?;
795            let all_headers = self.all_headers_for_provider(
796                prepared.provider.as_ref(),
797                "POST",
798                &prepared.url,
799                &prepared.body_json,
800                &prepared.body_bytes,
801            );
802            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
803
804            let auth = auth_header.as_ref().map(str_pair);
805            http::request::post_binary(
806                &self.http,
807                &prepared.url,
808                auth,
809                &extra,
810                prepared.body_bytes,
811                self.config.max_retries,
812            )
813            .await
814        })
815    }
816
817    fn transcribe(&self, req: CreateTranscriptionRequest) -> BoxFuture<'_, Result<TranscriptionResponse>> {
818        Box::pin(async move {
819            let prepared = self.prepare_request(&req, |p| p.audio_transcriptions_path(), &req.model, None)?;
820
821            let auth_header = self
822                .resolve_auth_header_for_provider(prepared.provider.as_ref())
823                .await?;
824            let all_headers = self.all_headers_for_provider(
825                prepared.provider.as_ref(),
826                "POST",
827                &prepared.url,
828                &prepared.body_json,
829                &prepared.body_bytes,
830            );
831            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
832
833            let auth = auth_header.as_ref().map(str_pair);
834            let mut raw = http::request::post_json_raw(
835                &self.http,
836                &prepared.url,
837                auth,
838                &extra,
839                prepared.body_bytes,
840                self.config.max_retries,
841            )
842            .await?;
843            prepared.provider.transform_response(&mut raw)?;
844            serde_json::from_value::<TranscriptionResponse>(raw).map_err(LiterLlmError::from)
845        })
846    }
847
848    fn moderate(&self, req: ModerationRequest) -> BoxFuture<'_, Result<ModerationResponse>> {
849        Box::pin(async move {
850            let model = req.model.as_deref().unwrap_or_default();
851            let prepared = self.prepare_request(&req, |p| p.moderations_path(), model, None)?;
852
853            let auth_header = self
854                .resolve_auth_header_for_provider(prepared.provider.as_ref())
855                .await?;
856            let all_headers = self.all_headers_for_provider(
857                prepared.provider.as_ref(),
858                "POST",
859                &prepared.url,
860                &prepared.body_json,
861                &prepared.body_bytes,
862            );
863            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
864
865            let auth = auth_header.as_ref().map(str_pair);
866            let mut raw = http::request::post_json_raw(
867                &self.http,
868                &prepared.url,
869                auth,
870                &extra,
871                prepared.body_bytes,
872                self.config.max_retries,
873            )
874            .await?;
875            prepared.provider.transform_response(&mut raw)?;
876            serde_json::from_value::<ModerationResponse>(raw).map_err(LiterLlmError::from)
877        })
878    }
879
880    fn rerank(&self, req: RerankRequest) -> BoxFuture<'_, Result<RerankResponse>> {
881        Box::pin(async move {
882            let prepared = self.prepare_request(&req, |p| p.rerank_path(), &req.model, None)?;
883
884            let auth_header = self
885                .resolve_auth_header_for_provider(prepared.provider.as_ref())
886                .await?;
887            let all_headers = self.all_headers_for_provider(
888                prepared.provider.as_ref(),
889                "POST",
890                &prepared.url,
891                &prepared.body_json,
892                &prepared.body_bytes,
893            );
894            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
895
896            let auth = auth_header.as_ref().map(str_pair);
897            let mut raw = http::request::post_json_raw(
898                &self.http,
899                &prepared.url,
900                auth,
901                &extra,
902                prepared.body_bytes,
903                self.config.max_retries,
904            )
905            .await?;
906            prepared.provider.transform_response(&mut raw)?;
907            serde_json::from_value::<RerankResponse>(raw).map_err(LiterLlmError::from)
908        })
909    }
910
911    fn search(&self, req: SearchRequest) -> BoxFuture<'_, Result<SearchResponse>> {
912        Box::pin(async move {
913            let prepared = self.prepare_request(&req, |p| p.search_path(), &req.model, None)?;
914
915            let auth_header = self
916                .resolve_auth_header_for_provider(prepared.provider.as_ref())
917                .await?;
918            let all_headers = self.all_headers_for_provider(
919                prepared.provider.as_ref(),
920                "POST",
921                &prepared.url,
922                &prepared.body_json,
923                &prepared.body_bytes,
924            );
925            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
926
927            let auth = auth_header.as_ref().map(str_pair);
928            let mut raw = http::request::post_json_raw(
929                &self.http,
930                &prepared.url,
931                auth,
932                &extra,
933                prepared.body_bytes,
934                self.config.max_retries,
935            )
936            .await?;
937            prepared.provider.transform_response(&mut raw)?;
938            serde_json::from_value::<SearchResponse>(raw).map_err(LiterLlmError::from)
939        })
940    }
941
942    fn ocr(&self, req: OcrRequest) -> BoxFuture<'_, Result<OcrResponse>> {
943        Box::pin(async move {
944            let prepared = self.prepare_request(&req, |p| p.ocr_path(), &req.model, None)?;
945
946            let auth_header = self
947                .resolve_auth_header_for_provider(prepared.provider.as_ref())
948                .await?;
949            let all_headers = self.all_headers_for_provider(
950                prepared.provider.as_ref(),
951                "POST",
952                &prepared.url,
953                &prepared.body_json,
954                &prepared.body_bytes,
955            );
956            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
957
958            let auth = auth_header.as_ref().map(str_pair);
959            let mut raw = http::request::post_json_raw(
960                &self.http,
961                &prepared.url,
962                auth,
963                &extra,
964                prepared.body_bytes,
965                self.config.max_retries,
966            )
967            .await?;
968            prepared.provider.transform_response(&mut raw)?;
969            serde_json::from_value::<OcrResponse>(raw).map_err(LiterLlmError::from)
970        })
971    }
972}
973
974#[cfg(any(feature = "native-http", feature = "wasm-http"))]
975impl LlmClientRaw for DefaultClient {
976    fn chat_raw(&self, req: ChatCompletionRequest) -> BoxFuture<'_, Result<RawExchange<ChatCompletionResponse>>> {
977        Box::pin(async move {
978            let prepared = self.prepare_request(&req, |p| p.chat_completions_path(), &req.model, Some(false))?;
979            let raw_request = prepared.body_json.clone();
980
981            let auth_header = self
982                .resolve_auth_header_for_provider(prepared.provider.as_ref())
983                .await?;
984            let all_headers = self.all_headers_for_provider(
985                prepared.provider.as_ref(),
986                "POST",
987                &prepared.url,
988                &prepared.body_json,
989                &prepared.body_bytes,
990            );
991            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
992
993            let auth = auth_header.as_ref().map(str_pair);
994            let mut raw = http::request::post_json_raw(
995                &self.http,
996                &prepared.url,
997                auth,
998                &extra,
999                prepared.body_bytes,
1000                self.config.max_retries,
1001            )
1002            .await?;
1003
1004            let raw_response = Some(raw.clone());
1005            prepared.provider.transform_response(&mut raw)?;
1006            let data = serde_json::from_value::<ChatCompletionResponse>(raw).map_err(LiterLlmError::from)?;
1007
1008            Ok(RawExchange {
1009                data,
1010                raw_request,
1011                raw_response,
1012            })
1013        })
1014    }
1015
1016    fn chat_stream_raw(
1017        &self,
1018        req: ChatCompletionRequest,
1019    ) -> BoxFuture<'_, Result<RawStreamExchange<BoxStream<'static, Result<ChatCompletionChunk>>>>> {
1020        Box::pin(async move {
1021            let prepared = self.prepare_request(&req, |p| p.chat_completions_path(), &req.model, Some(true))?;
1022            let raw_request = prepared.body_json.clone();
1023
1024            let bare_model = prepared.provider.strip_model_prefix(&req.model);
1025            let url = prepared
1026                .provider
1027                .build_stream_url(prepared.provider.chat_completions_path(), bare_model);
1028
1029            let auth_header = self
1030                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1031                .await?;
1032            let all_headers = self.all_headers_for_provider(
1033                prepared.provider.as_ref(),
1034                "POST",
1035                &url,
1036                &prepared.body_json,
1037                &prepared.body_bytes,
1038            );
1039            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1040            let auth = auth_header.as_ref().map(str_pair);
1041
1042            let stream = match prepared.provider.stream_format() {
1043                provider::StreamFormat::Sse => {
1044                    let provider = Arc::clone(&prepared.provider);
1045                    let parse_event = move |data: &str| provider.parse_stream_event(data);
1046                    http::streaming::post_stream(
1047                        &self.http,
1048                        &url,
1049                        auth,
1050                        &extra,
1051                        prepared.body_bytes,
1052                        self.config.max_retries,
1053                        parse_event,
1054                    )
1055                    .await?
1056                }
1057                provider::StreamFormat::AwsEventStream => {
1058                    http::eventstream::post_eventstream(
1059                        &self.http,
1060                        &url,
1061                        auth,
1062                        &extra,
1063                        prepared.body_bytes,
1064                        self.config.max_retries,
1065                        provider::bedrock::parse_bedrock_stream_event,
1066                    )
1067                    .await?
1068                }
1069            };
1070
1071            Ok(RawStreamExchange { stream, raw_request })
1072        })
1073    }
1074
1075    fn embed_raw(&self, req: EmbeddingRequest) -> BoxFuture<'_, Result<RawExchange<EmbeddingResponse>>> {
1076        Box::pin(async move {
1077            let prepared = self.prepare_request(&req, |p| p.embeddings_path(), &req.model, None)?;
1078            let raw_request = prepared.body_json.clone();
1079
1080            let auth_header = self
1081                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1082                .await?;
1083            let all_headers = self.all_headers_for_provider(
1084                prepared.provider.as_ref(),
1085                "POST",
1086                &prepared.url,
1087                &prepared.body_json,
1088                &prepared.body_bytes,
1089            );
1090            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1091
1092            let auth = auth_header.as_ref().map(str_pair);
1093            let mut raw = http::request::post_json_raw(
1094                &self.http,
1095                &prepared.url,
1096                auth,
1097                &extra,
1098                prepared.body_bytes,
1099                self.config.max_retries,
1100            )
1101            .await?;
1102
1103            let raw_response = Some(raw.clone());
1104            prepared.provider.transform_response(&mut raw)?;
1105            let data = serde_json::from_value::<EmbeddingResponse>(raw).map_err(LiterLlmError::from)?;
1106
1107            Ok(RawExchange {
1108                data,
1109                raw_request,
1110                raw_response,
1111            })
1112        })
1113    }
1114
1115    fn image_generate_raw(&self, req: CreateImageRequest) -> BoxFuture<'_, Result<RawExchange<ImagesResponse>>> {
1116        Box::pin(async move {
1117            let model = req.model.as_deref().unwrap_or_default();
1118            let prepared = self.prepare_request(&req, |p| p.image_generations_path(), model, None)?;
1119            let raw_request = prepared.body_json.clone();
1120
1121            let auth_header = self
1122                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1123                .await?;
1124            let all_headers = self.all_headers_for_provider(
1125                prepared.provider.as_ref(),
1126                "POST",
1127                &prepared.url,
1128                &prepared.body_json,
1129                &prepared.body_bytes,
1130            );
1131            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1132
1133            let auth = auth_header.as_ref().map(str_pair);
1134            let mut raw = http::request::post_json_raw(
1135                &self.http,
1136                &prepared.url,
1137                auth,
1138                &extra,
1139                prepared.body_bytes,
1140                self.config.max_retries,
1141            )
1142            .await?;
1143
1144            let raw_response = Some(raw.clone());
1145            prepared.provider.transform_response(&mut raw)?;
1146            let data = serde_json::from_value::<ImagesResponse>(raw).map_err(LiterLlmError::from)?;
1147
1148            Ok(RawExchange {
1149                data,
1150                raw_request,
1151                raw_response,
1152            })
1153        })
1154    }
1155
1156    fn transcribe_raw(
1157        &self,
1158        req: CreateTranscriptionRequest,
1159    ) -> BoxFuture<'_, Result<RawExchange<TranscriptionResponse>>> {
1160        Box::pin(async move {
1161            let prepared = self.prepare_request(&req, |p| p.audio_transcriptions_path(), &req.model, None)?;
1162            let raw_request = prepared.body_json.clone();
1163
1164            let auth_header = self
1165                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1166                .await?;
1167            let all_headers = self.all_headers_for_provider(
1168                prepared.provider.as_ref(),
1169                "POST",
1170                &prepared.url,
1171                &prepared.body_json,
1172                &prepared.body_bytes,
1173            );
1174            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1175
1176            let auth = auth_header.as_ref().map(str_pair);
1177            let mut raw = http::request::post_json_raw(
1178                &self.http,
1179                &prepared.url,
1180                auth,
1181                &extra,
1182                prepared.body_bytes,
1183                self.config.max_retries,
1184            )
1185            .await?;
1186
1187            let raw_response = Some(raw.clone());
1188            prepared.provider.transform_response(&mut raw)?;
1189            let data = serde_json::from_value::<TranscriptionResponse>(raw).map_err(LiterLlmError::from)?;
1190
1191            Ok(RawExchange {
1192                data,
1193                raw_request,
1194                raw_response,
1195            })
1196        })
1197    }
1198
1199    fn moderate_raw(&self, req: ModerationRequest) -> BoxFuture<'_, Result<RawExchange<ModerationResponse>>> {
1200        Box::pin(async move {
1201            let model = req.model.as_deref().unwrap_or_default();
1202            let prepared = self.prepare_request(&req, |p| p.moderations_path(), model, None)?;
1203            let raw_request = prepared.body_json.clone();
1204
1205            let auth_header = self
1206                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1207                .await?;
1208            let all_headers = self.all_headers_for_provider(
1209                prepared.provider.as_ref(),
1210                "POST",
1211                &prepared.url,
1212                &prepared.body_json,
1213                &prepared.body_bytes,
1214            );
1215            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1216
1217            let auth = auth_header.as_ref().map(str_pair);
1218            let mut raw = http::request::post_json_raw(
1219                &self.http,
1220                &prepared.url,
1221                auth,
1222                &extra,
1223                prepared.body_bytes,
1224                self.config.max_retries,
1225            )
1226            .await?;
1227
1228            let raw_response = Some(raw.clone());
1229            prepared.provider.transform_response(&mut raw)?;
1230            let data = serde_json::from_value::<ModerationResponse>(raw).map_err(LiterLlmError::from)?;
1231
1232            Ok(RawExchange {
1233                data,
1234                raw_request,
1235                raw_response,
1236            })
1237        })
1238    }
1239
1240    fn rerank_raw(&self, req: RerankRequest) -> BoxFuture<'_, Result<RawExchange<RerankResponse>>> {
1241        Box::pin(async move {
1242            let prepared = self.prepare_request(&req, |p| p.rerank_path(), &req.model, None)?;
1243            let raw_request = prepared.body_json.clone();
1244
1245            let auth_header = self
1246                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1247                .await?;
1248            let all_headers = self.all_headers_for_provider(
1249                prepared.provider.as_ref(),
1250                "POST",
1251                &prepared.url,
1252                &prepared.body_json,
1253                &prepared.body_bytes,
1254            );
1255            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1256
1257            let auth = auth_header.as_ref().map(str_pair);
1258            let mut raw = http::request::post_json_raw(
1259                &self.http,
1260                &prepared.url,
1261                auth,
1262                &extra,
1263                prepared.body_bytes,
1264                self.config.max_retries,
1265            )
1266            .await?;
1267
1268            let raw_response = Some(raw.clone());
1269            prepared.provider.transform_response(&mut raw)?;
1270            let data = serde_json::from_value::<RerankResponse>(raw).map_err(LiterLlmError::from)?;
1271
1272            Ok(RawExchange {
1273                data,
1274                raw_request,
1275                raw_response,
1276            })
1277        })
1278    }
1279
1280    fn search_raw(&self, req: SearchRequest) -> BoxFuture<'_, Result<RawExchange<SearchResponse>>> {
1281        Box::pin(async move {
1282            let prepared = self.prepare_request(&req, |p| p.search_path(), &req.model, None)?;
1283            let raw_request = prepared.body_json.clone();
1284
1285            let auth_header = self
1286                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1287                .await?;
1288            let all_headers = self.all_headers_for_provider(
1289                prepared.provider.as_ref(),
1290                "POST",
1291                &prepared.url,
1292                &prepared.body_json,
1293                &prepared.body_bytes,
1294            );
1295            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1296
1297            let auth = auth_header.as_ref().map(str_pair);
1298            let mut raw = http::request::post_json_raw(
1299                &self.http,
1300                &prepared.url,
1301                auth,
1302                &extra,
1303                prepared.body_bytes,
1304                self.config.max_retries,
1305            )
1306            .await?;
1307
1308            let raw_response = Some(raw.clone());
1309            prepared.provider.transform_response(&mut raw)?;
1310            let data = serde_json::from_value::<SearchResponse>(raw).map_err(LiterLlmError::from)?;
1311
1312            Ok(RawExchange {
1313                data,
1314                raw_request,
1315                raw_response,
1316            })
1317        })
1318    }
1319
1320    fn ocr_raw(&self, req: OcrRequest) -> BoxFuture<'_, Result<RawExchange<OcrResponse>>> {
1321        Box::pin(async move {
1322            let prepared = self.prepare_request(&req, |p| p.ocr_path(), &req.model, None)?;
1323            let raw_request = prepared.body_json.clone();
1324
1325            let auth_header = self
1326                .resolve_auth_header_for_provider(prepared.provider.as_ref())
1327                .await?;
1328            let all_headers = self.all_headers_for_provider(
1329                prepared.provider.as_ref(),
1330                "POST",
1331                &prepared.url,
1332                &prepared.body_json,
1333                &prepared.body_bytes,
1334            );
1335            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1336
1337            let auth = auth_header.as_ref().map(str_pair);
1338            let mut raw = http::request::post_json_raw(
1339                &self.http,
1340                &prepared.url,
1341                auth,
1342                &extra,
1343                prepared.body_bytes,
1344                self.config.max_retries,
1345            )
1346            .await?;
1347
1348            let raw_response = Some(raw.clone());
1349            prepared.provider.transform_response(&mut raw)?;
1350            let data = serde_json::from_value::<OcrResponse>(raw).map_err(LiterLlmError::from)?;
1351
1352            Ok(RawExchange {
1353                data,
1354                raw_request,
1355                raw_response,
1356            })
1357        })
1358    }
1359}
1360
1361#[cfg(any(feature = "native-http", feature = "wasm-http"))]
1362impl FileClient for DefaultClient {
1363    fn create_file(&self, req: CreateFileRequest) -> BoxFuture<'_, Result<FileObject>> {
1364        Box::pin(async move {
1365            let url = self.provider.build_url(self.provider.files_path(), "");
1366            let auth_header = self.resolve_auth_header().await?;
1367            let auth = auth_header.as_ref().map(str_pair);
1368            let all_headers = self.all_headers("POST", &url, &serde_json::Value::Null, &[]);
1369            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1370
1371            // Decode the base64-encoded file data into raw bytes for the multipart upload.
1372            use base64::Engine;
1373            let file_bytes = base64::engine::general_purpose::STANDARD
1374                .decode(&req.file)
1375                .map_err(|e| LiterLlmError::BadRequest {
1376                    message: format!("invalid base64 file data: {e}"),
1377                })?;
1378
1379            let filename = req.filename.unwrap_or_else(|| "upload".to_owned());
1380            let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(filename);
1381            let purpose_str = serde_json::to_value(&req.purpose)?
1382                .as_str()
1383                .unwrap_or_default()
1384                .to_owned();
1385            let form = reqwest::multipart::Form::new()
1386                .part("file", file_part)
1387                .text("purpose", purpose_str);
1388
1389            let raw = http::request::post_multipart(&self.http, &url, auth, &extra, form).await?;
1390            serde_json::from_value::<FileObject>(raw).map_err(LiterLlmError::from)
1391        })
1392    }
1393
1394    fn retrieve_file(&self, file_id: &str) -> BoxFuture<'_, Result<FileObject>> {
1395        let file_id = file_id.to_owned();
1396        Box::pin(async move {
1397            let url = format!(
1398                "{}/{}",
1399                self.provider.build_url(self.provider.files_path(), ""),
1400                file_id
1401            );
1402            let auth_header = self.resolve_auth_header().await?;
1403            let auth = auth_header.as_ref().map(str_pair);
1404            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1405            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1406
1407            let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1408            serde_json::from_value::<FileObject>(raw).map_err(LiterLlmError::from)
1409        })
1410    }
1411
1412    fn delete_file(&self, file_id: &str) -> BoxFuture<'_, Result<DeleteResponse>> {
1413        let file_id = file_id.to_owned();
1414        Box::pin(async move {
1415            let url = format!(
1416                "{}/{}",
1417                self.provider.build_url(self.provider.files_path(), ""),
1418                file_id
1419            );
1420            let auth_header = self.resolve_auth_header().await?;
1421            let auth = auth_header.as_ref().map(str_pair);
1422            let all_headers = self.all_headers("DELETE", &url, &serde_json::Value::Null, &[]);
1423            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1424
1425            let raw = http::request::delete_json(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1426            serde_json::from_value::<DeleteResponse>(raw).map_err(LiterLlmError::from)
1427        })
1428    }
1429
1430    fn list_files(&self, query: Option<FileListQuery>) -> BoxFuture<'_, Result<FileListResponse>> {
1431        Box::pin(async move {
1432            let base_url = self.provider.build_url(self.provider.files_path(), "");
1433            let url = if let Some(ref q) = query {
1434                let mut params = Vec::new();
1435                if let Some(ref purpose) = q.purpose {
1436                    params.push(format!("purpose={purpose}"));
1437                }
1438                if let Some(limit) = q.limit {
1439                    params.push(format!("limit={limit}"));
1440                }
1441                if let Some(ref after) = q.after {
1442                    params.push(format!("after={after}"));
1443                }
1444                if params.is_empty() {
1445                    base_url
1446                } else {
1447                    format!("{base_url}?{}", params.join("&"))
1448                }
1449            } else {
1450                base_url
1451            };
1452            let auth_header = self.resolve_auth_header().await?;
1453            let auth = auth_header.as_ref().map(str_pair);
1454            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1455            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1456
1457            let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1458            serde_json::from_value::<FileListResponse>(raw).map_err(LiterLlmError::from)
1459        })
1460    }
1461
1462    fn file_content(&self, file_id: &str) -> BoxFuture<'_, Result<bytes::Bytes>> {
1463        let file_id = file_id.to_owned();
1464        Box::pin(async move {
1465            let url = format!(
1466                "{}/{}/content",
1467                self.provider.build_url(self.provider.files_path(), ""),
1468                file_id
1469            );
1470            let auth_header = self.resolve_auth_header().await?;
1471            let auth = auth_header.as_ref().map(str_pair);
1472            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1473            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1474
1475            http::request::get_binary(&self.http, &url, auth, &extra, self.config.max_retries).await
1476        })
1477    }
1478}
1479
1480#[cfg(any(feature = "native-http", feature = "wasm-http"))]
1481impl BatchClient for DefaultClient {
1482    fn create_batch(&self, req: CreateBatchRequest) -> BoxFuture<'_, Result<BatchObject>> {
1483        Box::pin(async move {
1484            let url = self.provider.build_url(self.provider.batches_path(), "");
1485            let body_bytes = bytes::Bytes::from(serde_json::to_vec(&req)?);
1486            let body_json = serde_json::to_value(&req)?;
1487
1488            let auth_header = self.resolve_auth_header().await?;
1489            let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
1490            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1491            let auth = auth_header.as_ref().map(str_pair);
1492
1493            let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
1494                .await?;
1495            serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
1496        })
1497    }
1498
1499    fn retrieve_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>> {
1500        let batch_id = batch_id.to_owned();
1501        Box::pin(async move {
1502            let url = format!(
1503                "{}/{}",
1504                self.provider.build_url(self.provider.batches_path(), ""),
1505                batch_id
1506            );
1507            let auth_header = self.resolve_auth_header().await?;
1508            let auth = auth_header.as_ref().map(str_pair);
1509            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1510            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1511
1512            let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1513            serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
1514        })
1515    }
1516
1517    fn list_batches(&self, query: Option<BatchListQuery>) -> BoxFuture<'_, Result<BatchListResponse>> {
1518        Box::pin(async move {
1519            let base_url = self.provider.build_url(self.provider.batches_path(), "");
1520            let url = if let Some(ref q) = query {
1521                let mut params = Vec::new();
1522                if let Some(limit) = q.limit {
1523                    params.push(format!("limit={limit}"));
1524                }
1525                if let Some(ref after) = q.after {
1526                    params.push(format!("after={after}"));
1527                }
1528                if params.is_empty() {
1529                    base_url
1530                } else {
1531                    format!("{base_url}?{}", params.join("&"))
1532                }
1533            } else {
1534                base_url
1535            };
1536            let auth_header = self.resolve_auth_header().await?;
1537            let auth = auth_header.as_ref().map(str_pair);
1538            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1539            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1540
1541            let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1542            serde_json::from_value::<BatchListResponse>(raw).map_err(LiterLlmError::from)
1543        })
1544    }
1545
1546    fn cancel_batch(&self, batch_id: &str) -> BoxFuture<'_, Result<BatchObject>> {
1547        let batch_id = batch_id.to_owned();
1548        Box::pin(async move {
1549            let url = format!(
1550                "{}/{}/cancel",
1551                self.provider.build_url(self.provider.batches_path(), ""),
1552                batch_id
1553            );
1554            let auth_header = self.resolve_auth_header().await?;
1555            let body_json = serde_json::Value::Null;
1556            let body_bytes = bytes::Bytes::new();
1557            let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
1558            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1559            let auth = auth_header.as_ref().map(str_pair);
1560
1561            let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
1562                .await?;
1563            serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
1564        })
1565    }
1566}
1567
1568#[cfg(any(feature = "native-http", feature = "wasm-http"))]
1569impl ResponseClient for DefaultClient {
1570    fn create_response(&self, req: CreateResponseRequest) -> BoxFuture<'_, Result<ResponseObject>> {
1571        Box::pin(async move {
1572            let url = self.provider.build_url(self.provider.responses_path(), "");
1573            let body_bytes = bytes::Bytes::from(serde_json::to_vec(&req)?);
1574            let body_json = serde_json::to_value(&req)?;
1575
1576            let auth_header = self.resolve_auth_header().await?;
1577            let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
1578            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1579            let auth = auth_header.as_ref().map(str_pair);
1580
1581            let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
1582                .await?;
1583            serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
1584        })
1585    }
1586
1587    fn retrieve_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>> {
1588        let id = id.to_owned();
1589        Box::pin(async move {
1590            let url = format!("{}/{}", self.provider.build_url(self.provider.responses_path(), ""), id);
1591            let auth_header = self.resolve_auth_header().await?;
1592            let auth = auth_header.as_ref().map(str_pair);
1593            let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
1594            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1595
1596            let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
1597            serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
1598        })
1599    }
1600
1601    fn cancel_response(&self, id: &str) -> BoxFuture<'_, Result<ResponseObject>> {
1602        let id = id.to_owned();
1603        Box::pin(async move {
1604            let url = format!(
1605                "{}/{}/cancel",
1606                self.provider.build_url(self.provider.responses_path(), ""),
1607                id
1608            );
1609            let auth_header = self.resolve_auth_header().await?;
1610            let body_json = serde_json::Value::Null;
1611            let body_bytes = bytes::Bytes::new();
1612            let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
1613            let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
1614            let auth = auth_header.as_ref().map(str_pair);
1615
1616            let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
1617                .await?;
1618            serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
1619        })
1620    }
1621}