portkey_sdk/client/
config.rs

1//! Portkey client configuration and builder.
2//!
3//! This module provides the configuration types and builder pattern for creating
4//! and customizing [`PortkeyClient`] instances.
5
6use 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/// Configuration for the Portkey API client.
20///
21/// This struct holds all the necessary configuration parameters for creating and using
22/// a Portkey API client, including authentication credentials, API endpoint information,
23/// and HTTP client settings.
24///
25/// # Examples
26///
27/// Creating a config with virtual key:
28/// ```no_run
29/// use portkey_sdk::PortkeyConfig;
30/// use portkey_sdk::builder::AuthMethod;
31///
32/// let config = PortkeyConfig::builder()
33///     .with_api_key("your-portkey-api-key")
34///     .with_auth_method(AuthMethod::virtual_key("your-virtual-key"))
35///     .build()
36///     .unwrap();
37/// ```
38///
39/// Creating a config with provider auth:
40/// ```no_run
41/// use portkey_sdk::PortkeyConfig;
42/// use portkey_sdk::builder::AuthMethod;
43///
44/// let config = PortkeyConfig::builder()
45///     .with_api_key("your-portkey-api-key")
46///     .with_auth_method(AuthMethod::provider_auth("openai", "Bearer sk-..."))
47///     .build()
48///     .unwrap();
49/// ```
50///
51/// Creating a config from environment:
52/// ```no_run
53/// # use portkey_sdk::PortkeyConfig;
54/// // Requires PORTKEY_API_KEY and PORTKEY_VIRTUAL_KEY environment variables
55/// let config = PortkeyConfig::from_env().unwrap();
56/// ```
57#[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 for authentication with the Portkey API.
66    ///
67    /// This is your Portkey API key from the dashboard (x-portkey-api-key header).
68    api_key: String,
69
70    /// Authentication method for provider routing.
71    ///
72    /// Specifies how to authenticate with LLM providers through Portkey.
73    auth_method: AuthMethod,
74
75    /// Base URL for the Portkey API.
76    ///
77    /// Defaults to the official Portkey API endpoint or can be set to a self-hosted gateway.
78    #[builder(default = "Self::default_base_url()")]
79    base_url: String,
80
81    /// Timeout for HTTP requests.
82    ///
83    /// Controls how long the client will wait for API responses before timing out.
84    #[builder(default = "Self::default_timeout()")]
85    timeout: Duration,
86
87    /// Optional custom reqwest client.
88    ///
89    /// If provided, this client will be used instead of creating a new one.
90    /// This allows for custom configuration of the HTTP client (e.g., proxies, custom headers, etc.).
91    #[builder(default = "None")]
92    client: Option<Client>,
93
94    /// Optional trace ID for request tracking.
95    ///
96    /// An ID you can pass to refer to one or more requests later on.
97    /// If not provided, Portkey generates a trace ID automatically.
98    #[builder(default = "None")]
99    trace_id: Option<String>,
100
101    /// Optional metadata to attach to requests.
102    ///
103    /// Arbitrary metadata that will be logged with your requests in Portkey.
104    #[builder(default = "None")]
105    metadata: Option<HashMap<String, serde_json::Value>>,
106
107    /// Optional cache namespace.
108    ///
109    /// Partition your Portkey cache store based on custom strings.
110    #[builder(default = "None")]
111    cache_namespace: Option<String>,
112
113    /// Optional cache force refresh flag.
114    ///
115    /// Forces a cache refresh by making a new API call and storing the updated value.
116    #[builder(default = "None")]
117    cache_force_refresh: Option<bool>,
118}
119
120impl PortkeyBuilder {
121    /// Returns the default base URL for the Portkey API.
122    fn default_base_url() -> String {
123        "https://api.portkey.ai/v1".to_string()
124    }
125
126    /// Returns the default timeout.
127    fn default_timeout() -> Duration {
128        Duration::from_secs(30)
129    }
130
131    /// Validates the configuration before building.
132    fn validate_config(&self) -> Result<(), String> {
133        // Validate API key is not empty
134        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        // Validate timeout is reasonable
141        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    /// Creates a Portkey API client directly from the builder.
154    ///
155    /// This is a convenience method that builds the configuration and
156    /// creates a client in one step. This is the recommended way to
157    /// create a client.
158    ///
159    /// # Examples
160    ///
161    /// ```no_run
162    /// # use portkey_sdk::PortkeyConfig;
163    /// let client = PortkeyConfig::builder()
164    ///     .with_api_key("your-api-key")
165    ///     .build_client()
166    ///     .unwrap();
167    /// ```
168    pub fn build_client(self) -> Result<PortkeyClient> {
169        let config = self.build()?;
170        PortkeyClient::new(config)
171    }
172}
173
174impl PortkeyConfig {
175    /// Creates a new configuration builder.
176    ///
177    /// This is the recommended way to construct a `PortkeyConfig`.
178    ///
179    /// # Examples
180    ///
181    /// ```no_run
182    /// # use portkey_sdk::PortkeyConfig;
183    /// let config = PortkeyConfig::builder()
184    ///     .with_api_key("your-api-key")
185    ///     .build()
186    ///     .unwrap();
187    /// ```
188    pub fn builder() -> PortkeyBuilder {
189        PortkeyBuilder::default()
190    }
191
192    /// Creates a new Portkey API client using this configuration.
193    ///
194    /// # Examples
195    ///
196    /// ```no_run
197    /// # use portkey_sdk::PortkeyConfig;
198    /// let config = PortkeyConfig::builder()
199    ///     .with_api_key("your-api-key")
200    ///     .build()
201    ///     .unwrap();
202    ///
203    /// let client = config.build_client().unwrap();
204    /// ```
205    pub fn build_client(self) -> Result<PortkeyClient> {
206        PortkeyClient::new(self)
207    }
208
209    /// Returns the API key.
210    pub fn api_key(&self) -> &str {
211        &self.api_key
212    }
213
214    /// Returns a masked version of the API key for safe display/logging.
215    ///
216    /// Shows the first 4 characters followed by "****", or just "****"
217    /// if the key is shorter than 4 characters.
218    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    /// Returns the authentication method.
227    pub fn auth_method(&self) -> &AuthMethod {
228        &self.auth_method
229    }
230
231    /// Returns the base URL.
232    pub fn base_url(&self) -> &str {
233        &self.base_url
234    }
235
236    /// Returns the timeout duration.
237    pub fn timeout(&self) -> Duration {
238        self.timeout
239    }
240
241    /// Returns a clone of the custom reqwest client, if one was provided.
242    pub(crate) fn client(&self) -> Option<Client> {
243        self.client.clone()
244    }
245
246    /// Returns the trace ID, if set.
247    pub fn trace_id(&self) -> Option<&str> {
248        self.trace_id.as_deref()
249    }
250
251    /// Returns the metadata, if set.
252    pub fn metadata(&self) -> Option<&HashMap<String, serde_json::Value>> {
253        self.metadata.as_ref()
254    }
255
256    /// Returns the cache namespace, if set.
257    pub fn cache_namespace(&self) -> Option<&str> {
258        self.cache_namespace.as_deref()
259    }
260
261    /// Returns the cache force refresh flag, if set.
262    pub fn cache_force_refresh(&self) -> Option<bool> {
263        self.cache_force_refresh
264    }
265
266    /// Creates a configuration from environment variables.
267    ///
268    /// # Environment Variables
269    ///
270    /// **Required:**
271    /// - `PORTKEY_API_KEY` - Your Portkey API key
272    ///
273    /// **Authentication (choose one):**
274    /// - `PORTKEY_VIRTUAL_KEY` - Virtual key for managed provider credentials
275    /// - `PORTKEY_PROVIDER` + `PORTKEY_AUTHORIZATION` - Direct provider auth
276    /// - `PORTKEY_CONFIG` - Config ID for complex routing
277    ///
278    /// **Optional:**
279    /// - `PORTKEY_CUSTOM_HOST` - Custom host URL (with provider auth)
280    /// - `PORTKEY_BASE_URL` - Base URL for the API
281    /// - `PORTKEY_TIMEOUT_SECS` - Request timeout in seconds
282    /// - `PORTKEY_TRACE_ID` - Trace ID for request tracking
283    /// - `PORTKEY_CACHE_NAMESPACE` - Cache namespace
284    /// - `PORTKEY_CACHE_FORCE_REFRESH` - Force cache refresh (true/false)
285    ///
286    /// # Examples
287    ///
288    /// ```bash
289    /// # Virtual key authentication
290    /// export PORTKEY_API_KEY=your-portkey-api-key
291    /// export PORTKEY_VIRTUAL_KEY=your-virtual-key
292    ///
293    /// # Provider authentication
294    /// export PORTKEY_API_KEY=your-portkey-api-key
295    /// export PORTKEY_PROVIDER=openai
296    /// export PORTKEY_AUTHORIZATION="Bearer sk-..."
297    ///
298    /// # Config-based authentication
299    /// export PORTKEY_API_KEY=your-portkey-api-key
300    /// export PORTKEY_CONFIG=pc-config-123
301    /// ```
302    #[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        // Determine authentication method
317        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        // Optional: custom base URL
346        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        // Optional: custom timeout
354        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        // Optional: trace ID
372        if let Ok(trace_id) = std::env::var("PORTKEY_TRACE_ID") {
373            builder = builder.with_trace_id(trace_id);
374        }
375
376        // Optional: cache namespace
377        if let Ok(cache_namespace) = std::env::var("PORTKEY_CACHE_NAMESPACE") {
378            builder = builder.with_cache_namespace(cache_namespace);
379        }
380
381        // Optional: cache force refresh
382        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}