openai_tools/common/auth.rs
1//! Authentication abstraction for OpenAI and Azure OpenAI APIs
2//!
3//! This module provides a unified interface for authenticating with both
4//! OpenAI API and Azure OpenAI API. It handles the differences in:
5//! - Header format (Bearer token vs api-key)
6//! - Endpoint URL construction
7//! - Environment variable names
8//!
9//! # Quick Start
10//!
11//! ## OpenAI API (existing users - no changes needed)
12//!
13//! ```rust,no_run
14//! use openai_tools::common::auth::AuthProvider;
15//!
16//! // From environment variable OPENAI_API_KEY
17//! let auth = AuthProvider::openai_from_env()?;
18//! # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
19//! ```
20//!
21//! ## Azure OpenAI API
22//!
23//! ```rust,no_run
24//! use openai_tools::common::auth::{AuthProvider, AzureAuth};
25//!
26//! // From environment variables (AZURE_OPENAI_API_KEY, AZURE_OPENAI_BASE_URL)
27//! let auth = AuthProvider::azure_from_env()?;
28//!
29//! // Or explicit configuration with complete base URL
30//! let auth = AuthProvider::Azure(
31//! AzureAuth::new(
32//! "your-api-key",
33//! "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview"
34//! )
35//! );
36//! # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
37//! ```
38//!
39//! ## URL-based Provider Detection
40//!
41//! Use `from_url_with_key` to auto-detect the provider based on URL pattern:
42//!
43//! ```rust
44//! use openai_tools::common::auth::AuthProvider;
45//!
46//! // Azure URL detected automatically (*.openai.azure.com)
47//! let auth = AuthProvider::from_url_with_key(
48//! "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview",
49//! "your-api-key"
50//! );
51//!
52//! // OpenAI-compatible URL (non-Azure URLs)
53//! let auth = AuthProvider::from_url_with_key(
54//! "http://localhost:11434/v1", // Ollama, vLLM, etc.
55//! "ollama"
56//! );
57//! ```
58//!
59//! ## Auto-detection from Environment
60//!
61//! ```rust,no_run
62//! use openai_tools::common::auth::AuthProvider;
63//!
64//! // Uses Azure if AZURE_OPENAI_API_KEY is set, otherwise OpenAI
65//! let auth = AuthProvider::from_env()?;
66//! # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
67//! ```
68
69use crate::common::errors::{OpenAIToolError, Result};
70use dotenvy::dotenv;
71use request::header::{HeaderMap, HeaderValue};
72use std::env;
73
74/// Default OpenAI API base URL
75const OPENAI_DEFAULT_BASE_URL: &str = "https://api.openai.com/v1";
76
77/// Authentication provider for OpenAI APIs
78///
79/// This enum encapsulates the authentication strategy for different API providers.
80/// It handles both endpoint URL construction and HTTP header application.
81///
82/// # Example
83///
84/// ```rust,no_run
85/// use openai_tools::common::auth::AuthProvider;
86///
87/// // Auto-detect from environment
88/// let auth = AuthProvider::from_env()?;
89///
90/// // Get endpoint for chat completions
91/// let endpoint = auth.endpoint("chat/completions");
92///
93/// // Apply auth headers to a request
94/// let mut headers = request::header::HeaderMap::new();
95/// auth.apply_headers(&mut headers)?;
96/// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
97/// ```
98#[derive(Debug, Clone)]
99pub enum AuthProvider {
100 /// OpenAI API authentication
101 OpenAI(OpenAIAuth),
102 /// Azure OpenAI API authentication
103 Azure(AzureAuth),
104}
105
106/// OpenAI API authentication configuration
107///
108/// Handles authentication for the standard OpenAI API using Bearer tokens.
109///
110/// # Header Format
111///
112/// ```text
113/// Authorization: Bearer sk-...
114/// ```
115///
116/// # Endpoint Format
117///
118/// ```text
119/// https://api.openai.com/v1/{path}
120/// ```
121#[derive(Debug, Clone)]
122pub struct OpenAIAuth {
123 /// The API key (sk-...)
124 api_key: String,
125 /// Base URL for API requests (default: https://api.openai.com/v1)
126 base_url: String,
127}
128
129impl OpenAIAuth {
130 /// Creates a new OpenAI authentication configuration
131 ///
132 /// # Arguments
133 ///
134 /// * `api_key` - OpenAI API key (typically starts with "sk-")
135 ///
136 /// # Returns
137 ///
138 /// A new `OpenAIAuth` instance with default base URL
139 ///
140 /// # Example
141 ///
142 /// ```rust
143 /// use openai_tools::common::auth::OpenAIAuth;
144 ///
145 /// let auth = OpenAIAuth::new("sk-your-api-key");
146 /// ```
147 pub fn new<T: Into<String>>(api_key: T) -> Self {
148 Self { api_key: api_key.into(), base_url: OPENAI_DEFAULT_BASE_URL.to_string() }
149 }
150
151 /// Sets a custom base URL
152 ///
153 /// Use this for proxies or alternative OpenAI-compatible APIs.
154 ///
155 /// # Arguments
156 ///
157 /// * `url` - Custom base URL (without trailing slash)
158 ///
159 /// # Returns
160 ///
161 /// Self for method chaining
162 ///
163 /// # Example
164 ///
165 /// ```rust
166 /// use openai_tools::common::auth::OpenAIAuth;
167 ///
168 /// let auth = OpenAIAuth::new("sk-key")
169 /// .with_base_url("https://my-proxy.example.com/v1");
170 /// ```
171 pub fn with_base_url<T: Into<String>>(mut self, url: T) -> Self {
172 self.base_url = url.into();
173 self
174 }
175
176 /// Returns the API key
177 pub fn api_key(&self) -> &str {
178 &self.api_key
179 }
180
181 /// Returns the base URL
182 pub fn base_url(&self) -> &str {
183 &self.base_url
184 }
185
186 /// Constructs the full endpoint URL for a given path
187 ///
188 /// # Arguments
189 ///
190 /// * `path` - API path (e.g., "chat/completions")
191 ///
192 /// # Returns
193 ///
194 /// Full URL string
195 fn endpoint(&self, path: &str) -> String {
196 format!("{}/{}", self.base_url.trim_end_matches('/'), path.trim_start_matches('/'))
197 }
198
199 /// Applies authentication headers to a request
200 ///
201 /// Adds the `Authorization: Bearer {key}` header.
202 fn apply_headers(&self, headers: &mut HeaderMap) -> Result<()> {
203 headers.insert(
204 "Authorization",
205 HeaderValue::from_str(&format!("Bearer {}", self.api_key)).map_err(|e| OpenAIToolError::Error(format!("Invalid header value: {}", e)))?,
206 );
207 Ok(())
208 }
209}
210
211/// Azure OpenAI API authentication configuration
212///
213/// Handles authentication for Azure OpenAI Service, which uses different
214/// header names and endpoint URL patterns than the standard OpenAI API.
215///
216/// # Header Format
217///
218/// ```text
219/// api-key: {key}
220/// ```
221///
222/// # Endpoint Format
223///
224/// The `base_url` should be a complete endpoint URL including deployment path,
225/// API path (e.g., `/chat/completions`), and query parameters (e.g., `?api-version=...`).
226/// The `endpoint()` method returns this URL as-is.
227///
228/// # Example
229///
230/// ```rust
231/// use openai_tools::common::auth::AzureAuth;
232///
233/// // For Chat API
234/// let auth = AzureAuth::new(
235/// "your-api-key",
236/// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview"
237/// );
238///
239/// // For Embedding API
240/// let auth = AzureAuth::new(
241/// "your-api-key",
242/// "https://my-resource.openai.azure.com/openai/deployments/text-embedding/embeddings?api-version=2024-08-01-preview"
243/// );
244/// ```
245#[derive(Debug, Clone)]
246pub struct AzureAuth {
247 /// API key
248 api_key: String,
249 /// Complete endpoint URL for API requests
250 base_url: String,
251}
252
253impl AzureAuth {
254 /// Creates a new Azure OpenAI authentication configuration
255 ///
256 /// # Arguments
257 ///
258 /// * `api_key` - Azure OpenAI API key
259 /// * `base_url` - Complete endpoint URL including deployment path, API path, and api-version
260 ///
261 /// # Returns
262 ///
263 /// A new `AzureAuth` instance
264 ///
265 /// # Example
266 ///
267 /// ```rust
268 /// use openai_tools::common::auth::AzureAuth;
269 ///
270 /// // Complete endpoint URL for Chat API
271 /// let auth = AzureAuth::new(
272 /// "your-api-key",
273 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview"
274 /// );
275 /// ```
276 pub fn new<T: Into<String>>(api_key: T, base_url: T) -> Self {
277 Self { api_key: api_key.into(), base_url: base_url.into() }
278 }
279
280 /// Returns the API key
281 pub fn api_key(&self) -> &str {
282 &self.api_key
283 }
284
285 /// Returns the base URL
286 pub fn base_url(&self) -> &str {
287 &self.base_url
288 }
289
290 /// Returns the endpoint URL
291 ///
292 /// # Arguments
293 ///
294 /// * `_path` - Ignored (for API compatibility with OpenAIAuth)
295 ///
296 /// # Returns
297 ///
298 /// The complete base URL as-is
299 fn endpoint(&self, _path: &str) -> String {
300 self.base_url.clone()
301 }
302
303 /// Applies authentication headers to a request
304 ///
305 /// Uses `api-key` header for Azure OpenAI authentication.
306 fn apply_headers(&self, headers: &mut HeaderMap) -> Result<()> {
307 headers.insert("api-key", HeaderValue::from_str(&self.api_key).map_err(|e| OpenAIToolError::Error(format!("Invalid header value: {}", e)))?);
308 Ok(())
309 }
310}
311
312impl AuthProvider {
313 /// Creates an OpenAI authentication provider from environment variables
314 ///
315 /// Reads the API key from `OPENAI_API_KEY` environment variable.
316 ///
317 /// # Returns
318 ///
319 /// `Result<AuthProvider>` - OpenAI auth provider or error if env var not set
320 ///
321 /// # Environment Variables
322 ///
323 /// | Variable | Required | Description |
324 /// |----------|----------|-------------|
325 /// | `OPENAI_API_KEY` | Yes | OpenAI API key |
326 ///
327 /// # Example
328 ///
329 /// ```rust,no_run
330 /// use openai_tools::common::auth::AuthProvider;
331 ///
332 /// let auth = AuthProvider::openai_from_env()?;
333 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
334 /// ```
335 pub fn openai_from_env() -> Result<Self> {
336 dotenv().ok();
337 let api_key = env::var("OPENAI_API_KEY").map_err(|_| OpenAIToolError::Error("OPENAI_API_KEY environment variable not set".into()))?;
338 Ok(Self::OpenAI(OpenAIAuth::new(api_key)))
339 }
340
341 /// Creates an Azure OpenAI authentication provider from environment variables
342 ///
343 /// # Returns
344 ///
345 /// `Result<AuthProvider>` - Azure auth provider or error if required vars not set
346 ///
347 /// # Environment Variables
348 ///
349 /// | Variable | Required | Description |
350 /// |----------|----------|-------------|
351 /// | `AZURE_OPENAI_API_KEY` | Yes | Azure API key |
352 /// | `AZURE_OPENAI_BASE_URL` | Yes | Complete endpoint URL including deployment, API path, and api-version |
353 ///
354 /// # Example
355 ///
356 /// ```rust,no_run
357 /// use openai_tools::common::auth::AuthProvider;
358 ///
359 /// // With environment variables:
360 /// // AZURE_OPENAI_API_KEY=xxx
361 /// // AZURE_OPENAI_BASE_URL=https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview
362 /// let auth = AuthProvider::azure_from_env()?;
363 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
364 /// ```
365 pub fn azure_from_env() -> Result<Self> {
366 dotenv().ok();
367
368 // Get API key
369 let api_key =
370 env::var("AZURE_OPENAI_API_KEY").map_err(|_| OpenAIToolError::Error("AZURE_OPENAI_API_KEY environment variable not set".into()))?;
371
372 // Get base URL (required)
373 let base_url =
374 env::var("AZURE_OPENAI_BASE_URL").map_err(|_| OpenAIToolError::Error("AZURE_OPENAI_BASE_URL environment variable not set".into()))?;
375
376 Ok(Self::Azure(AzureAuth::new(api_key, base_url)))
377 }
378
379 /// Creates an authentication provider by auto-detecting from environment
380 ///
381 /// Tries Azure first (if `AZURE_OPENAI_API_KEY` is set), then falls back to OpenAI.
382 ///
383 /// # Returns
384 ///
385 /// `Result<AuthProvider>` - Detected auth provider or error
386 ///
387 /// # Detection Order
388 ///
389 /// 1. If `AZURE_OPENAI_API_KEY` is set → Azure
390 /// 2. If `OPENAI_API_KEY` is set → OpenAI
391 /// 3. Otherwise → Error
392 ///
393 /// # Example
394 ///
395 /// ```rust,no_run
396 /// use openai_tools::common::auth::AuthProvider;
397 ///
398 /// let auth = AuthProvider::from_env()?;
399 /// match &auth {
400 /// AuthProvider::OpenAI(_) => println!("Using OpenAI"),
401 /// AuthProvider::Azure(_) => println!("Using Azure"),
402 /// }
403 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
404 /// ```
405 pub fn from_env() -> Result<Self> {
406 dotenv().ok();
407
408 // Try Azure first if its key is present
409 if env::var("AZURE_OPENAI_API_KEY").is_ok() {
410 return Self::azure_from_env();
411 }
412
413 // Fall back to OpenAI
414 Self::openai_from_env()
415 }
416
417 /// Constructs the full endpoint URL for a given API path
418 ///
419 /// # Arguments
420 ///
421 /// * `path` - API path (e.g., "chat/completions", "embeddings")
422 ///
423 /// # Returns
424 ///
425 /// Full endpoint URL appropriate for the provider
426 ///
427 /// # Example
428 ///
429 /// ```rust,no_run
430 /// use openai_tools::common::auth::AuthProvider;
431 ///
432 /// let auth = AuthProvider::openai_from_env()?;
433 /// let url = auth.endpoint("chat/completions");
434 /// // OpenAI: https://api.openai.com/v1/chat/completions
435 /// // Azure: https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={ver}
436 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
437 /// ```
438 pub fn endpoint(&self, path: &str) -> String {
439 match self {
440 Self::OpenAI(auth) => auth.endpoint(path),
441 Self::Azure(auth) => auth.endpoint(path),
442 }
443 }
444
445 /// Applies authentication headers to a request
446 ///
447 /// # Arguments
448 ///
449 /// * `headers` - Mutable reference to header map
450 ///
451 /// # Returns
452 ///
453 /// `Result<()>` - Success or error if header value is invalid
454 ///
455 /// # Header Differences
456 ///
457 /// | Provider | Header Name | Value Format |
458 /// |----------|-------------|--------------|
459 /// | OpenAI | `Authorization` | `Bearer {key}` |
460 /// | Azure | `api-key` | `{key}` |
461 pub fn apply_headers(&self, headers: &mut HeaderMap) -> Result<()> {
462 match self {
463 Self::OpenAI(auth) => auth.apply_headers(headers),
464 Self::Azure(auth) => auth.apply_headers(headers),
465 }
466 }
467
468 /// Returns the API key (for backward compatibility)
469 ///
470 /// # Returns
471 ///
472 /// The API key or token string
473 pub fn api_key(&self) -> &str {
474 match self {
475 Self::OpenAI(auth) => auth.api_key(),
476 Self::Azure(auth) => auth.api_key(),
477 }
478 }
479
480 /// Returns whether this is an Azure provider
481 ///
482 /// # Returns
483 ///
484 /// `true` if Azure, `false` if OpenAI
485 pub fn is_azure(&self) -> bool {
486 matches!(self, Self::Azure(_))
487 }
488
489 /// Returns whether this is an OpenAI provider
490 ///
491 /// # Returns
492 ///
493 /// `true` if OpenAI, `false` if Azure
494 pub fn is_openai(&self) -> bool {
495 matches!(self, Self::OpenAI(_))
496 }
497
498 /// Creates an authentication provider by detecting the provider from URL pattern.
499 ///
500 /// This method analyzes the URL to determine whether it's an Azure OpenAI endpoint
501 /// or a standard OpenAI (or compatible) endpoint.
502 ///
503 /// # Arguments
504 ///
505 /// * `base_url` - The complete base URL for API requests
506 /// * `api_key` - The API key or token
507 ///
508 /// # URL Detection
509 ///
510 /// | URL Pattern | Detected Provider |
511 /// |-------------|-------------------|
512 /// | `*.openai.azure.com*` | Azure |
513 /// | All others | OpenAI (or compatible) |
514 ///
515 /// # Returns
516 ///
517 /// `AuthProvider` - Detected provider
518 ///
519 /// # Example
520 ///
521 /// ```rust
522 /// use openai_tools::common::auth::AuthProvider;
523 ///
524 /// // OpenAI compatible API
525 /// let openai = AuthProvider::from_url_with_key(
526 /// "https://api.openai.com/v1",
527 /// "sk-key",
528 /// );
529 /// assert!(openai.is_openai());
530 ///
531 /// // Azure OpenAI (complete base URL)
532 /// let azure = AuthProvider::from_url_with_key(
533 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview",
534 /// "azure-key",
535 /// );
536 /// assert!(azure.is_azure());
537 ///
538 /// // Local API (Ollama, LocalAI, etc.)
539 /// let local = AuthProvider::from_url_with_key(
540 /// "http://localhost:11434/v1",
541 /// "dummy-key",
542 /// );
543 /// assert!(local.is_openai()); // Treated as OpenAI-compatible
544 /// ```
545 pub fn from_url_with_key<S: Into<String>>(base_url: S, api_key: S) -> Self {
546 let url_str = base_url.into();
547 let api_key_str = api_key.into();
548
549 // Check if URL matches Azure pattern
550 if url_str.contains(".openai.azure.com") {
551 Self::Azure(AzureAuth::new(api_key_str, url_str))
552 } else {
553 // OpenAI or compatible API
554 Self::OpenAI(OpenAIAuth::new(api_key_str).with_base_url(url_str))
555 }
556 }
557
558 /// Creates an authentication provider from URL, using environment variables for credentials.
559 ///
560 /// This is a convenience method that combines URL-based detection with
561 /// environment variable-based credential loading.
562 ///
563 /// # Arguments
564 ///
565 /// * `base_url` - The complete base URL for API requests
566 ///
567 /// # Environment Variables
568 ///
569 /// For Azure URLs (`*.openai.azure.com`):
570 /// - `AZURE_OPENAI_API_KEY` (required)
571 ///
572 /// For other URLs:
573 /// - `OPENAI_API_KEY` (required)
574 ///
575 /// # Example
576 ///
577 /// ```rust,no_run
578 /// use openai_tools::common::auth::AuthProvider;
579 ///
580 /// // Uses OPENAI_API_KEY from environment
581 /// let auth = AuthProvider::from_url("https://api.openai.com/v1")?;
582 ///
583 /// // Uses AZURE_OPENAI_API_KEY from environment
584 /// let azure = AuthProvider::from_url(
585 /// "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview"
586 /// )?;
587 /// # Ok::<(), openai_tools::common::errors::OpenAIToolError>(())
588 /// ```
589 pub fn from_url<S: Into<String>>(base_url: S) -> Result<Self> {
590 let url_str = base_url.into();
591 dotenv().ok();
592
593 if url_str.contains(".openai.azure.com") {
594 // Azure: get credentials from Azure env vars
595 let api_key = env::var("AZURE_OPENAI_API_KEY")
596 .map_err(|_| OpenAIToolError::Error("Azure URL detected but AZURE_OPENAI_API_KEY is not set".into()))?;
597
598 Ok(Self::Azure(AzureAuth::new(api_key, url_str)))
599 } else {
600 // OpenAI: get credentials from OpenAI env var
601 let api_key = env::var("OPENAI_API_KEY").map_err(|_| OpenAIToolError::Error("OPENAI_API_KEY environment variable not set".into()))?;
602
603 Ok(Self::OpenAI(OpenAIAuth::new(api_key).with_base_url(url_str)))
604 }
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 // OpenAI Auth Tests
613
614 #[test]
615 fn test_openai_auth_new() {
616 let auth = OpenAIAuth::new("sk-test-key");
617 assert_eq!(auth.api_key(), "sk-test-key");
618 assert_eq!(auth.base_url(), OPENAI_DEFAULT_BASE_URL);
619 }
620
621 #[test]
622 fn test_openai_auth_with_base_url() {
623 let auth = OpenAIAuth::new("sk-test-key").with_base_url("https://custom.example.com/v1");
624 assert_eq!(auth.base_url(), "https://custom.example.com/v1");
625 }
626
627 #[test]
628 fn test_openai_endpoint() {
629 let auth = OpenAIAuth::new("sk-test-key");
630 assert_eq!(auth.endpoint("chat/completions"), "https://api.openai.com/v1/chat/completions");
631 assert_eq!(auth.endpoint("/chat/completions"), "https://api.openai.com/v1/chat/completions");
632 }
633
634 #[test]
635 fn test_openai_apply_headers() {
636 let auth = OpenAIAuth::new("sk-test-key");
637 let mut headers = HeaderMap::new();
638 auth.apply_headers(&mut headers).unwrap();
639
640 assert_eq!(headers.get("Authorization").unwrap(), "Bearer sk-test-key");
641 }
642
643 #[test]
644 fn test_openai_endpoint_trailing_slash_handling() {
645 let auth = OpenAIAuth::new("key").with_base_url("https://example.com/v1/");
646 assert_eq!(auth.endpoint("chat/completions"), "https://example.com/v1/chat/completions");
647 assert_eq!(auth.endpoint("/chat/completions"), "https://example.com/v1/chat/completions");
648 }
649
650 // Azure Auth Tests (Simplified API - base_url only)
651
652 #[test]
653 fn test_azure_auth_new() {
654 let auth = AzureAuth::new(
655 "api-key",
656 "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview",
657 );
658 assert_eq!(auth.api_key(), "api-key");
659 assert_eq!(auth.base_url(), "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview");
660 }
661
662 #[test]
663 fn test_azure_endpoint_returns_base_url() {
664 // Azure endpoint() returns base_url as-is (path is ignored)
665 let auth =
666 AzureAuth::new("key", "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview");
667 let endpoint = auth.endpoint("ignored");
668 assert_eq!(endpoint, "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview");
669 }
670
671 #[test]
672 fn test_azure_apply_headers() {
673 let auth = AzureAuth::new("my-api-key", "https://my-resource.openai.azure.com");
674 let mut headers = HeaderMap::new();
675 auth.apply_headers(&mut headers).unwrap();
676
677 assert_eq!(headers.get("api-key").unwrap(), "my-api-key");
678 assert!(headers.get("Authorization").is_none());
679 }
680
681 // AuthProvider Tests
682
683 #[test]
684 fn test_auth_provider_openai() {
685 let auth = AuthProvider::OpenAI(OpenAIAuth::new("sk-key"));
686 assert!(auth.is_openai());
687 assert!(!auth.is_azure());
688 assert_eq!(auth.api_key(), "sk-key");
689 }
690
691 #[test]
692 fn test_auth_provider_azure() {
693 let auth = AuthProvider::Azure(AzureAuth::new("key", "https://my-resource.openai.azure.com/openai/deployments/gpt-4o"));
694 assert!(auth.is_azure());
695 assert!(!auth.is_openai());
696 assert_eq!(auth.api_key(), "key");
697 }
698
699 #[test]
700 fn test_auth_provider_endpoint_openai() {
701 let auth = AuthProvider::OpenAI(OpenAIAuth::new("key"));
702 assert_eq!(auth.endpoint("chat/completions"), "https://api.openai.com/v1/chat/completions");
703 }
704
705 #[test]
706 fn test_auth_provider_endpoint_azure() {
707 // Azure endpoint returns base_url as-is (path is ignored)
708 let base_url = "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview";
709 let auth = AuthProvider::Azure(AzureAuth::new("key", base_url));
710 let endpoint = auth.endpoint("ignored");
711 assert_eq!(endpoint, base_url);
712 }
713
714 #[test]
715 fn test_auth_provider_apply_headers() {
716 // OpenAI
717 let openai_auth = AuthProvider::OpenAI(OpenAIAuth::new("sk-key"));
718 let mut headers = HeaderMap::new();
719 openai_auth.apply_headers(&mut headers).unwrap();
720 assert!(headers.get("Authorization").unwrap().to_str().unwrap().starts_with("Bearer"));
721
722 // Azure
723 let azure_auth = AuthProvider::Azure(AzureAuth::new("azure-key", "https://my-resource.openai.azure.com"));
724 let mut headers = HeaderMap::new();
725 azure_auth.apply_headers(&mut headers).unwrap();
726 assert_eq!(headers.get("api-key").unwrap(), "azure-key");
727 }
728
729 #[test]
730 fn test_from_env_returns_correct_provider_type() {
731 // Test direct construction (which from_env uses internally)
732 let openai = AuthProvider::OpenAI(OpenAIAuth::new("sk-test"));
733 assert!(openai.is_openai());
734 assert!(!openai.is_azure());
735
736 let azure = AuthProvider::Azure(AzureAuth::new("key", "https://my-resource.openai.azure.com/openai/deployments/gpt-4o"));
737 assert!(azure.is_azure());
738 assert!(!azure.is_openai());
739 }
740
741 // URL-based provider detection tests (from_url_with_key)
742
743 #[test]
744 fn test_from_url_with_key_openai_api() {
745 let auth = AuthProvider::from_url_with_key("https://api.openai.com/v1", "sk-test-key");
746
747 assert!(auth.is_openai());
748 assert!(!auth.is_azure());
749 assert_eq!(auth.api_key(), "sk-test-key");
750 assert_eq!(auth.endpoint("chat/completions"), "https://api.openai.com/v1/chat/completions");
751 }
752
753 #[test]
754 fn test_from_url_with_key_azure() {
755 let base_url = "https://my-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-08-01-preview";
756 let auth = AuthProvider::from_url_with_key(base_url, "azure-api-key");
757
758 assert!(auth.is_azure());
759 assert!(!auth.is_openai());
760 assert_eq!(auth.api_key(), "azure-api-key");
761
762 // Azure endpoint returns base_url as-is (path is ignored)
763 let endpoint = auth.endpoint("ignored");
764 assert_eq!(endpoint, base_url);
765 }
766
767 #[test]
768 fn test_from_url_with_key_local_api_ollama() {
769 // Local APIs like Ollama should be treated as OpenAI-compatible
770 let auth = AuthProvider::from_url_with_key("http://localhost:11434/v1", "ollama");
771
772 assert!(auth.is_openai());
773 assert_eq!(auth.endpoint("chat/completions"), "http://localhost:11434/v1/chat/completions");
774 }
775
776 #[test]
777 fn test_from_url_with_key_custom_openai_compatible() {
778 // Custom OpenAI-compatible endpoints (e.g., vLLM, LocalAI)
779 let auth = AuthProvider::from_url_with_key("https://my-proxy.example.com/openai/v1", "proxy-key");
780
781 assert!(auth.is_openai());
782 assert_eq!(auth.endpoint("embeddings"), "https://my-proxy.example.com/openai/v1/embeddings");
783 }
784
785 #[test]
786 fn test_from_url_with_key_azure_various_patterns() {
787 // Test various Azure URL patterns
788 let patterns = [
789 "https://eastus.openai.azure.com/openai/deployments/gpt-4o",
790 "https://my-company-resource.openai.azure.com/openai/deployments/gpt-4o",
791 "https://test.openai.azure.com/openai/deployments/gpt-4o?api-version=2024-08-01-preview",
792 ];
793
794 for url in patterns {
795 let auth = AuthProvider::from_url_with_key(url, "key");
796 assert!(auth.is_azure(), "Should be Azure provider for URL: {}", url);
797 }
798 }
799
800 #[test]
801 fn test_from_url_with_key_headers_openai() {
802 let auth = AuthProvider::from_url_with_key("https://api.openai.com/v1", "sk-secret-key");
803
804 let mut headers = HeaderMap::new();
805 auth.apply_headers(&mut headers).unwrap();
806
807 assert_eq!(headers.get("Authorization").unwrap(), "Bearer sk-secret-key");
808 }
809
810 #[test]
811 fn test_from_url_with_key_headers_azure() {
812 let auth = AuthProvider::from_url_with_key("https://resource.openai.azure.com/openai/deployments/gpt-4o", "azure-secret");
813
814 let mut headers = HeaderMap::new();
815 auth.apply_headers(&mut headers).unwrap();
816
817 assert_eq!(headers.get("api-key").unwrap(), "azure-secret");
818 assert!(headers.get("Authorization").is_none());
819 }
820}