portkey_sdk/client/
portkey.rs

1//! Portkey API client implementation.
2//!
3//! This module contains the main [`PortkeyClient`] struct and its implementation,
4//! providing the core HTTP client functionality for interacting with the Portkey API.
5
6use std::fmt;
7use std::sync::Arc;
8
9use reqwest::multipart::Form;
10use reqwest::{Client, Method, RequestBuilder, Response};
11
12use super::auth::AuthMethod;
13use super::config::PortkeyConfig;
14#[cfg(feature = "tracing")]
15use crate::TRACING_TARGET_CLIENT;
16use crate::error::Result;
17
18/// Main Portkey API client for interacting with all Portkey services.
19///
20/// The `PortkeyClient` provides access to all Portkey API endpoints through specialized
21/// service interfaces. It handles authentication, request/response serialization,
22/// and provides a consistent async interface for all operations.
23///
24/// # Features
25///
26/// - **Thread-safe**: Safe to use across multiple threads
27/// - **Cheap to clone**: Uses `Arc` internally for efficient cloning
28/// - **Automatic authentication**: Handles API key authentication automatically
29///
30/// # Examples
31///
32/// ## Basic usage with environment configuration
33///
34/// ```no_run
35/// use portkey_sdk::{PortkeyClient, Result};
36///
37/// # async fn example() -> Result<()> {
38/// let client = PortkeyClient::from_env()?;
39/// # Ok(())
40/// # }
41/// ```
42///
43/// ## Custom configuration with builder pattern
44///
45/// ```no_run
46/// use portkey_sdk::{PortkeyConfig, PortkeyClient, Result};
47/// use std::time::Duration;
48///
49/// # async fn example() -> Result<()> {
50/// let client = PortkeyConfig::builder()
51///     .with_api_key("your-api-key")
52///     .with_base_url("https://api.portkey.ai/v1")
53///     .with_timeout(Duration::from_secs(30))
54///     .build_client()?;
55/// # Ok(())
56/// # }
57/// ```
58///
59/// ## Multi-threaded usage
60///
61/// The client is cheap to clone (uses `Arc` internally):
62///
63/// ```no_run
64/// use portkey_sdk::{PortkeyClient, Result};
65/// use tokio::task;
66///
67/// # async fn example() -> Result<()> {
68/// let client = PortkeyClient::from_env()?;
69///
70/// let handles: Vec<_> = (0..3).map(|i| {
71///     let client = client.clone();
72///     task::spawn(async move {
73///         // Use client here
74///         Ok::<(), portkey_sdk::Error>(())
75///     })
76/// }).collect();
77///
78/// for handle in handles {
79///     handle.await.unwrap()?;
80/// }
81/// # Ok(())
82/// # }
83/// ```
84#[derive(Clone)]
85pub struct PortkeyClient {
86    pub(crate) inner: Arc<PortkeyClientInner>,
87}
88
89/// Inner client state that is shared via Arc for cheap cloning.
90#[derive(Debug)]
91pub(crate) struct PortkeyClientInner {
92    pub(crate) config: PortkeyConfig,
93    pub(crate) client: Client,
94}
95
96impl PortkeyClient {
97    /// Creates a new Portkey API client.
98    #[cfg_attr(feature = "tracing", tracing::instrument(skip(config), fields(api_key = %config.masked_api_key())))]
99    pub fn new(config: PortkeyConfig) -> Result<Self> {
100        #[cfg(feature = "tracing")]
101        tracing::debug!(target: TRACING_TARGET_CLIENT, "Creating Portkey client");
102
103        let client = if let Some(custom_client) = config.client() {
104            custom_client
105        } else {
106            Client::builder().timeout(config.timeout()).build()?
107        };
108
109        #[cfg(feature = "tracing")]
110        tracing::info!(
111            target: TRACING_TARGET_CLIENT,
112            base_url = %config.base_url(),
113            timeout = ?config.timeout(),
114            api_key = %config.masked_api_key(),
115            custom_client = config.client().is_some(),
116            "Portkey client created successfully"
117        );
118
119        let inner = Arc::new(PortkeyClientInner { config, client });
120        Ok(Self { inner })
121    }
122
123    /// Creates a new configuration builder for constructing a Portkey client.
124    ///
125    /// This is a convenience method that returns a `PortkeyBuilder` for building
126    /// a custom client configuration.
127    ///
128    /// # Example
129    /// ```no_run
130    /// # use portkey_sdk::{PortkeyClient, Result};
131    /// # use portkey_sdk::builder::AuthMethod;
132    /// # use std::time::Duration;
133    /// # async fn example() -> Result<()> {
134    /// let client = PortkeyClient::builder()
135    ///     .with_api_key("your-api-key")
136    ///     .with_auth_method(AuthMethod::VirtualKey {
137    ///         virtual_key: "your-virtual-key".to_string(),
138    ///     })
139    ///     .with_timeout(Duration::from_secs(60))
140    ///     .build_client()?;
141    /// # Ok(())
142    /// # }
143    /// ```
144    pub fn builder() -> crate::builder::PortkeyBuilder {
145        PortkeyConfig::builder()
146    }
147
148    /// Creates a new Portkey API client from environment variables.
149    ///
150    /// This is a convenience method that creates a PortkeyConfig from environment
151    /// variables and then creates a client from that config.
152    ///
153    /// # Environment Variables
154    ///
155    /// - `PORTKEY_API_KEY` - Your Portkey API key (required)
156    /// - `PORTKEY_BASE_URL` - Base URL for the API (optional, defaults to <https://api.portkey.ai/v1>)
157    /// - `PORTKEY_TIMEOUT_SECS` - Request timeout in seconds (optional, defaults to 30)
158    ///
159    /// # Example
160    /// ```no_run
161    /// # use portkey_sdk::{PortkeyClient, Result};
162    /// # async fn example() -> Result<()> {
163    /// let client = PortkeyClient::from_env()?;
164    /// # Ok(())
165    /// # }
166    /// ```
167    #[cfg_attr(feature = "tracing", tracing::instrument)]
168    pub fn from_env() -> Result<Self> {
169        #[cfg(feature = "tracing")]
170        tracing::debug!(target: TRACING_TARGET_CLIENT, "Creating Portkey client from environment");
171
172        let config = PortkeyConfig::from_env()?;
173        Self::new(config)
174    }
175
176    /// Applies Portkey-specific headers to a request builder.
177    ///
178    /// This method adds all required and optional Portkey headers to the request.
179    /// If metadata serialization fails, it logs a warning and continues without the metadata header.
180    #[cfg_attr(
181        feature = "tracing",
182        tracing::instrument(skip(self, builder), fields(auth_method))
183    )]
184    fn apply_portkey_headers(&self, mut builder: RequestBuilder) -> RequestBuilder {
185        // Always add the Portkey API key
186        builder = builder.header("x-portkey-api-key", self.inner.config.api_key());
187
188        // Add authentication method headers
189        match self.inner.config.auth_method() {
190            AuthMethod::VirtualKey { virtual_key } => {
191                #[cfg(feature = "tracing")]
192                tracing::trace!(target: TRACING_TARGET_CLIENT, "Using virtual key authentication");
193
194                builder = builder.header("x-portkey-virtual-key", virtual_key);
195            }
196            AuthMethod::ProviderAuth {
197                provider,
198                authorization,
199                custom_host,
200            } => {
201                #[cfg(feature = "tracing")]
202                tracing::trace!(target: TRACING_TARGET_CLIENT, provider = %provider, "Using provider authentication");
203
204                builder = builder.header("x-portkey-provider", provider);
205                builder = builder.header("Authorization", authorization);
206                if let Some(host) = custom_host {
207                    builder = builder.header("x-portkey-custom-host", host);
208                }
209            }
210            AuthMethod::Config { config_id } => {
211                #[cfg(feature = "tracing")]
212                tracing::trace!(target: TRACING_TARGET_CLIENT, config_id = %config_id, "Using config-based authentication");
213
214                builder = builder.header("x-portkey-config", config_id);
215            }
216        }
217
218        // Add optional headers
219        if let Some(trace_id) = self.inner.config.trace_id() {
220            #[cfg(feature = "tracing")]
221            tracing::trace!(target: TRACING_TARGET_CLIENT, trace_id = %trace_id, "Adding trace ID");
222
223            builder = builder.header("x-portkey-trace-id", trace_id);
224        }
225
226        if let Some(metadata) = self.inner.config.metadata() {
227            match serde_json::to_string(metadata) {
228                Ok(metadata_json) => {
229                    #[cfg(feature = "tracing")]
230                    tracing::trace!(target: TRACING_TARGET_CLIENT, "Adding metadata header");
231
232                    builder = builder.header("x-portkey-metadata", metadata_json);
233                }
234                Err(_e) => {
235                    #[cfg(feature = "tracing")]
236                    tracing::warn!(target: TRACING_TARGET_CLIENT, error = %_e, "Failed to serialize metadata, skipping header");
237                }
238            }
239        }
240
241        if let Some(cache_namespace) = self.inner.config.cache_namespace() {
242            #[cfg(feature = "tracing")]
243            tracing::trace!(target: TRACING_TARGET_CLIENT, cache_namespace = %cache_namespace, "Adding cache namespace");
244
245            builder = builder.header("x-portkey-cache-namespace", cache_namespace);
246        }
247
248        if let Some(cache_force_refresh) = self.inner.config.cache_force_refresh() {
249            #[cfg(feature = "tracing")]
250            tracing::trace!(target: TRACING_TARGET_CLIENT, cache_force_refresh, "Adding cache force refresh");
251
252            builder = builder.header(
253                "x-portkey-cache-force-refresh",
254                cache_force_refresh.to_string(),
255            );
256        }
257
258        builder
259    }
260
261    /// Parses the base URL and appends the given path.
262    fn parse_url(&self, path: &str) -> Result<url::Url> {
263        let mut url = url::Url::parse(self.inner.config.base_url())?;
264        url.set_path(&format!("{}{}", url.path().trim_end_matches('/'), path));
265        Ok(url)
266    }
267
268    /// Builds a URL with the given path and optional query parameters.
269    fn build_url(&self, path: &str, params: &[(&str, &str)]) -> Result<url::Url> {
270        let mut url = self.parse_url(path)?;
271
272        if !params.is_empty() {
273            url.query_pairs_mut().extend_pairs(params);
274        }
275
276        Ok(url)
277    }
278
279    /// Creates an HTTP request with the specified method.
280    fn request(&self, method: Method, url: url::Url) -> RequestBuilder {
281        #[cfg(feature = "tracing")]
282        tracing::trace!(
283            target: TRACING_TARGET_CLIENT,
284            url = %url,
285            method = %method,
286            "Creating HTTP request"
287        );
288
289        let builder = self
290            .inner
291            .client
292            .request(method, url)
293            .timeout(self.inner.config.timeout());
294
295        self.apply_portkey_headers(builder)
296    }
297
298    /// Sends a GET request and returns the response.
299    pub(crate) async fn send(&self, method: Method, path: &str) -> Result<Response> {
300        let url = self.parse_url(path)?;
301        let response = self.request(method, url).send().await?;
302        Ok(response)
303    }
304
305    /// Sends a request with JSON body.
306    pub(crate) async fn send_json<T: serde::Serialize>(
307        &self,
308        method: Method,
309        path: &str,
310        data: &T,
311    ) -> Result<Response> {
312        let url = self.parse_url(path)?;
313        let response = self.request(method, url).json(data).send().await?;
314        Ok(response)
315    }
316
317    /// Sends a request with query parameters.
318    pub(crate) async fn send_with_params(
319        &self,
320        method: Method,
321        path: &str,
322        params: &[(&str, &str)],
323    ) -> Result<Response> {
324        let url = self.build_url(path, params)?;
325        let response = self.request(method, url).send().await?;
326        Ok(response)
327    }
328
329    /// Sends a request with multipart form data.
330    pub(crate) async fn send_multipart(
331        &self,
332        method: Method,
333        path: &str,
334        form: Form,
335    ) -> Result<Response> {
336        let url = self.parse_url(path)?;
337        let response = self.request(method, url).multipart(form).send().await?;
338        Ok(response)
339    }
340
341    /// Creates a request builder for custom query parameter building.
342    /// Use this for complex query scenarios that need conditional parameters.
343    pub(crate) fn request_builder(&self, method: Method, path: &str) -> Result<RequestBuilder> {
344        let url = self.parse_url(path)?;
345        Ok(self.request(method, url))
346    }
347}
348
349impl fmt::Debug for PortkeyClient {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        f.debug_struct("PortkeyClient")
352            .field("api_key", &self.inner.config.masked_api_key())
353            .field("base_url", &self.inner.config.base_url())
354            .field("timeout", &self.inner.config.timeout())
355            .finish()
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use std::time::Duration;
362
363    use super::*;
364
365    fn create_test_config() -> PortkeyConfig {
366        PortkeyConfig::builder()
367            .with_api_key("test_api_key")
368            .with_auth_method(AuthMethod::VirtualKey {
369                virtual_key: "test_virtual_key".to_string(),
370            })
371            .build()
372            .unwrap()
373    }
374
375    #[test]
376    fn test_client_creation() -> Result<()> {
377        let config = create_test_config();
378        let client = PortkeyClient::new(config)?;
379
380        assert_eq!(client.inner.config.api_key(), "test_api_key");
381        assert_eq!(client.inner.config.base_url(), "https://api.portkey.ai/v1");
382
383        Ok(())
384    }
385
386    #[test]
387    fn test_client_creation_with_custom_config() -> Result<()> {
388        let config = PortkeyConfig::builder()
389            .with_api_key("custom_key")
390            .with_auth_method(AuthMethod::ProviderAuth {
391                provider: "openai".to_string(),
392                authorization: "Bearer sk-test".to_string(),
393                custom_host: None,
394            })
395            .with_base_url("https://custom.api.com")
396            .with_timeout(Duration::from_secs(60))
397            .build()?;
398
399        let client = PortkeyClient::new(config)?;
400
401        assert_eq!(client.inner.config.api_key(), "custom_key");
402        assert_eq!(client.inner.config.base_url(), "https://custom.api.com");
403        assert_eq!(client.inner.config.timeout(), Duration::from_secs(60));
404
405        Ok(())
406    }
407
408    #[test]
409    fn test_client_clone() -> Result<()> {
410        let config = create_test_config();
411        let client = PortkeyClient::new(config)?;
412        let cloned = client.clone();
413
414        assert_eq!(client.inner.config.api_key(), cloned.inner.config.api_key());
415        assert_eq!(
416            client.inner.config.base_url(),
417            cloned.inner.config.base_url()
418        );
419
420        Ok(())
421    }
422
423    #[test]
424    fn test_builder_convenience_method() -> Result<()> {
425        let client = PortkeyClient::builder()
426            .with_api_key("test_key")
427            .with_auth_method(AuthMethod::VirtualKey {
428                virtual_key: "test_vk".to_string(),
429            })
430            .build_client()?;
431
432        assert_eq!(client.inner.config.api_key(), "test_key");
433
434        Ok(())
435    }
436
437    #[test]
438    fn test_debug_impl_masks_api_key() -> Result<()> {
439        let config = PortkeyConfig::builder()
440            .with_api_key("secret_api_key_12345")
441            .with_auth_method(AuthMethod::VirtualKey {
442                virtual_key: "vk-123".to_string(),
443            })
444            .build()?;
445
446        let client = PortkeyClient::new(config)?;
447        let debug_output = format!("{:?}", client);
448
449        assert!(debug_output.contains("secr****"));
450        assert!(!debug_output.contains("secret_api_key_12345"));
451
452        Ok(())
453    }
454
455    #[test]
456    fn test_auth_method_virtual_key() -> Result<()> {
457        let config = PortkeyConfig::builder()
458            .with_api_key("test_key")
459            .with_auth_method(AuthMethod::VirtualKey {
460                virtual_key: "vk-test".to_string(),
461            })
462            .build()?;
463
464        let client = PortkeyClient::new(config)?;
465
466        matches!(
467            client.inner.config.auth_method(),
468            AuthMethod::VirtualKey { virtual_key } if virtual_key == "vk-test"
469        );
470
471        Ok(())
472    }
473
474    #[test]
475    fn test_auth_method_provider_auth() -> Result<()> {
476        let config = PortkeyConfig::builder()
477            .with_api_key("test_key")
478            .with_auth_method(AuthMethod::ProviderAuth {
479                provider: "anthropic".to_string(),
480                authorization: "Bearer token".to_string(),
481                custom_host: Some("https://custom.host".to_string()),
482            })
483            .build()?;
484
485        let client = PortkeyClient::new(config)?;
486
487        matches!(
488            client.inner.config.auth_method(),
489            AuthMethod::ProviderAuth { provider, .. } if provider == "anthropic"
490        );
491
492        Ok(())
493    }
494
495    #[test]
496    fn test_auth_method_config() -> Result<()> {
497        let config = PortkeyConfig::builder()
498            .with_api_key("test_key")
499            .with_auth_method(AuthMethod::Config {
500                config_id: "pc-123".to_string(),
501            })
502            .build()?;
503
504        let client = PortkeyClient::new(config)?;
505
506        matches!(
507            client.inner.config.auth_method(),
508            AuthMethod::Config { config_id } if config_id == "pc-123"
509        );
510
511        Ok(())
512    }
513
514    #[test]
515    fn test_optional_headers_config() -> Result<()> {
516        let mut metadata = std::collections::HashMap::new();
517        metadata.insert("key".to_string(), serde_json::json!("value"));
518
519        let config = PortkeyConfig::builder()
520            .with_api_key("test_key")
521            .with_auth_method(AuthMethod::VirtualKey {
522                virtual_key: "vk-test".to_string(),
523            })
524            .with_trace_id("trace-123")
525            .with_metadata(metadata)
526            .with_cache_namespace("my-cache")
527            .with_cache_force_refresh(true)
528            .build()?;
529
530        let client = PortkeyClient::new(config)?;
531
532        assert_eq!(client.inner.config.trace_id(), Some("trace-123"));
533        assert_eq!(client.inner.config.cache_namespace(), Some("my-cache"));
534        assert_eq!(client.inner.config.cache_force_refresh(), Some(true));
535        assert!(client.inner.config.metadata().is_some());
536
537        Ok(())
538    }
539}