liter_llm 1.0.0.pre.rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +239 -0
  3. data/ext/liter_llm_rb/extconf.rb +65 -0
  4. data/ext/liter_llm_rb/native/.cargo/config.toml +23 -0
  5. data/ext/liter_llm_rb/native/Cargo.lock +3713 -0
  6. data/ext/liter_llm_rb/native/Cargo.toml +32 -0
  7. data/ext/liter_llm_rb/native/build.rs +15 -0
  8. data/ext/liter_llm_rb/native/src/lib.rs +1079 -0
  9. data/lib/liter_llm.rb +8 -0
  10. data/sig/liter_llm.rbs +416 -0
  11. data/vendor/Cargo.toml +54 -0
  12. data/vendor/liter-llm/Cargo.toml +92 -0
  13. data/vendor/liter-llm/README.md +252 -0
  14. data/vendor/liter-llm/schemas/pricing.json +40 -0
  15. data/vendor/liter-llm/schemas/providers.json +1662 -0
  16. data/vendor/liter-llm/src/auth/azure_ad.rs +264 -0
  17. data/vendor/liter-llm/src/auth/bedrock_sts.rs +353 -0
  18. data/vendor/liter-llm/src/auth/mod.rs +68 -0
  19. data/vendor/liter-llm/src/auth/vertex_oauth.rs +353 -0
  20. data/vendor/liter-llm/src/client/config.rs +351 -0
  21. data/vendor/liter-llm/src/client/managed.rs +622 -0
  22. data/vendor/liter-llm/src/client/mod.rs +864 -0
  23. data/vendor/liter-llm/src/cost.rs +212 -0
  24. data/vendor/liter-llm/src/error.rs +190 -0
  25. data/vendor/liter-llm/src/http/eventstream.rs +860 -0
  26. data/vendor/liter-llm/src/http/mod.rs +12 -0
  27. data/vendor/liter-llm/src/http/request.rs +438 -0
  28. data/vendor/liter-llm/src/http/retry.rs +72 -0
  29. data/vendor/liter-llm/src/http/streaming.rs +289 -0
  30. data/vendor/liter-llm/src/lib.rs +37 -0
  31. data/vendor/liter-llm/src/provider/anthropic.rs +2250 -0
  32. data/vendor/liter-llm/src/provider/azure.rs +579 -0
  33. data/vendor/liter-llm/src/provider/bedrock.rs +1543 -0
  34. data/vendor/liter-llm/src/provider/cohere.rs +654 -0
  35. data/vendor/liter-llm/src/provider/custom.rs +404 -0
  36. data/vendor/liter-llm/src/provider/google_ai.rs +281 -0
  37. data/vendor/liter-llm/src/provider/mistral.rs +188 -0
  38. data/vendor/liter-llm/src/provider/mod.rs +616 -0
  39. data/vendor/liter-llm/src/provider/vertex.rs +1504 -0
  40. data/vendor/liter-llm/src/tests.rs +1425 -0
  41. data/vendor/liter-llm/src/tokenizer.rs +281 -0
  42. data/vendor/liter-llm/src/tower/budget.rs +599 -0
  43. data/vendor/liter-llm/src/tower/cache.rs +502 -0
  44. data/vendor/liter-llm/src/tower/cache_opendal.rs +270 -0
  45. data/vendor/liter-llm/src/tower/cooldown.rs +231 -0
  46. data/vendor/liter-llm/src/tower/cost.rs +404 -0
  47. data/vendor/liter-llm/src/tower/fallback.rs +121 -0
  48. data/vendor/liter-llm/src/tower/health.rs +219 -0
  49. data/vendor/liter-llm/src/tower/hooks.rs +369 -0
  50. data/vendor/liter-llm/src/tower/mod.rs +77 -0
  51. data/vendor/liter-llm/src/tower/rate_limit.rs +300 -0
  52. data/vendor/liter-llm/src/tower/router.rs +436 -0
  53. data/vendor/liter-llm/src/tower/service.rs +181 -0
  54. data/vendor/liter-llm/src/tower/tests.rs +539 -0
  55. data/vendor/liter-llm/src/tower/tests_common.rs +252 -0
  56. data/vendor/liter-llm/src/tower/tracing.rs +209 -0
  57. data/vendor/liter-llm/src/tower/types.rs +170 -0
  58. data/vendor/liter-llm/src/types/audio.rs +52 -0
  59. data/vendor/liter-llm/src/types/batch.rs +77 -0
  60. data/vendor/liter-llm/src/types/chat.rs +214 -0
  61. data/vendor/liter-llm/src/types/common.rs +244 -0
  62. data/vendor/liter-llm/src/types/embedding.rs +84 -0
  63. data/vendor/liter-llm/src/types/files.rs +58 -0
  64. data/vendor/liter-llm/src/types/image.rs +40 -0
  65. data/vendor/liter-llm/src/types/mod.rs +27 -0
  66. data/vendor/liter-llm/src/types/models.rs +21 -0
  67. data/vendor/liter-llm/src/types/moderation.rs +80 -0
  68. data/vendor/liter-llm/src/types/ocr.rs +87 -0
  69. data/vendor/liter-llm/src/types/rerank.rs +46 -0
  70. data/vendor/liter-llm/src/types/responses.rs +55 -0
  71. data/vendor/liter-llm/src/types/search.rs +45 -0
  72. data/vendor/liter-llm/tests/contract.rs +332 -0
  73. data/vendor/liter-llm-ffi/Cargo.toml +30 -0
  74. data/vendor/liter-llm-ffi/build.rs +66 -0
  75. data/vendor/liter-llm-ffi/cbindgen.toml +60 -0
  76. data/vendor/liter-llm-ffi/liter_llm.h +850 -0
  77. data/vendor/liter-llm-ffi/src/lib.rs +2488 -0
  78. metadata +286 -0
@@ -0,0 +1,864 @@
1
+ pub mod config;
2
+ #[cfg(all(feature = "native-http", feature = "tower"))]
3
+ pub mod managed;
4
+
5
+ use std::future::Future;
6
+ use std::pin::Pin;
7
+ #[cfg(feature = "native-http")]
8
+ use std::sync::Arc;
9
+
10
+ use futures_core::Stream;
11
+
12
+ use crate::error::Result;
13
+ use crate::types::audio::{CreateSpeechRequest, CreateTranscriptionRequest, TranscriptionResponse};
14
+ use crate::types::batch::{BatchListQuery, BatchListResponse, BatchObject, CreateBatchRequest};
15
+ use crate::types::files::{CreateFileRequest, DeleteResponse, FileListQuery, FileListResponse, FileObject};
16
+ use crate::types::image::{CreateImageRequest, ImagesResponse};
17
+ use crate::types::moderation::{ModerationRequest, ModerationResponse};
18
+ use crate::types::ocr::{OcrRequest, OcrResponse};
19
+ use crate::types::rerank::{RerankRequest, RerankResponse};
20
+ use crate::types::responses::{CreateResponseRequest, ResponseObject};
21
+ use crate::types::search::{SearchRequest, SearchResponse};
22
+ use crate::types::{
23
+ ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, EmbeddingRequest, EmbeddingResponse,
24
+ ModelsListResponse,
25
+ };
26
+
27
+ // DefaultClient and its LlmClient impl require reqwest + tokio.
28
+ #[cfg(feature = "native-http")]
29
+ use crate::auth::Credential;
30
+ #[cfg(feature = "native-http")]
31
+ use crate::error::LiterLlmError;
32
+ #[cfg(feature = "native-http")]
33
+ use crate::http;
34
+ #[cfg(feature = "native-http")]
35
+ use crate::provider::{self, OpenAiCompatibleProvider, OpenAiProvider, Provider};
36
+ #[cfg(feature = "native-http")]
37
+ use secrecy::ExposeSecret;
38
+
39
+ pub use config::{ClientConfig, ClientConfigBuilder};
40
+
41
+ /// A boxed future returning `Result<T>`.
42
+ pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
43
+
44
+ /// A boxed stream of `Result<T>`.
45
+ pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = Result<T>> + Send + 'a>>;
46
+
47
+ /// Result of [`DefaultClient::prepare_request`]:
48
+ /// `(url, optional_auth_header, body_json, body_bytes)`.
49
+ ///
50
+ /// The body is pre-serialized into `bytes::Bytes` so it is serialized exactly
51
+ /// once — the same bytes are used for signing headers and for the HTTP request
52
+ /// body. On retry, cloning `Bytes` is a zero-copy ref-count bump.
53
+ ///
54
+ /// `body_json` is the pre-serialization JSON value, retained so that
55
+ /// [`Provider::dynamic_headers`] can inspect request fields without
56
+ /// re-parsing.
57
+ ///
58
+ /// The auth header is `None` when the provider requires no authentication
59
+ /// (e.g. local models or providers with `auth: none`).
60
+ /// Extra headers are accessed directly from the provider via `extra_headers()`.
61
+ #[cfg(feature = "native-http")]
62
+ type PreparedRequest = (String, Option<(String, String)>, serde_json::Value, bytes::Bytes);
63
+
64
+ /// Convert an owned `(String, String)` auth header pair to `(&str, &str)` borrows.
65
+ ///
66
+ /// Centralises the four identical `map(|(n, v)| (n.as_str(), v.as_str()))` expressions
67
+ /// that appear wherever we hand headers to the HTTP layer.
68
+ #[cfg(feature = "native-http")]
69
+ fn str_pair(pair: &(String, String)) -> (&str, &str) {
70
+ (pair.0.as_str(), pair.1.as_str())
71
+ }
72
+
73
+ /// Core LLM client trait.
74
+ pub trait LlmClient: Send + Sync {
75
+ /// Send a chat completion request.
76
+ fn chat(&self, req: ChatCompletionRequest) -> BoxFuture<'_, ChatCompletionResponse>;
77
+
78
+ /// Send a streaming chat completion request.
79
+ fn chat_stream(&self, req: ChatCompletionRequest) -> BoxFuture<'_, BoxStream<'_, ChatCompletionChunk>>;
80
+
81
+ /// Send an embedding request.
82
+ fn embed(&self, req: EmbeddingRequest) -> BoxFuture<'_, EmbeddingResponse>;
83
+
84
+ /// List available models.
85
+ fn list_models(&self) -> BoxFuture<'_, ModelsListResponse>;
86
+
87
+ /// Generate an image.
88
+ fn image_generate(&self, req: CreateImageRequest) -> BoxFuture<'_, ImagesResponse>;
89
+
90
+ /// Generate speech audio from text.
91
+ fn speech(&self, req: CreateSpeechRequest) -> BoxFuture<'_, bytes::Bytes>;
92
+
93
+ /// Transcribe audio to text.
94
+ fn transcribe(&self, req: CreateTranscriptionRequest) -> BoxFuture<'_, TranscriptionResponse>;
95
+
96
+ /// Check content against moderation policies.
97
+ fn moderate(&self, req: ModerationRequest) -> BoxFuture<'_, ModerationResponse>;
98
+
99
+ /// Rerank documents by relevance to a query.
100
+ fn rerank(&self, req: RerankRequest) -> BoxFuture<'_, RerankResponse>;
101
+
102
+ /// Perform a web/document search.
103
+ fn search(&self, req: SearchRequest) -> BoxFuture<'_, SearchResponse>;
104
+
105
+ /// Extract text from a document via OCR.
106
+ fn ocr(&self, req: OcrRequest) -> BoxFuture<'_, OcrResponse>;
107
+ }
108
+
109
+ /// File management operations (upload, list, retrieve, delete).
110
+ pub trait FileClient: Send + Sync {
111
+ /// Upload a file.
112
+ fn create_file(&self, req: CreateFileRequest) -> BoxFuture<'_, FileObject>;
113
+
114
+ /// Retrieve metadata for a file.
115
+ fn retrieve_file(&self, file_id: &str) -> BoxFuture<'_, FileObject>;
116
+
117
+ /// Delete a file.
118
+ fn delete_file(&self, file_id: &str) -> BoxFuture<'_, DeleteResponse>;
119
+
120
+ /// List files, optionally filtered by query parameters.
121
+ fn list_files(&self, query: Option<FileListQuery>) -> BoxFuture<'_, FileListResponse>;
122
+
123
+ /// Retrieve the raw content of a file.
124
+ fn file_content(&self, file_id: &str) -> BoxFuture<'_, bytes::Bytes>;
125
+ }
126
+
127
+ /// Batch processing operations (create, list, retrieve, cancel).
128
+ pub trait BatchClient: Send + Sync {
129
+ /// Create a new batch job.
130
+ fn create_batch(&self, req: CreateBatchRequest) -> BoxFuture<'_, BatchObject>;
131
+
132
+ /// Retrieve a batch by ID.
133
+ fn retrieve_batch(&self, batch_id: &str) -> BoxFuture<'_, BatchObject>;
134
+
135
+ /// List batches, optionally filtered by query parameters.
136
+ fn list_batches(&self, query: Option<BatchListQuery>) -> BoxFuture<'_, BatchListResponse>;
137
+
138
+ /// Cancel an in-progress batch.
139
+ fn cancel_batch(&self, batch_id: &str) -> BoxFuture<'_, BatchObject>;
140
+ }
141
+
142
+ /// Responses API operations (create, retrieve, cancel).
143
+ pub trait ResponseClient: Send + Sync {
144
+ /// Create a new response.
145
+ fn create_response(&self, req: CreateResponseRequest) -> BoxFuture<'_, ResponseObject>;
146
+
147
+ /// Retrieve a response by ID.
148
+ fn retrieve_response(&self, id: &str) -> BoxFuture<'_, ResponseObject>;
149
+
150
+ /// Cancel an in-progress response.
151
+ fn cancel_response(&self, id: &str) -> BoxFuture<'_, ResponseObject>;
152
+ }
153
+
154
+ /// Default client implementation backed by `reqwest`.
155
+ ///
156
+ /// The provider is resolved **once** at construction time. For most
157
+ /// use-cases a single client talks to a single provider, so detecting the
158
+ /// provider per-request is unnecessary overhead and creates subtle bugs (e.g.
159
+ /// the old `list_models` hardcoded `"gpt-4"` as the detection key).
160
+ ///
161
+ /// If you need to talk to multiple providers, create one `DefaultClient` per
162
+ /// provider.
163
+ ///
164
+ /// The provider is stored behind an [`Arc`] so it can be shared cheaply into
165
+ /// async closures and streaming tasks that must be `'static`.
166
+ #[cfg(feature = "native-http")]
167
+ pub struct DefaultClient {
168
+ config: ClientConfig,
169
+ http: reqwest::Client,
170
+ /// Provider resolved at construction; shared via Arc so streaming closures
171
+ /// can capture an owned reference without requiring `unsafe`.
172
+ provider: Arc<dyn Provider>,
173
+ /// Pre-computed auth header `(name, value)` — avoids `format!("Bearer {key}")`
174
+ /// on every request. `None` when the provider requires no authentication.
175
+ cached_auth_header: Option<(String, String)>,
176
+ /// Pre-computed static extra headers — avoids converting `&'static str` pairs
177
+ /// to `(String, String)` on every request.
178
+ cached_extra_headers: Vec<(String, String)>,
179
+ }
180
+
181
+ #[cfg(feature = "native-http")]
182
+ impl DefaultClient {
183
+ /// Build a client.
184
+ ///
185
+ /// `model_hint` guides provider auto-detection when no explicit
186
+ /// `base_url` override is present in the config. For example, passing
187
+ /// `Some("groq/llama3-70b")` selects the Groq provider. Pass `None` to
188
+ /// default to OpenAI.
189
+ ///
190
+ /// # Errors
191
+ ///
192
+ /// Returns a wrapped [`reqwest::Error`] if the underlying HTTP client
193
+ /// cannot be constructed. Header names and values are pre-validated by
194
+ /// [`ClientConfigBuilder::header`], so they are inserted directly here.
195
+ pub fn new(config: ClientConfig, model_hint: Option<&str>) -> Result<Self> {
196
+ let provider = build_provider(&config, model_hint);
197
+ // Validate configuration eagerly so callers get a clear error at
198
+ // construction time rather than on the first request.
199
+ provider.validate()?;
200
+
201
+ // Build the header map from pre-validated headers stored in the config.
202
+ // The builder already validated each header name/value, so these
203
+ // conversions are expected to succeed; return a proper error if they
204
+ // somehow fail rather than panicking.
205
+ let mut header_map = reqwest::header::HeaderMap::new();
206
+ for (k, v) in config.headers() {
207
+ let name =
208
+ reqwest::header::HeaderName::from_bytes(k.as_bytes()).map_err(|_| LiterLlmError::InvalidHeader {
209
+ name: k.clone(),
210
+ reason: "pre-validated header name became invalid".into(),
211
+ })?;
212
+ let val = reqwest::header::HeaderValue::from_str(v).map_err(|_| LiterLlmError::InvalidHeader {
213
+ name: k.clone(),
214
+ reason: "pre-validated header value became invalid".into(),
215
+ })?;
216
+ header_map.insert(name, val);
217
+ }
218
+
219
+ let http = reqwest::Client::builder()
220
+ .timeout(config.timeout)
221
+ .default_headers(header_map)
222
+ .build()
223
+ .map_err(LiterLlmError::from)?;
224
+
225
+ // Pre-compute the auth header once at construction time to avoid
226
+ // `format!("Bearer {key}")` on every request.
227
+ let cached_auth_header = provider
228
+ .auth_header(config.api_key.expose_secret())
229
+ .map(|(name, value)| (name.into_owned(), value.into_owned()));
230
+
231
+ // Pre-compute static extra headers once to avoid `&'static str` ->
232
+ // `String` conversion on every request.
233
+ let cached_extra_headers = provider
234
+ .extra_headers()
235
+ .iter()
236
+ .map(|&(name, value)| (name.to_owned(), value.to_owned()))
237
+ .collect();
238
+
239
+ Ok(Self {
240
+ config,
241
+ http,
242
+ provider,
243
+ cached_auth_header,
244
+ cached_extra_headers,
245
+ })
246
+ }
247
+
248
+ /// Shared helper: build the URL, resolve auth header strings, strip model
249
+ /// prefix from the request body, set the `stream` flag, apply provider
250
+ /// transform, and return everything needed to fire a request.
251
+ ///
252
+ /// `stream` is inserted into the body **before** `transform_request` runs,
253
+ /// so providers can inspect the final body state in one pass.
254
+ ///
255
+ /// Returns `(url, optional_auth_header, body_value)` where the auth header
256
+ /// is `None` when the provider requires no authentication.
257
+ /// Extra headers are accessed directly from `self.cached_extra_headers`.
258
+ fn prepare_request(
259
+ &self,
260
+ serializable: &impl serde::Serialize,
261
+ endpoint_path: &str,
262
+ model: &str,
263
+ stream: Option<bool>,
264
+ ) -> Result<PreparedRequest> {
265
+ if model.is_empty() {
266
+ return Err(LiterLlmError::BadRequest {
267
+ message: "model must not be empty".into(),
268
+ });
269
+ }
270
+
271
+ let bare_model = self.provider.strip_model_prefix(model).to_owned();
272
+ // Use build_url so providers like Azure and Bedrock can embed the model
273
+ // name or deployment identifier into the URL.
274
+ let url = self.provider.build_url(endpoint_path, &bare_model);
275
+ let auth_header = self.cached_auth_header.clone();
276
+
277
+ let mut body = serde_json::to_value(serializable)?;
278
+ if let Some(obj) = body.as_object_mut() {
279
+ obj.insert("model".into(), serde_json::Value::String(bare_model));
280
+ if let Some(s) = stream {
281
+ obj.insert("stream".into(), serde_json::Value::Bool(s));
282
+ }
283
+ }
284
+ self.provider.transform_request(&mut body)?;
285
+
286
+ // Serialize exactly once — the same bytes are used for signing and for
287
+ // the HTTP request body. `Bytes` is reference-counted, so cloning on
288
+ // retry is a zero-copy bump.
289
+ let body_bytes = bytes::Bytes::from(serde_json::to_vec(&body)?);
290
+
291
+ Ok((url, auth_header, body, body_bytes))
292
+ }
293
+
294
+ /// Resolve the auth header for a request.
295
+ ///
296
+ /// When a [`CredentialProvider`] is configured, it is called to obtain a
297
+ /// fresh credential which overrides the pre-computed `cached_auth_header`.
298
+ /// Otherwise the cached header (built at construction from the static
299
+ /// `api_key`) is returned as-is.
300
+ async fn resolve_auth_header(&self) -> Result<Option<(String, String)>> {
301
+ if let Some(ref cp) = self.config.credential_provider {
302
+ let credential = cp.resolve().await?;
303
+ match credential {
304
+ Credential::BearerToken(token) => Ok(Some((
305
+ "Authorization".to_owned(),
306
+ format!("Bearer {}", token.expose_secret()),
307
+ ))),
308
+ Credential::AwsCredentials { .. } => {
309
+ // AWS credentials are handled via signing_headers, not the auth header.
310
+ // Return None so the normal auth header is skipped.
311
+ Ok(None)
312
+ }
313
+ }
314
+ } else {
315
+ Ok(self.cached_auth_header.clone())
316
+ }
317
+ }
318
+
319
+ /// Build the combined header list for a request.
320
+ ///
321
+ /// Merges the provider's pre-computed static [`Provider::extra_headers`], the
322
+ /// dynamic signing headers returned by [`Provider::signing_headers`],
323
+ /// and the per-request [`Provider::dynamic_headers`] computed from the
324
+ /// JSON body. Returns an owned vec of `(name, value)` pairs; callers
325
+ /// borrow these for the HTTP layer.
326
+ fn all_headers(
327
+ &self,
328
+ method: &str,
329
+ url: &str,
330
+ body_json: &serde_json::Value,
331
+ body_bytes: &[u8],
332
+ ) -> Vec<(String, String)> {
333
+ // Start with dynamic signing headers (e.g. SigV4 Authorization + x-amz-date).
334
+ let mut headers = self.provider.signing_headers(method, url, body_bytes);
335
+ // Append pre-computed static provider extra headers (e.g. anthropic-version).
336
+ headers.extend(self.cached_extra_headers.iter().cloned());
337
+ // Append per-request dynamic headers (e.g. anthropic-beta).
338
+ headers.extend(self.provider.dynamic_headers(body_json));
339
+ headers
340
+ }
341
+ }
342
+
343
+ #[cfg(feature = "native-http")]
344
+ /// Resolve the provider to use for all requests on this client.
345
+ ///
346
+ /// Priority:
347
+ /// 1. Explicit `base_url` in config -> custom OpenAI-compatible provider.
348
+ /// 2. `model_hint` -> auto-detect by model name prefix.
349
+ /// 3. Default -> OpenAI.
350
+ fn build_provider(config: &ClientConfig, model_hint: Option<&str>) -> Arc<dyn Provider> {
351
+ if let Some(ref base_url) = config.base_url {
352
+ return Arc::new(OpenAiCompatibleProvider {
353
+ name: "custom".into(),
354
+ base_url: base_url.clone(),
355
+ env_var: None,
356
+ model_prefixes: vec![],
357
+ });
358
+ }
359
+
360
+ if let Some(model) = model_hint
361
+ && let Some(p) = provider::detect_provider(model)
362
+ {
363
+ // detect_provider returns Box<dyn Provider>; convert to Arc.
364
+ return Arc::from(p);
365
+ }
366
+
367
+ Arc::new(OpenAiProvider)
368
+ }
369
+
370
+ #[cfg(feature = "native-http")]
371
+ impl LlmClient for DefaultClient {
372
+ fn chat(&self, req: ChatCompletionRequest) -> BoxFuture<'_, ChatCompletionResponse> {
373
+ Box::pin(async move {
374
+ // Pass stream=false so providers can inspect the flag in transform_request.
375
+ let (url, _cached_auth, body_json, body_bytes) =
376
+ self.prepare_request(&req, self.provider.chat_completions_path(), &req.model, Some(false))?;
377
+
378
+ let auth_header = self.resolve_auth_header().await?;
379
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
380
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
381
+
382
+ let auth = auth_header.as_ref().map(str_pair);
383
+ let mut raw =
384
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
385
+ .await?;
386
+ self.provider.transform_response(&mut raw)?;
387
+ serde_json::from_value::<ChatCompletionResponse>(raw).map_err(LiterLlmError::from)
388
+ })
389
+ }
390
+
391
+ fn chat_stream(&self, req: ChatCompletionRequest) -> BoxFuture<'_, BoxStream<'_, ChatCompletionChunk>> {
392
+ Box::pin(async move {
393
+ // Use prepare_request for validation, model-prefix stripping, and
394
+ // transform_request — then override the URL via build_stream_url.
395
+ let (_base_url, _cached_auth, body_json, body_bytes) =
396
+ self.prepare_request(&req, self.provider.chat_completions_path(), &req.model, Some(true))?;
397
+
398
+ // Always use build_stream_url for the streaming endpoint.
399
+ // The default implementation delegates to build_url, so this is safe
400
+ // for all providers. Providers with a distinct streaming endpoint
401
+ // (e.g. Bedrock /converse-stream) override build_stream_url.
402
+ let bare_model = self.provider.strip_model_prefix(&req.model);
403
+ let url = self
404
+ .provider
405
+ .build_stream_url(self.provider.chat_completions_path(), bare_model);
406
+
407
+ let auth_header = self.resolve_auth_header().await?;
408
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
409
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
410
+ let auth = auth_header.as_ref().map(str_pair);
411
+
412
+ match self.provider.stream_format() {
413
+ provider::StreamFormat::Sse => {
414
+ let provider = Arc::clone(&self.provider);
415
+ let parse_event = move |data: &str| provider.parse_stream_event(data);
416
+ let stream = http::streaming::post_stream(
417
+ &self.http,
418
+ &url,
419
+ auth,
420
+ &extra,
421
+ body_bytes,
422
+ self.config.max_retries,
423
+ parse_event,
424
+ )
425
+ .await?;
426
+ Ok(stream)
427
+ }
428
+ provider::StreamFormat::AwsEventStream => {
429
+ let stream = http::eventstream::post_eventstream(
430
+ &self.http,
431
+ &url,
432
+ auth,
433
+ &extra,
434
+ body_bytes,
435
+ self.config.max_retries,
436
+ provider::bedrock::parse_bedrock_stream_event,
437
+ )
438
+ .await?;
439
+ Ok(stream)
440
+ }
441
+ }
442
+ })
443
+ }
444
+
445
+ fn embed(&self, req: EmbeddingRequest) -> BoxFuture<'_, EmbeddingResponse> {
446
+ Box::pin(async move {
447
+ // Embeddings have no stream flag; pass None so it is not inserted.
448
+ let (url, _cached_auth, body_json, body_bytes) =
449
+ self.prepare_request(&req, self.provider.embeddings_path(), &req.model, None)?;
450
+
451
+ let auth_header = self.resolve_auth_header().await?;
452
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
453
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
454
+
455
+ let auth = auth_header.as_ref().map(str_pair);
456
+ let mut raw =
457
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
458
+ .await?;
459
+ self.provider.transform_response(&mut raw)?;
460
+ serde_json::from_value::<EmbeddingResponse>(raw).map_err(LiterLlmError::from)
461
+ })
462
+ }
463
+
464
+ fn list_models(&self) -> BoxFuture<'_, ModelsListResponse> {
465
+ Box::pin(async move {
466
+ // Use build_url so providers like Azure/Bedrock can customise the URL.
467
+ let url = self.provider.build_url(self.provider.models_path(), "");
468
+ let auth_header = self.resolve_auth_header().await?;
469
+ let auth = auth_header.as_ref().map(str_pair);
470
+ // list_models is a GET request; signing headers use an empty body,
471
+ // and dynamic_headers receives a null JSON value.
472
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
473
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
474
+
475
+ http::request::get_json(&self.http, &url, auth, &extra, self.config.max_retries).await
476
+ })
477
+ }
478
+
479
+ fn image_generate(&self, req: CreateImageRequest) -> BoxFuture<'_, ImagesResponse> {
480
+ Box::pin(async move {
481
+ let model = req.model.as_deref().unwrap_or_default();
482
+ let (url, _cached_auth, body_json, body_bytes) =
483
+ self.prepare_request(&req, self.provider.image_generations_path(), model, None)?;
484
+
485
+ let auth_header = self.resolve_auth_header().await?;
486
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
487
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
488
+
489
+ let auth = auth_header.as_ref().map(str_pair);
490
+ let mut raw =
491
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
492
+ .await?;
493
+ self.provider.transform_response(&mut raw)?;
494
+ serde_json::from_value::<ImagesResponse>(raw).map_err(LiterLlmError::from)
495
+ })
496
+ }
497
+
498
+ fn speech(&self, req: CreateSpeechRequest) -> BoxFuture<'_, bytes::Bytes> {
499
+ Box::pin(async move {
500
+ let (url, _cached_auth, body_json, body_bytes) =
501
+ self.prepare_request(&req, self.provider.audio_speech_path(), &req.model, None)?;
502
+
503
+ let auth_header = self.resolve_auth_header().await?;
504
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
505
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
506
+
507
+ let auth = auth_header.as_ref().map(str_pair);
508
+ http::request::post_binary(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries).await
509
+ })
510
+ }
511
+
512
+ fn transcribe(&self, req: CreateTranscriptionRequest) -> BoxFuture<'_, TranscriptionResponse> {
513
+ Box::pin(async move {
514
+ let (url, _cached_auth, body_json, body_bytes) =
515
+ self.prepare_request(&req, self.provider.audio_transcriptions_path(), &req.model, None)?;
516
+
517
+ let auth_header = self.resolve_auth_header().await?;
518
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
519
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
520
+
521
+ let auth = auth_header.as_ref().map(str_pair);
522
+ let mut raw =
523
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
524
+ .await?;
525
+ self.provider.transform_response(&mut raw)?;
526
+ serde_json::from_value::<TranscriptionResponse>(raw).map_err(LiterLlmError::from)
527
+ })
528
+ }
529
+
530
+ fn moderate(&self, req: ModerationRequest) -> BoxFuture<'_, ModerationResponse> {
531
+ Box::pin(async move {
532
+ let model = req.model.as_deref().unwrap_or_default();
533
+ let (url, _cached_auth, body_json, body_bytes) =
534
+ self.prepare_request(&req, self.provider.moderations_path(), model, None)?;
535
+
536
+ let auth_header = self.resolve_auth_header().await?;
537
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
538
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
539
+
540
+ let auth = auth_header.as_ref().map(str_pair);
541
+ let mut raw =
542
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
543
+ .await?;
544
+ self.provider.transform_response(&mut raw)?;
545
+ serde_json::from_value::<ModerationResponse>(raw).map_err(LiterLlmError::from)
546
+ })
547
+ }
548
+
549
+ fn rerank(&self, req: RerankRequest) -> BoxFuture<'_, RerankResponse> {
550
+ Box::pin(async move {
551
+ let (url, _cached_auth, body_json, body_bytes) =
552
+ self.prepare_request(&req, self.provider.rerank_path(), &req.model, None)?;
553
+
554
+ let auth_header = self.resolve_auth_header().await?;
555
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
556
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
557
+
558
+ let auth = auth_header.as_ref().map(str_pair);
559
+ let mut raw =
560
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
561
+ .await?;
562
+ self.provider.transform_response(&mut raw)?;
563
+ serde_json::from_value::<RerankResponse>(raw).map_err(LiterLlmError::from)
564
+ })
565
+ }
566
+
567
+ fn search(&self, req: SearchRequest) -> BoxFuture<'_, SearchResponse> {
568
+ Box::pin(async move {
569
+ let (url, _cached_auth, body_json, body_bytes) =
570
+ self.prepare_request(&req, self.provider.search_path(), &req.model, None)?;
571
+
572
+ let auth_header = self.resolve_auth_header().await?;
573
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
574
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
575
+
576
+ let auth = auth_header.as_ref().map(str_pair);
577
+ let mut raw =
578
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
579
+ .await?;
580
+ self.provider.transform_response(&mut raw)?;
581
+ serde_json::from_value::<SearchResponse>(raw).map_err(LiterLlmError::from)
582
+ })
583
+ }
584
+
585
+ fn ocr(&self, req: OcrRequest) -> BoxFuture<'_, OcrResponse> {
586
+ Box::pin(async move {
587
+ let (url, _cached_auth, body_json, body_bytes) =
588
+ self.prepare_request(&req, self.provider.ocr_path(), &req.model, None)?;
589
+
590
+ let auth_header = self.resolve_auth_header().await?;
591
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
592
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
593
+
594
+ let auth = auth_header.as_ref().map(str_pair);
595
+ let mut raw =
596
+ http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
597
+ .await?;
598
+ self.provider.transform_response(&mut raw)?;
599
+ serde_json::from_value::<OcrResponse>(raw).map_err(LiterLlmError::from)
600
+ })
601
+ }
602
+ }
603
+
604
+ #[cfg(feature = "native-http")]
605
+ impl FileClient for DefaultClient {
606
+ fn create_file(&self, req: CreateFileRequest) -> BoxFuture<'_, FileObject> {
607
+ Box::pin(async move {
608
+ let url = self.provider.build_url(self.provider.files_path(), "");
609
+ let auth_header = self.resolve_auth_header().await?;
610
+ let auth = auth_header.as_ref().map(str_pair);
611
+ let all_headers = self.all_headers("POST", &url, &serde_json::Value::Null, &[]);
612
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
613
+
614
+ // Decode the base64-encoded file data into raw bytes for the multipart upload.
615
+ use base64::Engine;
616
+ let file_bytes = base64::engine::general_purpose::STANDARD
617
+ .decode(&req.file)
618
+ .map_err(|e| LiterLlmError::BadRequest {
619
+ message: format!("invalid base64 file data: {e}"),
620
+ })?;
621
+
622
+ let filename = req.filename.unwrap_or_else(|| "upload".to_owned());
623
+ let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(filename);
624
+ let purpose_str = serde_json::to_value(&req.purpose)?
625
+ .as_str()
626
+ .unwrap_or_default()
627
+ .to_owned();
628
+ let form = reqwest::multipart::Form::new()
629
+ .part("file", file_part)
630
+ .text("purpose", purpose_str);
631
+
632
+ let raw = http::request::post_multipart(&self.http, &url, auth, &extra, form).await?;
633
+ serde_json::from_value::<FileObject>(raw).map_err(LiterLlmError::from)
634
+ })
635
+ }
636
+
637
+ fn retrieve_file(&self, file_id: &str) -> BoxFuture<'_, FileObject> {
638
+ let file_id = file_id.to_owned();
639
+ Box::pin(async move {
640
+ let url = format!(
641
+ "{}/{}",
642
+ self.provider.build_url(self.provider.files_path(), ""),
643
+ file_id
644
+ );
645
+ let auth_header = self.resolve_auth_header().await?;
646
+ let auth = auth_header.as_ref().map(str_pair);
647
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
648
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
649
+
650
+ let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
651
+ serde_json::from_value::<FileObject>(raw).map_err(LiterLlmError::from)
652
+ })
653
+ }
654
+
655
+ fn delete_file(&self, file_id: &str) -> BoxFuture<'_, DeleteResponse> {
656
+ let file_id = file_id.to_owned();
657
+ Box::pin(async move {
658
+ let url = format!(
659
+ "{}/{}",
660
+ self.provider.build_url(self.provider.files_path(), ""),
661
+ file_id
662
+ );
663
+ let auth_header = self.resolve_auth_header().await?;
664
+ let auth = auth_header.as_ref().map(str_pair);
665
+ let all_headers = self.all_headers("DELETE", &url, &serde_json::Value::Null, &[]);
666
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
667
+
668
+ let raw = http::request::delete_json(&self.http, &url, auth, &extra, self.config.max_retries).await?;
669
+ serde_json::from_value::<DeleteResponse>(raw).map_err(LiterLlmError::from)
670
+ })
671
+ }
672
+
673
+ fn list_files(&self, query: Option<FileListQuery>) -> BoxFuture<'_, FileListResponse> {
674
+ Box::pin(async move {
675
+ let base_url = self.provider.build_url(self.provider.files_path(), "");
676
+ let url = if let Some(ref q) = query {
677
+ let mut params = Vec::new();
678
+ if let Some(ref purpose) = q.purpose {
679
+ params.push(format!("purpose={purpose}"));
680
+ }
681
+ if let Some(limit) = q.limit {
682
+ params.push(format!("limit={limit}"));
683
+ }
684
+ if let Some(ref after) = q.after {
685
+ params.push(format!("after={after}"));
686
+ }
687
+ if params.is_empty() {
688
+ base_url
689
+ } else {
690
+ format!("{base_url}?{}", params.join("&"))
691
+ }
692
+ } else {
693
+ base_url
694
+ };
695
+ let auth_header = self.resolve_auth_header().await?;
696
+ let auth = auth_header.as_ref().map(str_pair);
697
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
698
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
699
+
700
+ let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
701
+ serde_json::from_value::<FileListResponse>(raw).map_err(LiterLlmError::from)
702
+ })
703
+ }
704
+
705
+ fn file_content(&self, file_id: &str) -> BoxFuture<'_, bytes::Bytes> {
706
+ let file_id = file_id.to_owned();
707
+ Box::pin(async move {
708
+ let url = format!(
709
+ "{}/{}/content",
710
+ self.provider.build_url(self.provider.files_path(), ""),
711
+ file_id
712
+ );
713
+ let auth_header = self.resolve_auth_header().await?;
714
+ let auth = auth_header.as_ref().map(str_pair);
715
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
716
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
717
+
718
+ http::request::get_binary(&self.http, &url, auth, &extra, self.config.max_retries).await
719
+ })
720
+ }
721
+ }
722
+
723
+ #[cfg(feature = "native-http")]
724
+ impl BatchClient for DefaultClient {
725
+ fn create_batch(&self, req: CreateBatchRequest) -> BoxFuture<'_, BatchObject> {
726
+ Box::pin(async move {
727
+ let url = self.provider.build_url(self.provider.batches_path(), "");
728
+ let body_bytes = bytes::Bytes::from(serde_json::to_vec(&req)?);
729
+ let body_json = serde_json::to_value(&req)?;
730
+
731
+ let auth_header = self.resolve_auth_header().await?;
732
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
733
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
734
+ let auth = auth_header.as_ref().map(str_pair);
735
+
736
+ let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
737
+ .await?;
738
+ serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
739
+ })
740
+ }
741
+
742
+ fn retrieve_batch(&self, batch_id: &str) -> BoxFuture<'_, BatchObject> {
743
+ let batch_id = batch_id.to_owned();
744
+ Box::pin(async move {
745
+ let url = format!(
746
+ "{}/{}",
747
+ self.provider.build_url(self.provider.batches_path(), ""),
748
+ batch_id
749
+ );
750
+ let auth_header = self.resolve_auth_header().await?;
751
+ let auth = auth_header.as_ref().map(str_pair);
752
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
753
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
754
+
755
+ let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
756
+ serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
757
+ })
758
+ }
759
+
760
+ fn list_batches(&self, query: Option<BatchListQuery>) -> BoxFuture<'_, BatchListResponse> {
761
+ Box::pin(async move {
762
+ let base_url = self.provider.build_url(self.provider.batches_path(), "");
763
+ let url = if let Some(ref q) = query {
764
+ let mut params = Vec::new();
765
+ if let Some(limit) = q.limit {
766
+ params.push(format!("limit={limit}"));
767
+ }
768
+ if let Some(ref after) = q.after {
769
+ params.push(format!("after={after}"));
770
+ }
771
+ if params.is_empty() {
772
+ base_url
773
+ } else {
774
+ format!("{base_url}?{}", params.join("&"))
775
+ }
776
+ } else {
777
+ base_url
778
+ };
779
+ let auth_header = self.resolve_auth_header().await?;
780
+ let auth = auth_header.as_ref().map(str_pair);
781
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
782
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
783
+
784
+ let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
785
+ serde_json::from_value::<BatchListResponse>(raw).map_err(LiterLlmError::from)
786
+ })
787
+ }
788
+
789
+ fn cancel_batch(&self, batch_id: &str) -> BoxFuture<'_, BatchObject> {
790
+ let batch_id = batch_id.to_owned();
791
+ Box::pin(async move {
792
+ let url = format!(
793
+ "{}/{}/cancel",
794
+ self.provider.build_url(self.provider.batches_path(), ""),
795
+ batch_id
796
+ );
797
+ let auth_header = self.resolve_auth_header().await?;
798
+ let body_json = serde_json::Value::Null;
799
+ let body_bytes = bytes::Bytes::new();
800
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
801
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
802
+ let auth = auth_header.as_ref().map(str_pair);
803
+
804
+ let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
805
+ .await?;
806
+ serde_json::from_value::<BatchObject>(raw).map_err(LiterLlmError::from)
807
+ })
808
+ }
809
+ }
810
+
811
+ #[cfg(feature = "native-http")]
812
+ impl ResponseClient for DefaultClient {
813
+ fn create_response(&self, req: CreateResponseRequest) -> BoxFuture<'_, ResponseObject> {
814
+ Box::pin(async move {
815
+ let url = self.provider.build_url(self.provider.responses_path(), "");
816
+ let body_bytes = bytes::Bytes::from(serde_json::to_vec(&req)?);
817
+ let body_json = serde_json::to_value(&req)?;
818
+
819
+ let auth_header = self.resolve_auth_header().await?;
820
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
821
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
822
+ let auth = auth_header.as_ref().map(str_pair);
823
+
824
+ let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
825
+ .await?;
826
+ serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
827
+ })
828
+ }
829
+
830
+ fn retrieve_response(&self, id: &str) -> BoxFuture<'_, ResponseObject> {
831
+ let id = id.to_owned();
832
+ Box::pin(async move {
833
+ let url = format!("{}/{}", self.provider.build_url(self.provider.responses_path(), ""), id);
834
+ let auth_header = self.resolve_auth_header().await?;
835
+ let auth = auth_header.as_ref().map(str_pair);
836
+ let all_headers = self.all_headers("GET", &url, &serde_json::Value::Null, &[]);
837
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
838
+
839
+ let raw = http::request::get_json_raw(&self.http, &url, auth, &extra, self.config.max_retries).await?;
840
+ serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
841
+ })
842
+ }
843
+
844
+ fn cancel_response(&self, id: &str) -> BoxFuture<'_, ResponseObject> {
845
+ let id = id.to_owned();
846
+ Box::pin(async move {
847
+ let url = format!(
848
+ "{}/{}/cancel",
849
+ self.provider.build_url(self.provider.responses_path(), ""),
850
+ id
851
+ );
852
+ let auth_header = self.resolve_auth_header().await?;
853
+ let body_json = serde_json::Value::Null;
854
+ let body_bytes = bytes::Bytes::new();
855
+ let all_headers = self.all_headers("POST", &url, &body_json, &body_bytes);
856
+ let extra: Vec<(&str, &str)> = all_headers.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
857
+ let auth = auth_header.as_ref().map(str_pair);
858
+
859
+ let raw = http::request::post_json_raw(&self.http, &url, auth, &extra, body_bytes, self.config.max_retries)
860
+ .await?;
861
+ serde_json::from_value::<ResponseObject>(raw).map_err(LiterLlmError::from)
862
+ })
863
+ }
864
+ }