liter_llm 1.0.0.pre.rc.8 → 1.0.0.pre.rc.9
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 +4 -4
- data/ext/liter_llm_rb/native/Cargo.toml +1 -1
- data/vendor/Cargo.toml +1 -1
- data/vendor/liter-llm/Cargo.toml +3 -1
- data/vendor/liter-llm/src/client/config.rs +1 -1
- data/vendor/liter-llm/src/client/config_file.rs +328 -0
- data/vendor/liter-llm/src/client/mod.rs +317 -130
- data/vendor/liter-llm/src/http/eventstream.rs +19 -19
- data/vendor/liter-llm/src/http/request.rs +1 -0
- data/vendor/liter-llm/src/lib.rs +2 -1
- data/vendor/liter-llm/src/provider/anthropic.rs +5 -2
- data/vendor/liter-llm/src/provider/vertex.rs +119 -0
- data/vendor/liter-llm/src/types/common.rs +7 -1
- data/vendor/liter-llm/tests/config_file.rs +408 -0
- data/vendor/liter-llm/tests/fixtures/config/budget_soft.toml +6 -0
- data/vendor/liter-llm/tests/fixtures/config/cache_only.toml +4 -0
- data/vendor/liter-llm/tests/fixtures/config/cache_opendal.toml +8 -0
- data/vendor/liter-llm/tests/fixtures/config/full.toml +42 -0
- data/vendor/liter-llm/tests/fixtures/config/invalid_nested_unknown.toml +3 -0
- data/vendor/liter-llm/tests/fixtures/config/invalid_unknown_field.toml +2 -0
- data/vendor/liter-llm/tests/fixtures/config/invalid_wrong_type.toml +1 -0
- data/vendor/liter-llm/tests/fixtures/config/minimal.toml +1 -0
- data/vendor/liter-llm/tests/fixtures/config/providers_only.toml +10 -0
- data/vendor/liter-llm/tests/fixtures/config/rate_limit_only.toml +3 -0
- data/vendor/liter-llm/tests/live_providers/anthropic.rs +278 -0
- data/vendor/liter-llm/tests/live_providers/azure.rs +88 -0
- data/vendor/liter-llm/tests/live_providers/bedrock.rs +60 -0
- data/vendor/liter-llm/tests/live_providers/cross_provider.rs +102 -0
- data/vendor/liter-llm/tests/live_providers/google_ai.rs +107 -0
- data/vendor/liter-llm/tests/live_providers/mistral.rs +96 -0
- data/vendor/liter-llm/tests/live_providers/openai.rs +109 -0
- data/vendor/liter-llm/tests/live_providers/vertex_ai.rs +96 -0
- data/vendor/liter-llm/tests/live_providers.rs +159 -0
- data/vendor/liter-llm-ffi/Cargo.toml +3 -3
- data/vendor/liter-llm-ffi/liter_llm.h +1 -1
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99f9baa37507b2b56d9d5300e3a53ede447fbfe898e57a93f757a0d36b6aa20d
|
|
4
|
+
data.tar.gz: 9fdadec249c006f4b368c545f879f5512b60759bc19d74206ab945017efb4826
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cf1243668cc3e7852198f92da30a92991072cca691c09b91600f8ab874d32d55e55f6e2156f785f6ddf2e8bf2d2d5a5f17c8d931e4c226c98740463153659a7
|
|
7
|
+
data.tar.gz: b6eb0d41eb748f0f6c2d4eee706cb23c04da9370681f7532335b99a6fea2dd2035421d44048ae79c7adfece41973dcf4883d9ea931cf31583e96ac016f8e899f
|
data/vendor/Cargo.toml
CHANGED
data/vendor/liter-llm/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "liter-llm"
|
|
3
|
-
version = "1.0.0-rc.
|
|
3
|
+
version = "1.0.0-rc.9"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
repository.workspace = true
|
|
@@ -81,12 +81,14 @@ serde_json = "1"
|
|
|
81
81
|
thiserror = "2"
|
|
82
82
|
tokenizers = { version = "0.22", features = ["http", "fancy-regex"], default-features = false, optional = true }
|
|
83
83
|
tokio = { version = "1", features = ["time", "rt", "macros"], optional = true }
|
|
84
|
+
toml = "1.1"
|
|
84
85
|
tower = { version = "0.5", features = ["retry", "limit", "timeout", "buffer", "load-shed", "steer", "util"], optional = true }
|
|
85
86
|
tower-http = { version = "0.6", features = ["follow-redirect", "set-header", "sensitive-headers", "trace", "request-id"], optional = true }
|
|
86
87
|
tracing = { version = "0.1", optional = true }
|
|
87
88
|
tracing-opentelemetry = { version = "0.32", optional = true }
|
|
88
89
|
|
|
89
90
|
[dev-dependencies]
|
|
91
|
+
futures-util = "0.3"
|
|
90
92
|
jsonschema = "0.45"
|
|
91
93
|
serial_test = "3"
|
|
92
94
|
tokio = { version = "1", features = ["test-util", "macros"] }
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//! TOML-based configuration file loading.
|
|
2
|
+
//!
|
|
3
|
+
//! Load client configuration from `liter-llm.toml` files with auto-discovery
|
|
4
|
+
//! (searches current directory and parents).
|
|
5
|
+
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::path::Path;
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
|
|
10
|
+
use serde::Deserialize;
|
|
11
|
+
|
|
12
|
+
use crate::error::{LiterLlmError, Result};
|
|
13
|
+
|
|
14
|
+
/// TOML file representation of client configuration.
|
|
15
|
+
///
|
|
16
|
+
/// All fields are optional — missing fields use defaults from [`ClientConfigBuilder`].
|
|
17
|
+
/// Convert to a builder via [`FileConfig::into_builder`].
|
|
18
|
+
///
|
|
19
|
+
/// # Example `liter-llm.toml`
|
|
20
|
+
///
|
|
21
|
+
/// ```toml
|
|
22
|
+
/// api_key = "sk-..."
|
|
23
|
+
/// base_url = "https://api.openai.com/v1"
|
|
24
|
+
/// timeout_secs = 120
|
|
25
|
+
/// max_retries = 5
|
|
26
|
+
///
|
|
27
|
+
/// [cache]
|
|
28
|
+
/// max_entries = 512
|
|
29
|
+
/// ttl_seconds = 600
|
|
30
|
+
/// backend = "memory"
|
|
31
|
+
///
|
|
32
|
+
/// [budget]
|
|
33
|
+
/// global_limit = 50.0
|
|
34
|
+
/// enforcement = "hard"
|
|
35
|
+
///
|
|
36
|
+
/// [[providers]]
|
|
37
|
+
/// name = "my-provider"
|
|
38
|
+
/// base_url = "https://my-llm.example.com/v1"
|
|
39
|
+
/// model_prefixes = ["my-provider/"]
|
|
40
|
+
/// ```
|
|
41
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
42
|
+
#[serde(deny_unknown_fields)]
|
|
43
|
+
pub struct FileConfig {
|
|
44
|
+
pub api_key: Option<String>,
|
|
45
|
+
pub base_url: Option<String>,
|
|
46
|
+
pub model_hint: Option<String>,
|
|
47
|
+
pub timeout_secs: Option<u64>,
|
|
48
|
+
pub max_retries: Option<u32>,
|
|
49
|
+
pub extra_headers: Option<HashMap<String, String>>,
|
|
50
|
+
pub cache: Option<FileCacheConfig>,
|
|
51
|
+
pub budget: Option<FileBudgetConfig>,
|
|
52
|
+
pub cooldown_secs: Option<u64>,
|
|
53
|
+
pub rate_limit: Option<FileRateLimitConfig>,
|
|
54
|
+
pub health_check_secs: Option<u64>,
|
|
55
|
+
pub cost_tracking: Option<bool>,
|
|
56
|
+
pub tracing: Option<bool>,
|
|
57
|
+
pub providers: Option<Vec<FileProviderConfig>>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
61
|
+
#[serde(deny_unknown_fields)]
|
|
62
|
+
pub struct FileCacheConfig {
|
|
63
|
+
pub max_entries: Option<usize>,
|
|
64
|
+
pub ttl_seconds: Option<u64>,
|
|
65
|
+
pub backend: Option<String>,
|
|
66
|
+
pub backend_config: Option<HashMap<String, String>>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
70
|
+
#[serde(deny_unknown_fields)]
|
|
71
|
+
pub struct FileBudgetConfig {
|
|
72
|
+
pub global_limit: Option<f64>,
|
|
73
|
+
pub model_limits: Option<HashMap<String, f64>>,
|
|
74
|
+
pub enforcement: Option<String>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
78
|
+
#[serde(deny_unknown_fields)]
|
|
79
|
+
pub struct FileRateLimitConfig {
|
|
80
|
+
pub rpm: Option<u32>,
|
|
81
|
+
pub tpm: Option<u64>,
|
|
82
|
+
pub window_seconds: Option<u64>,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
86
|
+
#[serde(deny_unknown_fields)]
|
|
87
|
+
pub struct FileProviderConfig {
|
|
88
|
+
pub name: String,
|
|
89
|
+
pub base_url: String,
|
|
90
|
+
pub auth_header: Option<String>,
|
|
91
|
+
pub model_prefixes: Vec<String>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl FileConfig {
|
|
95
|
+
/// Load from a TOML file path.
|
|
96
|
+
pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self> {
|
|
97
|
+
let path = path.as_ref();
|
|
98
|
+
let content = std::fs::read_to_string(path).map_err(|e| LiterLlmError::InternalError {
|
|
99
|
+
message: format!("failed to read config file {}: {e}", path.display()),
|
|
100
|
+
})?;
|
|
101
|
+
Self::from_toml_str(&content)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Parse from a TOML string.
|
|
105
|
+
pub fn from_toml_str(s: &str) -> Result<Self> {
|
|
106
|
+
toml::from_str(s).map_err(|e| LiterLlmError::InternalError {
|
|
107
|
+
message: format!("invalid TOML config: {e}"),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Discover `liter-llm.toml` by walking from current directory to filesystem root.
|
|
112
|
+
///
|
|
113
|
+
/// Returns `Ok(None)` if no config file is found.
|
|
114
|
+
pub fn discover() -> Result<Option<Self>> {
|
|
115
|
+
let mut current = std::env::current_dir().map_err(|e| LiterLlmError::InternalError {
|
|
116
|
+
message: format!("failed to get current directory: {e}"),
|
|
117
|
+
})?;
|
|
118
|
+
loop {
|
|
119
|
+
let config_path = current.join("liter-llm.toml");
|
|
120
|
+
if config_path.exists() {
|
|
121
|
+
return Ok(Some(Self::from_toml_file(config_path)?));
|
|
122
|
+
}
|
|
123
|
+
match current.parent() {
|
|
124
|
+
Some(parent) => current = parent.to_path_buf(),
|
|
125
|
+
None => break,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
Ok(None)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Convert into a [`ClientConfigBuilder`](super::ClientConfigBuilder),
|
|
132
|
+
/// applying all fields that are set.
|
|
133
|
+
///
|
|
134
|
+
/// Fields not present in the TOML file use the builder's defaults.
|
|
135
|
+
pub fn into_builder(self) -> super::ClientConfigBuilder {
|
|
136
|
+
let api_key = self.api_key.unwrap_or_default();
|
|
137
|
+
let mut builder = super::ClientConfigBuilder::new(api_key);
|
|
138
|
+
|
|
139
|
+
if let Some(url) = self.base_url {
|
|
140
|
+
builder = builder.base_url(url);
|
|
141
|
+
}
|
|
142
|
+
if let Some(t) = self.timeout_secs {
|
|
143
|
+
builder = builder.timeout(Duration::from_secs(t));
|
|
144
|
+
}
|
|
145
|
+
if let Some(r) = self.max_retries {
|
|
146
|
+
builder = builder.max_retries(r);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Extra headers: push validated headers directly to the builder's
|
|
150
|
+
// internal config. We cannot use `builder.header()` in a loop because
|
|
151
|
+
// it consumes `self` and on `Err` the builder is lost. Since we are
|
|
152
|
+
// in the same crate, we can access `pub(crate)` fields.
|
|
153
|
+
#[cfg(feature = "native-http")]
|
|
154
|
+
if let Some(headers) = self.extra_headers {
|
|
155
|
+
for (k, v) in headers {
|
|
156
|
+
// Validate header name and value before pushing.
|
|
157
|
+
if reqwest::header::HeaderName::from_bytes(k.as_bytes()).is_ok()
|
|
158
|
+
&& reqwest::header::HeaderValue::from_str(&v).is_ok()
|
|
159
|
+
{
|
|
160
|
+
builder.config.extra_headers.push((k, v));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Tower middleware configs
|
|
166
|
+
#[cfg(feature = "tower")]
|
|
167
|
+
{
|
|
168
|
+
// Cache
|
|
169
|
+
if let Some(cache) = self.cache {
|
|
170
|
+
use crate::tower::{CacheBackend, CacheConfig};
|
|
171
|
+
let backend = match cache.backend.as_deref() {
|
|
172
|
+
Some("memory") | None => CacheBackend::Memory,
|
|
173
|
+
#[cfg(feature = "opendal-cache")]
|
|
174
|
+
Some(scheme) => CacheBackend::OpenDal {
|
|
175
|
+
scheme: scheme.to_string(),
|
|
176
|
+
config: cache.backend_config.unwrap_or_default(),
|
|
177
|
+
},
|
|
178
|
+
#[cfg(not(feature = "opendal-cache"))]
|
|
179
|
+
Some(_) => CacheBackend::Memory,
|
|
180
|
+
};
|
|
181
|
+
builder = builder.cache(CacheConfig {
|
|
182
|
+
max_entries: cache.max_entries.unwrap_or(256),
|
|
183
|
+
ttl: Duration::from_secs(cache.ttl_seconds.unwrap_or(300)),
|
|
184
|
+
backend,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Budget
|
|
189
|
+
if let Some(budget) = self.budget {
|
|
190
|
+
use crate::tower::{BudgetConfig, Enforcement};
|
|
191
|
+
builder = builder.budget(BudgetConfig {
|
|
192
|
+
global_limit: budget.global_limit,
|
|
193
|
+
model_limits: budget.model_limits.unwrap_or_default(),
|
|
194
|
+
enforcement: match budget.enforcement.as_deref() {
|
|
195
|
+
Some("soft") => Enforcement::Soft,
|
|
196
|
+
_ => Enforcement::Hard,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Cooldown
|
|
202
|
+
if let Some(secs) = self.cooldown_secs {
|
|
203
|
+
builder = builder.cooldown(Duration::from_secs(secs));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Rate limit
|
|
207
|
+
if let Some(rl) = self.rate_limit {
|
|
208
|
+
use crate::tower::RateLimitConfig;
|
|
209
|
+
builder = builder.rate_limit(RateLimitConfig {
|
|
210
|
+
rpm: rl.rpm,
|
|
211
|
+
tpm: rl.tpm,
|
|
212
|
+
window: Duration::from_secs(rl.window_seconds.unwrap_or(60)),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Health check
|
|
217
|
+
if let Some(secs) = self.health_check_secs {
|
|
218
|
+
builder = builder.health_check(Duration::from_secs(secs));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Cost tracking
|
|
222
|
+
if let Some(ct) = self.cost_tracking {
|
|
223
|
+
builder = builder.cost_tracking(ct);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Tracing
|
|
227
|
+
if let Some(t) = self.tracing {
|
|
228
|
+
builder = builder.tracing(t);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
builder
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Get the custom provider configurations from this file config.
|
|
236
|
+
pub fn providers(&self) -> &[FileProviderConfig] {
|
|
237
|
+
self.providers.as_deref().unwrap_or(&[])
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[cfg(test)]
|
|
242
|
+
mod tests {
|
|
243
|
+
use super::*;
|
|
244
|
+
|
|
245
|
+
#[test]
|
|
246
|
+
fn parse_minimal_config() {
|
|
247
|
+
let toml = r#"api_key = "sk-test""#;
|
|
248
|
+
let config = FileConfig::from_toml_str(toml).unwrap();
|
|
249
|
+
assert_eq!(config.api_key.as_deref(), Some("sk-test"));
|
|
250
|
+
assert!(config.base_url.is_none());
|
|
251
|
+
assert!(config.cache.is_none());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn parse_full_config() {
|
|
256
|
+
let toml = r#"
|
|
257
|
+
api_key = "sk-test"
|
|
258
|
+
base_url = "https://api.example.com/v1"
|
|
259
|
+
model_hint = "openai"
|
|
260
|
+
timeout_secs = 120
|
|
261
|
+
max_retries = 5
|
|
262
|
+
cooldown_secs = 30
|
|
263
|
+
health_check_secs = 60
|
|
264
|
+
cost_tracking = true
|
|
265
|
+
tracing = true
|
|
266
|
+
|
|
267
|
+
[cache]
|
|
268
|
+
max_entries = 512
|
|
269
|
+
ttl_seconds = 600
|
|
270
|
+
backend = "memory"
|
|
271
|
+
|
|
272
|
+
[budget]
|
|
273
|
+
global_limit = 50.0
|
|
274
|
+
enforcement = "hard"
|
|
275
|
+
|
|
276
|
+
[budget.model_limits]
|
|
277
|
+
"openai/gpt-4o" = 25.0
|
|
278
|
+
|
|
279
|
+
[rate_limit]
|
|
280
|
+
rpm = 60
|
|
281
|
+
tpm = 100000
|
|
282
|
+
|
|
283
|
+
[extra_headers]
|
|
284
|
+
"X-Custom" = "value"
|
|
285
|
+
|
|
286
|
+
[[providers]]
|
|
287
|
+
name = "my-provider"
|
|
288
|
+
base_url = "https://my-llm.example.com/v1"
|
|
289
|
+
auth_header = "Authorization"
|
|
290
|
+
model_prefixes = ["my-provider/"]
|
|
291
|
+
"#;
|
|
292
|
+
let config = FileConfig::from_toml_str(toml).unwrap();
|
|
293
|
+
assert_eq!(config.timeout_secs, Some(120));
|
|
294
|
+
assert_eq!(config.max_retries, Some(5));
|
|
295
|
+
assert!(config.cache.is_some());
|
|
296
|
+
assert!(config.budget.is_some());
|
|
297
|
+
assert_eq!(config.providers().len(), 1);
|
|
298
|
+
assert_eq!(config.providers()[0].name, "my-provider");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn rejects_unknown_fields() {
|
|
303
|
+
let toml = r#"
|
|
304
|
+
api_key = "sk-test"
|
|
305
|
+
unknown_field = true
|
|
306
|
+
"#;
|
|
307
|
+
assert!(FileConfig::from_toml_str(toml).is_err());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
fn into_builder_produces_valid_config() {
|
|
312
|
+
let toml = r#"
|
|
313
|
+
api_key = "sk-test"
|
|
314
|
+
timeout_secs = 30
|
|
315
|
+
max_retries = 2
|
|
316
|
+
"#;
|
|
317
|
+
let file_config = FileConfig::from_toml_str(toml).unwrap();
|
|
318
|
+
let config = file_config.into_builder().build();
|
|
319
|
+
assert_eq!(config.timeout, Duration::from_secs(30));
|
|
320
|
+
assert_eq!(config.max_retries, 2);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[test]
|
|
324
|
+
fn empty_config_is_valid() {
|
|
325
|
+
let config = FileConfig::from_toml_str("").unwrap();
|
|
326
|
+
assert!(config.api_key.is_none());
|
|
327
|
+
}
|
|
328
|
+
}
|