Skip to main content

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}