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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/ext/liter_llm_rb/native/Cargo.toml +1 -1
  3. data/vendor/Cargo.toml +1 -1
  4. data/vendor/liter-llm/Cargo.toml +3 -1
  5. data/vendor/liter-llm/src/client/config.rs +1 -1
  6. data/vendor/liter-llm/src/client/config_file.rs +328 -0
  7. data/vendor/liter-llm/src/client/mod.rs +317 -130
  8. data/vendor/liter-llm/src/http/eventstream.rs +19 -19
  9. data/vendor/liter-llm/src/http/request.rs +1 -0
  10. data/vendor/liter-llm/src/lib.rs +2 -1
  11. data/vendor/liter-llm/src/provider/anthropic.rs +5 -2
  12. data/vendor/liter-llm/src/provider/vertex.rs +119 -0
  13. data/vendor/liter-llm/src/types/common.rs +7 -1
  14. data/vendor/liter-llm/tests/config_file.rs +408 -0
  15. data/vendor/liter-llm/tests/fixtures/config/budget_soft.toml +6 -0
  16. data/vendor/liter-llm/tests/fixtures/config/cache_only.toml +4 -0
  17. data/vendor/liter-llm/tests/fixtures/config/cache_opendal.toml +8 -0
  18. data/vendor/liter-llm/tests/fixtures/config/full.toml +42 -0
  19. data/vendor/liter-llm/tests/fixtures/config/invalid_nested_unknown.toml +3 -0
  20. data/vendor/liter-llm/tests/fixtures/config/invalid_unknown_field.toml +2 -0
  21. data/vendor/liter-llm/tests/fixtures/config/invalid_wrong_type.toml +1 -0
  22. data/vendor/liter-llm/tests/fixtures/config/minimal.toml +1 -0
  23. data/vendor/liter-llm/tests/fixtures/config/providers_only.toml +10 -0
  24. data/vendor/liter-llm/tests/fixtures/config/rate_limit_only.toml +3 -0
  25. data/vendor/liter-llm/tests/live_providers/anthropic.rs +278 -0
  26. data/vendor/liter-llm/tests/live_providers/azure.rs +88 -0
  27. data/vendor/liter-llm/tests/live_providers/bedrock.rs +60 -0
  28. data/vendor/liter-llm/tests/live_providers/cross_provider.rs +102 -0
  29. data/vendor/liter-llm/tests/live_providers/google_ai.rs +107 -0
  30. data/vendor/liter-llm/tests/live_providers/mistral.rs +96 -0
  31. data/vendor/liter-llm/tests/live_providers/openai.rs +109 -0
  32. data/vendor/liter-llm/tests/live_providers/vertex_ai.rs +96 -0
  33. data/vendor/liter-llm/tests/live_providers.rs +159 -0
  34. data/vendor/liter-llm-ffi/Cargo.toml +3 -3
  35. data/vendor/liter-llm-ffi/liter_llm.h +1 -1
  36. metadata +22 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a82e2592d26a1a0506b48ddc1bbee3a5a03c458f1a71a2ebc4ff7893a581a5b1
4
- data.tar.gz: 5306a47cb98766a4b94efed94d3443fd139bc40cee2b928fbe81071393b51753
3
+ metadata.gz: 99f9baa37507b2b56d9d5300e3a53ede447fbfe898e57a93f757a0d36b6aa20d
4
+ data.tar.gz: 9fdadec249c006f4b368c545f879f5512b60759bc19d74206ab945017efb4826
5
5
  SHA512:
6
- metadata.gz: 6bc4475bcca4b90c6910127a05f63c9f3e7e2f20b0289cfeffcfb554d0c55046a09bb76bab6b420848aeeae6b29c28ab6c48be7ee888821e91c0fe6ff0afbfa2
7
- data.tar.gz: 71de09e766ac105c38afcae581e3aa7f4be0d5eb0fbc0a12dfb8d9112b02ba20feae96f2462793a4c4093feb9e31feaa6a5d1a970d3cb80e3598c604152848e6
6
+ metadata.gz: 6cf1243668cc3e7852198f92da30a92991072cca691c09b91600f8ab874d32d55e55f6e2156f785f6ddf2e8bf2d2d5a5f17c8d931e4c226c98740463153659a7
7
+ data.tar.gz: b6eb0d41eb748f0f6c2d4eee706cb23c04da9370681f7532335b99a6fea2dd2035421d44048ae79c7adfece41973dcf4883d9ea931cf31583e96ac016f8e899f
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "liter-llm-rb"
3
- version = "1.0.0-rc.8"
3
+ version = "1.0.0-rc.9"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
6
6
  license = "MIT"
data/vendor/Cargo.toml CHANGED
@@ -2,7 +2,7 @@
2
2
  members = ["liter-llm", "liter-llm-ffi"]
3
3
 
4
4
  [workspace.package]
5
- version = "1.0.0-rc.8"
5
+ version = "1.0.0-rc.9"
6
6
  edition = "2024"
7
7
  authors = ["Na'aman Hirschfeld <naaman@kreuzberg.dev>"]
8
8
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "liter-llm"
3
- version = "1.0.0-rc.8"
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"] }
@@ -174,7 +174,7 @@ impl std::fmt::Debug for ClientConfig {
174
174
  /// obtain a [`ClientConfig`].
175
175
  #[must_use]
176
176
  pub struct ClientConfigBuilder {
177
- config: ClientConfig,
177
+ pub(crate) config: ClientConfig,
178
178
  }
179
179
 
180
180
  impl ClientConfigBuilder {
@@ -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
+ }