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.
- checksums.yaml +7 -0
- data/README.md +239 -0
- data/ext/liter_llm_rb/extconf.rb +65 -0
- data/ext/liter_llm_rb/native/.cargo/config.toml +23 -0
- data/ext/liter_llm_rb/native/Cargo.lock +3713 -0
- data/ext/liter_llm_rb/native/Cargo.toml +32 -0
- data/ext/liter_llm_rb/native/build.rs +15 -0
- data/ext/liter_llm_rb/native/src/lib.rs +1079 -0
- data/lib/liter_llm.rb +8 -0
- data/sig/liter_llm.rbs +416 -0
- data/vendor/Cargo.toml +54 -0
- data/vendor/liter-llm/Cargo.toml +92 -0
- data/vendor/liter-llm/README.md +252 -0
- data/vendor/liter-llm/schemas/pricing.json +40 -0
- data/vendor/liter-llm/schemas/providers.json +1662 -0
- data/vendor/liter-llm/src/auth/azure_ad.rs +264 -0
- data/vendor/liter-llm/src/auth/bedrock_sts.rs +353 -0
- data/vendor/liter-llm/src/auth/mod.rs +68 -0
- data/vendor/liter-llm/src/auth/vertex_oauth.rs +353 -0
- data/vendor/liter-llm/src/client/config.rs +351 -0
- data/vendor/liter-llm/src/client/managed.rs +622 -0
- data/vendor/liter-llm/src/client/mod.rs +864 -0
- data/vendor/liter-llm/src/cost.rs +212 -0
- data/vendor/liter-llm/src/error.rs +190 -0
- data/vendor/liter-llm/src/http/eventstream.rs +860 -0
- data/vendor/liter-llm/src/http/mod.rs +12 -0
- data/vendor/liter-llm/src/http/request.rs +438 -0
- data/vendor/liter-llm/src/http/retry.rs +72 -0
- data/vendor/liter-llm/src/http/streaming.rs +289 -0
- data/vendor/liter-llm/src/lib.rs +37 -0
- data/vendor/liter-llm/src/provider/anthropic.rs +2250 -0
- data/vendor/liter-llm/src/provider/azure.rs +579 -0
- data/vendor/liter-llm/src/provider/bedrock.rs +1543 -0
- data/vendor/liter-llm/src/provider/cohere.rs +654 -0
- data/vendor/liter-llm/src/provider/custom.rs +404 -0
- data/vendor/liter-llm/src/provider/google_ai.rs +281 -0
- data/vendor/liter-llm/src/provider/mistral.rs +188 -0
- data/vendor/liter-llm/src/provider/mod.rs +616 -0
- data/vendor/liter-llm/src/provider/vertex.rs +1504 -0
- data/vendor/liter-llm/src/tests.rs +1425 -0
- data/vendor/liter-llm/src/tokenizer.rs +281 -0
- data/vendor/liter-llm/src/tower/budget.rs +599 -0
- data/vendor/liter-llm/src/tower/cache.rs +502 -0
- data/vendor/liter-llm/src/tower/cache_opendal.rs +270 -0
- data/vendor/liter-llm/src/tower/cooldown.rs +231 -0
- data/vendor/liter-llm/src/tower/cost.rs +404 -0
- data/vendor/liter-llm/src/tower/fallback.rs +121 -0
- data/vendor/liter-llm/src/tower/health.rs +219 -0
- data/vendor/liter-llm/src/tower/hooks.rs +369 -0
- data/vendor/liter-llm/src/tower/mod.rs +77 -0
- data/vendor/liter-llm/src/tower/rate_limit.rs +300 -0
- data/vendor/liter-llm/src/tower/router.rs +436 -0
- data/vendor/liter-llm/src/tower/service.rs +181 -0
- data/vendor/liter-llm/src/tower/tests.rs +539 -0
- data/vendor/liter-llm/src/tower/tests_common.rs +252 -0
- data/vendor/liter-llm/src/tower/tracing.rs +209 -0
- data/vendor/liter-llm/src/tower/types.rs +170 -0
- data/vendor/liter-llm/src/types/audio.rs +52 -0
- data/vendor/liter-llm/src/types/batch.rs +77 -0
- data/vendor/liter-llm/src/types/chat.rs +214 -0
- data/vendor/liter-llm/src/types/common.rs +244 -0
- data/vendor/liter-llm/src/types/embedding.rs +84 -0
- data/vendor/liter-llm/src/types/files.rs +58 -0
- data/vendor/liter-llm/src/types/image.rs +40 -0
- data/vendor/liter-llm/src/types/mod.rs +27 -0
- data/vendor/liter-llm/src/types/models.rs +21 -0
- data/vendor/liter-llm/src/types/moderation.rs +80 -0
- data/vendor/liter-llm/src/types/ocr.rs +87 -0
- data/vendor/liter-llm/src/types/rerank.rs +46 -0
- data/vendor/liter-llm/src/types/responses.rs +55 -0
- data/vendor/liter-llm/src/types/search.rs +45 -0
- data/vendor/liter-llm/tests/contract.rs +332 -0
- data/vendor/liter-llm-ffi/Cargo.toml +30 -0
- data/vendor/liter-llm-ffi/build.rs +66 -0
- data/vendor/liter-llm-ffi/cbindgen.toml +60 -0
- data/vendor/liter-llm-ffi/liter_llm.h +850 -0
- data/vendor/liter-llm-ffi/src/lib.rs +2488 -0
- 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
|
+
}
|