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,353 @@
1
+ //! Google Vertex AI OAuth2 credential provider (service-account JWT flow).
2
+ //!
3
+ //! Creates a self-signed JWT from a Google Cloud service account key, then
4
+ //! exchanges it for an access token via the Google OAuth2 token endpoint.
5
+ //! Tokens are cached and refreshed automatically when within 5 minutes of
6
+ //! expiry.
7
+ //!
8
+ //! # Environment variables
9
+ //!
10
+ //! | Variable | Description |
11
+ //! |----------|-------------|
12
+ //! | `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON key file |
13
+ //! | `VERTEX_AI_SCOPE` | OAuth scope (defaults to `https://www.googleapis.com/auth/cloud-platform`) |
14
+
15
+ use std::path::Path;
16
+ use std::time::{Instant, SystemTime, UNIX_EPOCH};
17
+
18
+ use jsonwebtoken::{Algorithm, EncodingKey, Header};
19
+ use secrecy::{ExposeSecret, SecretString};
20
+ use tokio::sync::RwLock;
21
+
22
+ use super::{Credential, CredentialProvider};
23
+ use crate::client::BoxFuture;
24
+ use crate::error::LiterLlmError;
25
+
26
+ /// Default OAuth2 scope for Google Cloud Platform / Vertex AI.
27
+ const DEFAULT_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";
28
+
29
+ /// Google OAuth2 token endpoint.
30
+ const TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token";
31
+
32
+ /// JWT grant type for the token exchange.
33
+ const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
34
+
35
+ /// JWT lifetime in seconds (Google allows up to 3600).
36
+ const JWT_LIFETIME_SECS: u64 = 3600;
37
+
38
+ /// Minimum remaining lifetime before a cached token is considered expired.
39
+ const EXPIRY_BUFFER_SECS: u64 = 300;
40
+
41
+ /// Cached token and its acquisition timestamp + lifetime.
42
+ struct CachedToken {
43
+ token: SecretString,
44
+ acquired_at: Instant,
45
+ expires_in_secs: u64,
46
+ }
47
+
48
+ impl CachedToken {
49
+ /// Returns `true` if the token is still valid with the safety buffer.
50
+ fn is_valid(&self) -> bool {
51
+ let elapsed = self.acquired_at.elapsed().as_secs();
52
+ elapsed + EXPIRY_BUFFER_SECS < self.expires_in_secs
53
+ }
54
+ }
55
+
56
+ /// Google Vertex AI OAuth2 credential provider using the service-account
57
+ /// JWT assertion flow (two-legged OAuth).
58
+ ///
59
+ /// Signs a JWT with the service account's private key, then exchanges it
60
+ /// at `https://oauth2.googleapis.com/token` for a short-lived access token.
61
+ pub struct VertexOAuthCredentialProvider {
62
+ service_account_email: String,
63
+ private_key_pem: SecretString,
64
+ scope: String,
65
+ cached: RwLock<Option<CachedToken>>,
66
+ http_client: reqwest::Client,
67
+ }
68
+
69
+ /// Intentionally redacts the private key to prevent accidental exposure.
70
+ impl std::fmt::Debug for VertexOAuthCredentialProvider {
71
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72
+ f.debug_struct("VertexOAuthCredentialProvider")
73
+ .field("service_account_email", &self.service_account_email)
74
+ .field("private_key_pem", &"[redacted]")
75
+ .field("scope", &self.scope)
76
+ .finish_non_exhaustive()
77
+ }
78
+ }
79
+
80
+ impl VertexOAuthCredentialProvider {
81
+ /// Create a provider from a parsed service account JSON object.
82
+ ///
83
+ /// Expects `client_email` and `private_key` fields in the JSON value.
84
+ ///
85
+ /// # Errors
86
+ ///
87
+ /// Returns [`LiterLlmError::Authentication`] if required fields are missing.
88
+ pub fn from_service_account_json(json: &serde_json::Value) -> Result<Self, LiterLlmError> {
89
+ let email = json
90
+ .get("client_email")
91
+ .and_then(serde_json::Value::as_str)
92
+ .ok_or_else(|| LiterLlmError::Authentication {
93
+ message: "service account JSON missing 'client_email' field".into(),
94
+ })?
95
+ .to_owned();
96
+
97
+ let key = json
98
+ .get("private_key")
99
+ .and_then(serde_json::Value::as_str)
100
+ .ok_or_else(|| LiterLlmError::Authentication {
101
+ message: "service account JSON missing 'private_key' field".into(),
102
+ })?
103
+ .to_owned();
104
+
105
+ Ok(Self {
106
+ service_account_email: email,
107
+ private_key_pem: SecretString::from(key),
108
+ scope: DEFAULT_SCOPE.to_owned(),
109
+ cached: RwLock::new(None),
110
+ http_client: reqwest::Client::new(),
111
+ })
112
+ }
113
+
114
+ /// Create a provider from a service account JSON key file on disk.
115
+ ///
116
+ /// # Errors
117
+ ///
118
+ /// Returns [`LiterLlmError::Authentication`] if the file cannot be read or
119
+ /// parsed, or if required fields are missing.
120
+ pub fn from_key_file(path: &Path) -> Result<Self, LiterLlmError> {
121
+ let contents = std::fs::read_to_string(path).map_err(|e| LiterLlmError::Authentication {
122
+ message: format!("failed to read service account key file {}: {e}", path.display()),
123
+ })?;
124
+
125
+ let json: serde_json::Value = serde_json::from_str(&contents).map_err(|e| LiterLlmError::Authentication {
126
+ message: format!("failed to parse service account key file: {e}"),
127
+ })?;
128
+
129
+ Self::from_service_account_json(&json)
130
+ }
131
+
132
+ /// Create a provider from the `GOOGLE_APPLICATION_CREDENTIALS` environment
133
+ /// variable, which should point to a service account JSON key file.
134
+ ///
135
+ /// # Errors
136
+ ///
137
+ /// Returns [`LiterLlmError::Authentication`] if the environment variable is
138
+ /// not set, the file cannot be read, or it cannot be parsed.
139
+ pub fn from_env() -> Result<Self, LiterLlmError> {
140
+ let path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS").map_err(|_| LiterLlmError::Authentication {
141
+ message: "missing required environment variable: GOOGLE_APPLICATION_CREDENTIALS".into(),
142
+ })?;
143
+
144
+ let mut provider = Self::from_key_file(Path::new(&path))?;
145
+
146
+ if let Ok(scope) = std::env::var("VERTEX_AI_SCOPE") {
147
+ provider.scope = scope;
148
+ }
149
+
150
+ Ok(provider)
151
+ }
152
+
153
+ /// Override the OAuth2 scope (default: `https://www.googleapis.com/auth/cloud-platform`).
154
+ #[must_use]
155
+ pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
156
+ self.scope = scope.into();
157
+ self
158
+ }
159
+
160
+ /// Override the HTTP client used for token requests.
161
+ #[must_use]
162
+ pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
163
+ self.http_client = client;
164
+ self
165
+ }
166
+
167
+ /// Build a signed JWT assertion and exchange it for an access token.
168
+ async fn fetch_token(&self) -> Result<CachedToken, LiterLlmError> {
169
+ let now = SystemTime::now()
170
+ .duration_since(UNIX_EPOCH)
171
+ .map_err(|e| LiterLlmError::Authentication {
172
+ message: format!("system clock error: {e}"),
173
+ })?
174
+ .as_secs();
175
+
176
+ let claims = JwtClaims {
177
+ iss: &self.service_account_email,
178
+ scope: &self.scope,
179
+ aud: TOKEN_ENDPOINT,
180
+ iat: now,
181
+ exp: now + JWT_LIFETIME_SECS,
182
+ };
183
+
184
+ let header = Header::new(Algorithm::RS256);
185
+ let encoding_key = EncodingKey::from_rsa_pem(self.private_key_pem.expose_secret().as_bytes()).map_err(|e| {
186
+ LiterLlmError::Authentication {
187
+ message: format!("invalid RSA private key: {e}"),
188
+ }
189
+ })?;
190
+
191
+ let assertion =
192
+ jsonwebtoken::encode(&header, &claims, &encoding_key).map_err(|e| LiterLlmError::Authentication {
193
+ message: format!("JWT signing failed: {e}"),
194
+ })?;
195
+
196
+ let resp = self
197
+ .http_client
198
+ .post(TOKEN_ENDPOINT)
199
+ .form(&[("grant_type", GRANT_TYPE), ("assertion", &assertion)])
200
+ .send()
201
+ .await
202
+ .map_err(|e| LiterLlmError::Authentication {
203
+ message: format!("Vertex OAuth token request failed: {e}"),
204
+ })?;
205
+
206
+ let status = resp.status();
207
+ let body = resp.text().await.map_err(|e| LiterLlmError::Authentication {
208
+ message: format!("Vertex OAuth token response unreadable: {e}"),
209
+ })?;
210
+
211
+ if !status.is_success() {
212
+ return Err(LiterLlmError::Authentication {
213
+ message: format!("Vertex OAuth token request returned {status}: {body}"),
214
+ });
215
+ }
216
+
217
+ let parsed: TokenResponse = serde_json::from_str(&body).map_err(|e| LiterLlmError::Authentication {
218
+ message: format!("Vertex OAuth token response parse error: {e}"),
219
+ })?;
220
+
221
+ Ok(CachedToken {
222
+ token: SecretString::from(parsed.access_token),
223
+ acquired_at: Instant::now(),
224
+ expires_in_secs: parsed.expires_in,
225
+ })
226
+ }
227
+ }
228
+
229
+ impl CredentialProvider for VertexOAuthCredentialProvider {
230
+ fn resolve(&self) -> BoxFuture<'_, Credential> {
231
+ Box::pin(async move {
232
+ // Fast path: read lock to check cache.
233
+ {
234
+ let guard = self.cached.read().await;
235
+ if let Some(ref cached) = *guard
236
+ && cached.is_valid()
237
+ {
238
+ return Ok(Credential::BearerToken(cached.token.clone()));
239
+ }
240
+ }
241
+
242
+ // Slow path: write lock to refresh.
243
+ let mut guard = self.cached.write().await;
244
+
245
+ // Double-check after acquiring write lock.
246
+ if let Some(ref cached) = *guard
247
+ && cached.is_valid()
248
+ {
249
+ return Ok(Credential::BearerToken(cached.token.clone()));
250
+ }
251
+
252
+ let fresh = self.fetch_token().await?;
253
+ let token = fresh.token.clone();
254
+ *guard = Some(fresh);
255
+
256
+ Ok(Credential::BearerToken(token))
257
+ })
258
+ }
259
+ }
260
+
261
+ /// JWT claims for the Google OAuth2 service-account assertion.
262
+ #[derive(serde::Serialize)]
263
+ struct JwtClaims<'a> {
264
+ iss: &'a str,
265
+ scope: &'a str,
266
+ aud: &'a str,
267
+ iat: u64,
268
+ exp: u64,
269
+ }
270
+
271
+ /// Minimal deserialization of the Google OAuth2 token response.
272
+ #[derive(serde::Deserialize)]
273
+ struct TokenResponse {
274
+ access_token: String,
275
+ expires_in: u64,
276
+ }
277
+
278
+ #[cfg(test)]
279
+ mod tests {
280
+ use super::*;
281
+
282
+ #[test]
283
+ fn cached_token_validity() {
284
+ let cached = CachedToken {
285
+ token: SecretString::from("test-token".to_owned()),
286
+ acquired_at: Instant::now(),
287
+ expires_in_secs: 3600,
288
+ };
289
+ assert!(cached.is_valid());
290
+ }
291
+
292
+ #[test]
293
+ fn cached_token_expired() {
294
+ let cached = CachedToken {
295
+ token: SecretString::from("test-token".to_owned()),
296
+ // Zero lifetime is immediately expired; avoids Duration subtraction panic on Windows.
297
+ acquired_at: Instant::now(),
298
+ expires_in_secs: 0,
299
+ };
300
+ assert!(!cached.is_valid());
301
+ }
302
+
303
+ #[test]
304
+ fn from_service_account_json_valid() {
305
+ let json = serde_json::json!({
306
+ "client_email": "test@project.iam.gserviceaccount.com",
307
+ "private_key": "-----BEGIN TEST RSA KEY-----\nMIIE...\n-----END TEST RSA KEY-----\n"
308
+ });
309
+ let provider = VertexOAuthCredentialProvider::from_service_account_json(&json);
310
+ assert!(provider.is_ok());
311
+ let provider = provider.expect("should succeed");
312
+ assert_eq!(provider.service_account_email, "test@project.iam.gserviceaccount.com");
313
+ assert_eq!(provider.scope, DEFAULT_SCOPE);
314
+ }
315
+
316
+ #[test]
317
+ fn from_service_account_json_missing_email() {
318
+ let json = serde_json::json!({
319
+ "private_key": "-----BEGIN TEST RSA KEY-----\nMIIE...\n-----END TEST RSA KEY-----\n"
320
+ });
321
+ let err = VertexOAuthCredentialProvider::from_service_account_json(&json).unwrap_err();
322
+ assert!(err.to_string().contains("client_email"));
323
+ }
324
+
325
+ #[test]
326
+ fn from_service_account_json_missing_key() {
327
+ let json = serde_json::json!({
328
+ "client_email": "test@project.iam.gserviceaccount.com"
329
+ });
330
+ let err = VertexOAuthCredentialProvider::from_service_account_json(&json).unwrap_err();
331
+ assert!(err.to_string().contains("private_key"));
332
+ }
333
+
334
+ #[test]
335
+ fn with_scope_override() {
336
+ let json = serde_json::json!({
337
+ "client_email": "test@project.iam.gserviceaccount.com",
338
+ "private_key": "-----BEGIN TEST RSA KEY-----\nMIIE...\n-----END TEST RSA KEY-----\n"
339
+ });
340
+ let provider = VertexOAuthCredentialProvider::from_service_account_json(&json)
341
+ .expect("should succeed")
342
+ .with_scope("https://custom.scope");
343
+ assert_eq!(provider.scope, "https://custom.scope");
344
+ }
345
+
346
+ #[tokio::test]
347
+ #[ignore] // Requires network access and a valid service account key file.
348
+ async fn live_vertex_oauth_token_exchange() {
349
+ let provider = VertexOAuthCredentialProvider::from_env().expect("GOOGLE_APPLICATION_CREDENTIALS not set");
350
+ let credential = provider.resolve().await.expect("token exchange failed");
351
+ assert!(matches!(credential, Credential::BearerToken(_)));
352
+ }
353
+ }