1use 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#[derive(Clone)]
85pub struct PortkeyClient {
86 pub(crate) inner: Arc<PortkeyClientInner>,
87}
88
89#[derive(Debug)]
91pub(crate) struct PortkeyClientInner {
92 pub(crate) config: PortkeyConfig,
93 pub(crate) client: Client,
94}
95
96impl PortkeyClient {
97 #[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 pub fn builder() -> crate::builder::PortkeyBuilder {
145 PortkeyConfig::builder()
146 }
147
148 #[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 #[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 builder = builder.header("x-portkey-api-key", self.inner.config.api_key());
187
188 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 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 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 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 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 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 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 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 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 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}