portkey_sdk/client/
config.rs1use std::collections::HashMap;
7use std::fmt;
8use std::time::Duration;
9
10use derive_builder::Builder;
11use reqwest::Client;
12
13use super::auth::AuthMethod;
14use super::portkey::PortkeyClient;
15#[cfg(feature = "tracing")]
16use crate::TRACING_TARGET_CONFIG;
17use crate::error::Result;
18
19#[derive(Clone, Builder)]
58#[builder(
59 name = "PortkeyBuilder",
60 pattern = "owned",
61 setter(into, strip_option, prefix = "with"),
62 build_fn(validate = "Self::validate_config")
63)]
64pub struct PortkeyConfig {
65 api_key: String,
69
70 auth_method: AuthMethod,
74
75 #[builder(default = "Self::default_base_url()")]
79 base_url: String,
80
81 #[builder(default = "Self::default_timeout()")]
85 timeout: Duration,
86
87 #[builder(default = "None")]
92 client: Option<Client>,
93
94 #[builder(default = "None")]
99 trace_id: Option<String>,
100
101 #[builder(default = "None")]
105 metadata: Option<HashMap<String, serde_json::Value>>,
106
107 #[builder(default = "None")]
111 cache_namespace: Option<String>,
112
113 #[builder(default = "None")]
117 cache_force_refresh: Option<bool>,
118}
119
120impl PortkeyBuilder {
121 fn default_base_url() -> String {
123 "https://api.portkey.ai/v1".to_string()
124 }
125
126 fn default_timeout() -> Duration {
128 Duration::from_secs(30)
129 }
130
131 fn validate_config(&self) -> Result<(), String> {
133 if let Some(ref api_key) = self.api_key
135 && api_key.trim().is_empty()
136 {
137 return Err("API key cannot be empty".to_string());
138 }
139
140 if let Some(timeout) = self.timeout {
142 if timeout.is_zero() {
143 return Err("Timeout must be greater than 0".to_string());
144 }
145 if timeout > Duration::from_secs(300) {
146 return Err("Timeout cannot exceed 300 seconds (5 minutes)".to_string());
147 }
148 }
149
150 Ok(())
151 }
152
153 pub fn build_client(self) -> Result<PortkeyClient> {
169 let config = self.build()?;
170 PortkeyClient::new(config)
171 }
172}
173
174impl PortkeyConfig {
175 pub fn builder() -> PortkeyBuilder {
189 PortkeyBuilder::default()
190 }
191
192 pub fn build_client(self) -> Result<PortkeyClient> {
206 PortkeyClient::new(self)
207 }
208
209 pub fn api_key(&self) -> &str {
211 &self.api_key
212 }
213
214 pub fn masked_api_key(&self) -> String {
219 if self.api_key.len() > 4 {
220 format!("{}****", &self.api_key[..4])
221 } else {
222 "****".to_string()
223 }
224 }
225
226 pub fn auth_method(&self) -> &AuthMethod {
228 &self.auth_method
229 }
230
231 pub fn base_url(&self) -> &str {
233 &self.base_url
234 }
235
236 pub fn timeout(&self) -> Duration {
238 self.timeout
239 }
240
241 pub(crate) fn client(&self) -> Option<Client> {
243 self.client.clone()
244 }
245
246 pub fn trace_id(&self) -> Option<&str> {
248 self.trace_id.as_deref()
249 }
250
251 pub fn metadata(&self) -> Option<&HashMap<String, serde_json::Value>> {
253 self.metadata.as_ref()
254 }
255
256 pub fn cache_namespace(&self) -> Option<&str> {
258 self.cache_namespace.as_deref()
259 }
260
261 pub fn cache_force_refresh(&self) -> Option<bool> {
263 self.cache_force_refresh
264 }
265
266 #[cfg_attr(feature = "tracing", tracing::instrument)]
303 pub fn from_env() -> Result<Self> {
304 #[cfg(feature = "tracing")]
305 tracing::debug!(target: TRACING_TARGET_CONFIG, "Loading configuration from environment");
306
307 let api_key = std::env::var("PORTKEY_API_KEY").map_err(|_| {
308 #[cfg(feature = "tracing")]
309 tracing::error!(target: TRACING_TARGET_CONFIG, "PORTKEY_API_KEY environment variable not set");
310
311 PortkeyBuilderError::ValidationError(
312 "PORTKEY_API_KEY environment variable not set".to_string(),
313 )
314 })?;
315
316 let auth_method = if let Ok(virtual_key) = std::env::var("PORTKEY_VIRTUAL_KEY") {
318 AuthMethod::VirtualKey { virtual_key }
319 } else if let Ok(provider) = std::env::var("PORTKEY_PROVIDER") {
320 let authorization = std::env::var("PORTKEY_AUTHORIZATION").map_err(|_| {
321 PortkeyBuilderError::ValidationError(
322 "PORTKEY_AUTHORIZATION required when PORTKEY_PROVIDER is set".to_string(),
323 )
324 })?;
325 let custom_host = std::env::var("PORTKEY_CUSTOM_HOST").ok();
326 AuthMethod::ProviderAuth {
327 provider,
328 authorization,
329 custom_host,
330 }
331 } else if let Ok(config_id) = std::env::var("PORTKEY_CONFIG") {
332 AuthMethod::Config { config_id }
333 } else {
334 return Err(PortkeyBuilderError::ValidationError(
335 "One of PORTKEY_VIRTUAL_KEY, PORTKEY_PROVIDER, or PORTKEY_CONFIG must be set"
336 .to_string(),
337 )
338 .into());
339 };
340
341 let mut builder = Self::builder()
342 .with_api_key(api_key)
343 .with_auth_method(auth_method);
344
345 if let Ok(base_url) = std::env::var("PORTKEY_BASE_URL") {
347 #[cfg(feature = "tracing")]
348 tracing::debug!(target: TRACING_TARGET_CONFIG, base_url = %base_url, "Using custom base URL");
349
350 builder = builder.with_base_url(base_url);
351 }
352
353 if let Ok(timeout_str) = std::env::var("PORTKEY_TIMEOUT_SECS") {
355 let timeout_secs = timeout_str.parse::<u64>().map_err(|_| {
356 #[cfg(feature = "tracing")]
357 tracing::error!(target: TRACING_TARGET_CONFIG, timeout_str = %timeout_str, "Invalid PORTKEY_TIMEOUT_SECS value");
358
359 PortkeyBuilderError::ValidationError(format!(
360 "Invalid PORTKEY_TIMEOUT_SECS value: {}",
361 timeout_str
362 ))
363 })?;
364
365 #[cfg(feature = "tracing")]
366 tracing::debug!(target: TRACING_TARGET_CONFIG, timeout_secs, "Using custom timeout");
367
368 builder = builder.with_timeout(Duration::from_secs(timeout_secs));
369 }
370
371 if let Ok(trace_id) = std::env::var("PORTKEY_TRACE_ID") {
373 builder = builder.with_trace_id(trace_id);
374 }
375
376 if let Ok(cache_namespace) = std::env::var("PORTKEY_CACHE_NAMESPACE") {
378 builder = builder.with_cache_namespace(cache_namespace);
379 }
380
381 if let Ok(cache_force_refresh_str) = std::env::var("PORTKEY_CACHE_FORCE_REFRESH")
383 && let Ok(cache_force_refresh) = cache_force_refresh_str.parse::<bool>()
384 {
385 builder = builder.with_cache_force_refresh(cache_force_refresh);
386 }
387
388 let config = builder.build()?;
389
390 #[cfg(feature = "tracing")]
391 tracing::info!(
392 target: TRACING_TARGET_CONFIG,
393 base_url = %config.base_url(),
394 timeout = ?config.timeout(),
395 "Configuration loaded successfully from environment"
396 );
397
398 Ok(config)
399 }
400}
401
402impl fmt::Debug for PortkeyConfig {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 f.debug_struct("PortkeyConfig")
405 .field("api_key", &self.masked_api_key())
406 .field("base_url", &self.base_url)
407 .field("timeout", &self.timeout)
408 .finish()
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn test_config_builder() -> Result<()> {
418 let config = PortkeyConfig::builder()
419 .with_api_key("test_key")
420 .with_auth_method(AuthMethod::VirtualKey {
421 virtual_key: "test_virtual_key".to_string(),
422 })
423 .build()?;
424
425 assert_eq!(config.api_key(), "test_key");
426 assert_eq!(config.base_url(), "https://api.portkey.ai/v1");
427 assert_eq!(config.timeout(), Duration::from_secs(30));
428
429 Ok(())
430 }
431
432 #[test]
433 fn test_config_builder_with_custom_values() -> Result<()> {
434 let config = PortkeyConfig::builder()
435 .with_api_key("test_key")
436 .with_auth_method(AuthMethod::VirtualKey {
437 virtual_key: "test_virtual_key".to_string(),
438 })
439 .with_base_url("https://custom.api.com")
440 .with_timeout(Duration::from_secs(60))
441 .build()?;
442
443 assert_eq!(config.api_key(), "test_key");
444 assert_eq!(config.base_url(), "https://custom.api.com");
445 assert_eq!(config.timeout(), Duration::from_secs(60));
446
447 Ok(())
448 }
449
450 #[test]
451 fn test_config_validation_empty_api_key() {
452 let result = PortkeyConfig::builder()
453 .with_api_key("")
454 .with_auth_method(AuthMethod::VirtualKey {
455 virtual_key: "test".to_string(),
456 })
457 .build();
458 assert!(result.is_err());
459 }
460
461 #[test]
462 fn test_config_validation_zero_timeout() {
463 let result = PortkeyConfig::builder()
464 .with_api_key("test_key")
465 .with_auth_method(AuthMethod::VirtualKey {
466 virtual_key: "test".to_string(),
467 })
468 .with_timeout(Duration::from_secs(0))
469 .build();
470
471 assert!(result.is_err());
472 }
473
474 #[test]
475 fn test_config_validation_excessive_timeout() {
476 let result = PortkeyConfig::builder()
477 .with_api_key("test_key")
478 .with_auth_method(AuthMethod::VirtualKey {
479 virtual_key: "test".to_string(),
480 })
481 .with_timeout(Duration::from_secs(400))
482 .build();
483
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn test_masked_api_key() -> Result<()> {
489 let config = PortkeyConfig::builder()
490 .with_api_key("test_key_12345")
491 .with_auth_method(AuthMethod::VirtualKey {
492 virtual_key: "test".to_string(),
493 })
494 .build()?;
495
496 assert_eq!(config.masked_api_key(), "test****");
497
498 Ok(())
499 }
500
501 #[test]
502 fn test_masked_api_key_short() -> Result<()> {
503 let config = PortkeyConfig::builder()
504 .with_api_key("abc")
505 .with_auth_method(AuthMethod::VirtualKey {
506 virtual_key: "test".to_string(),
507 })
508 .build()?;
509
510 assert_eq!(config.masked_api_key(), "****");
511
512 Ok(())
513 }
514
515 #[test]
516 fn test_auth_method_virtual_key() -> Result<()> {
517 let config = PortkeyConfig::builder()
518 .with_api_key("test_key")
519 .with_auth_method(AuthMethod::VirtualKey {
520 virtual_key: "vk-123".to_string(),
521 })
522 .build()?;
523
524 matches!(
525 config.auth_method(),
526 AuthMethod::VirtualKey { virtual_key } if virtual_key == "vk-123"
527 );
528
529 Ok(())
530 }
531
532 #[test]
533 fn test_auth_method_provider_auth() -> Result<()> {
534 let config = PortkeyConfig::builder()
535 .with_api_key("test_key")
536 .with_auth_method(AuthMethod::ProviderAuth {
537 provider: "openai".to_string(),
538 authorization: "Bearer sk-123".to_string(),
539 custom_host: None,
540 })
541 .build()?;
542
543 matches!(
544 config.auth_method(),
545 AuthMethod::ProviderAuth { provider, .. } if provider == "openai"
546 );
547
548 Ok(())
549 }
550
551 #[test]
552 fn test_auth_method_config() -> Result<()> {
553 let config = PortkeyConfig::builder()
554 .with_api_key("test_key")
555 .with_auth_method(AuthMethod::Config {
556 config_id: "pc-config-123".to_string(),
557 })
558 .build()?;
559
560 matches!(
561 config.auth_method(),
562 AuthMethod::Config { config_id } if config_id == "pc-config-123"
563 );
564
565 Ok(())
566 }
567
568 #[test]
569 fn test_optional_headers() -> Result<()> {
570 let mut metadata = HashMap::new();
571 metadata.insert("user_id".to_string(), serde_json::json!("12345"));
572
573 let config = PortkeyConfig::builder()
574 .with_api_key("test_key")
575 .with_auth_method(AuthMethod::VirtualKey {
576 virtual_key: "vk-123".to_string(),
577 })
578 .with_trace_id("trace-123")
579 .with_metadata(metadata.clone())
580 .with_cache_namespace("my-cache")
581 .with_cache_force_refresh(true)
582 .build()?;
583
584 assert_eq!(config.trace_id(), Some("trace-123"));
585 assert_eq!(config.cache_namespace(), Some("my-cache"));
586 assert_eq!(config.cache_force_refresh(), Some(true));
587 assert!(config.metadata().is_some());
588
589 Ok(())
590 }
591}