diff --git a/litellm-rust/crates/ai-gateway/src/ocr/hooks.rs b/litellm-rust/crates/ai-gateway/src/ocr/hooks.rs index c3b47ea96c6..806817abc0a 100644 --- a/litellm-rust/crates/ai-gateway/src/ocr/hooks.rs +++ b/litellm-rust/crates/ai-gateway/src/ocr/hooks.rs @@ -1,4 +1,3 @@ -use litellm_core::AuthError; use litellm_core::call_lifecycle::{CallLifecycleContext, CallLifecycleHooks, CallLifecycleTiming}; use litellm_core::error::Error; use litellm_core::ocr::prepare::map_ocr_params; @@ -196,16 +195,7 @@ async fn upload_reducto_document( upstream_headers: &[(String, String)], ) -> Result { let source = extract_document_source(document)?; - let Some(authorization) = upstream_headers - .iter() - .find(|(name, _)| name.eq_ignore_ascii_case("authorization")) - .map(|(_, value)| value.as_str()) - else { - return Err(Error::Auth(AuthError::Message( - "Reducto upload requires an Authorization header".to_string(), - ))); - }; - let Some(upload) = build_upload_request(source, authorization, api_base) else { + let Some(upload) = build_upload_request(source, api_base) else { return Ok(document.clone()); }; let part = reqwest::multipart::Part::bytes(upload.bytes) diff --git a/litellm-rust/crates/core/src/providers/auth/azure/credential_provider_cache.rs b/litellm-rust/crates/core/src/auth/azure/credential_provider_cache.rs similarity index 97% rename from litellm-rust/crates/core/src/providers/auth/azure/credential_provider_cache.rs rename to litellm-rust/crates/core/src/auth/azure/credential_provider_cache.rs index f857b9e3626..297e4cc6502 100644 --- a/litellm-rust/crates/core/src/providers/auth/azure/credential_provider_cache.rs +++ b/litellm-rust/crates/core/src/auth/azure/credential_provider_cache.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use azure_core::credentials::TokenCredential; use moka::future::Cache; -use crate::error::AuthError; +use crate::AuthError; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct AzureCredentialProviderCacheKey { diff --git a/litellm-rust/crates/core/src/providers/auth/azure/mod.rs b/litellm-rust/crates/core/src/auth/azure/mod.rs similarity index 100% rename from litellm-rust/crates/core/src/providers/auth/azure/mod.rs rename to litellm-rust/crates/core/src/auth/azure/mod.rs diff --git a/litellm-rust/crates/core/src/providers/auth/azure/native.rs b/litellm-rust/crates/core/src/auth/azure/native.rs similarity index 98% rename from litellm-rust/crates/core/src/providers/auth/azure/native.rs rename to litellm-rust/crates/core/src/auth/azure/native.rs index 682ba6eeb31..2b7a8153623 100644 --- a/litellm-rust/crates/core/src/providers/auth/azure/native.rs +++ b/litellm-rust/crates/core/src/auth/azure/native.rs @@ -12,9 +12,9 @@ use azure_identity::{ }; use sha2::{Digest, Sha256}; -use crate::error::AuthError; -use crate::providers::auth::secret::SecretValue; -use crate::providers::auth::token::ResolvedCredential; +use crate::AuthError; +use crate::auth::secret::SecretValue; +use crate::auth::token::ResolvedCredential; use super::credential_provider_cache::{ AzureCredentialProviderCache, AzureCredentialProviderCacheKey, @@ -296,7 +296,7 @@ mod tests { use azure_core::{Bytes, Result}; use super::{NativeAzureRequest, NativeAzureTokenAcquirer}; - use crate::providers::auth::secret::SecretValue; + use crate::auth::secret::SecretValue; #[derive(Debug, Default)] struct RecordingTokenClient { diff --git a/litellm-rust/crates/core/src/providers/auth/azure/resolve.rs b/litellm-rust/crates/core/src/auth/azure/resolve.rs similarity index 99% rename from litellm-rust/crates/core/src/providers/auth/azure/resolve.rs rename to litellm-rust/crates/core/src/auth/azure/resolve.rs index 96c7c71efeb..e9655d9f965 100644 --- a/litellm-rust/crates/core/src/providers/auth/azure/resolve.rs +++ b/litellm-rust/crates/core/src/auth/azure/resolve.rs @@ -1,5 +1,5 @@ -use crate::error::AuthError; -use crate::providers::auth::{ +use crate::AuthError; +use crate::auth::{ CredentialFileRef, CredentialLookup, CredentialRef, ResolvedCredential, SecretValue, TokenCallerHandle, }; @@ -392,8 +392,8 @@ mod tests { use serde_json::json; use super::{AzureCredentialPlan, oidc_reference, resolve_reference, select_auth_plan}; - use crate::providers::auth::azure::AzureAuthInputs; - use crate::providers::auth::{ + use crate::auth::azure::AzureAuthInputs; + use crate::auth::{ CredentialFileRef, CredentialLookup, CredentialLookupFuture, CredentialRef, CredentialResolver, CredentialResolverHandle, SecretValue, }; diff --git a/litellm-rust/crates/core/src/providers/auth/azure/types.rs b/litellm-rust/crates/core/src/auth/azure/types.rs similarity index 97% rename from litellm-rust/crates/core/src/providers/auth/azure/types.rs rename to litellm-rust/crates/core/src/auth/azure/types.rs index 4b52837b8f6..c0a4c761a1e 100644 --- a/litellm-rust/crates/core/src/providers/auth/azure/types.rs +++ b/litellm-rust/crates/core/src/auth/azure/types.rs @@ -1,8 +1,8 @@ use serde_json::{Map, Value}; use strum::EnumString; -use crate::error::AuthError; -use crate::providers::auth::{CredentialResolverHandle, SecretValue, TokenCallerHandle}; +use crate::AuthError; +use crate::auth::{CredentialResolverHandle, SecretValue, TokenCallerHandle}; pub const DEFAULT_AZURE_SCOPE: &str = "https://cognitiveservices.azure.com/.default"; diff --git a/litellm-rust/crates/core/src/providers/auth/credential.rs b/litellm-rust/crates/core/src/auth/credential.rs similarity index 99% rename from litellm-rust/crates/core/src/providers/auth/credential.rs rename to litellm-rust/crates/core/src/auth/credential.rs index 2f7ac710bda..9d4105c2500 100644 --- a/litellm-rust/crates/core/src/providers/auth/credential.rs +++ b/litellm-rust/crates/core/src/auth/credential.rs @@ -107,7 +107,7 @@ mod tests { CredentialRef, CredentialResolver, CredentialResolverHandle, }; use crate::AuthError; - use crate::providers::auth::SecretValue; + use crate::auth::SecretValue; #[derive(Debug)] struct HostResolver; diff --git a/litellm-rust/crates/core/src/providers/auth/http.rs b/litellm-rust/crates/core/src/auth/http.rs similarity index 98% rename from litellm-rust/crates/core/src/providers/auth/http.rs rename to litellm-rust/crates/core/src/auth/http.rs index 50f371b4c35..3dafa8a3217 100644 --- a/litellm-rust/crates/core/src/providers/auth/http.rs +++ b/litellm-rust/crates/core/src/auth/http.rs @@ -1,4 +1,4 @@ -use crate::error::AuthError; +use crate::AuthError; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CredentialPlacement { diff --git a/litellm-rust/crates/core/src/providers/auth/mod.rs b/litellm-rust/crates/core/src/auth/mod.rs similarity index 100% rename from litellm-rust/crates/core/src/providers/auth/mod.rs rename to litellm-rust/crates/core/src/auth/mod.rs diff --git a/litellm-rust/crates/core/src/providers/auth/policy.rs b/litellm-rust/crates/core/src/auth/policy.rs similarity index 96% rename from litellm-rust/crates/core/src/providers/auth/policy.rs rename to litellm-rust/crates/core/src/auth/policy.rs index e8451a4f66f..ea575a5177c 100644 --- a/litellm-rust/crates/core/src/providers/auth/policy.rs +++ b/litellm-rust/crates/core/src/auth/policy.rs @@ -1,4 +1,4 @@ -use crate::error::AuthError; +use crate::AuthError; use super::http::apply_credential; use super::{CredentialPlacement, ResolvedCredential}; @@ -70,7 +70,7 @@ impl ProviderAuthPolicy { #[cfg(test)] mod tests { use super::{CredentialPlanKind, CredentialRule, ExistingHeaderBehavior, ProviderAuthPolicy}; - use crate::providers::auth::{CredentialPlacement, ResolvedCredential, SecretValue}; + use crate::auth::{CredentialPlacement, ResolvedCredential, SecretValue}; const RULES: &[CredentialRule] = &[CredentialRule { kind: CredentialPlanKind::Static, diff --git a/litellm-rust/crates/core/src/providers/auth/secret.rs b/litellm-rust/crates/core/src/auth/secret.rs similarity index 97% rename from litellm-rust/crates/core/src/providers/auth/secret.rs rename to litellm-rust/crates/core/src/auth/secret.rs index 692cba061a0..3ecb0a835ee 100644 --- a/litellm-rust/crates/core/src/providers/auth/secret.rs +++ b/litellm-rust/crates/core/src/auth/secret.rs @@ -1,6 +1,6 @@ use veil::Redact; -#[derive(Clone, Redact)] +#[derive(Redact, Clone)] pub struct SecretValue(#[redact(with = "[REDACTED]")] String); impl SecretValue { diff --git a/litellm-rust/crates/core/src/providers/auth/token.rs b/litellm-rust/crates/core/src/auth/token.rs similarity index 97% rename from litellm-rust/crates/core/src/providers/auth/token.rs rename to litellm-rust/crates/core/src/auth/token.rs index bd4626675ae..922eb9a2041 100644 --- a/litellm-rust/crates/core/src/providers/auth/token.rs +++ b/litellm-rust/crates/core/src/auth/token.rs @@ -5,7 +5,7 @@ use std::time::SystemTime; use veil::Redact; -use crate::error::AuthError; +use crate::AuthError; use super::secret::SecretValue; diff --git a/litellm-rust/crates/core/src/lib.rs b/litellm-rust/crates/core/src/lib.rs index e3f5b4c12bc..dd613bd5925 100644 --- a/litellm-rust/crates/core/src/lib.rs +++ b/litellm-rust/crates/core/src/lib.rs @@ -1,4 +1,5 @@ pub mod audio_transcription; +pub mod auth; pub mod call_lifecycle; pub mod chat_completions; pub mod constants; diff --git a/litellm-rust/crates/core/src/ocr/transformation.rs b/litellm-rust/crates/core/src/ocr/transformation.rs index 5c3c0b239ed..77fbbfb7bba 100644 --- a/litellm-rust/crates/core/src/ocr/transformation.rs +++ b/litellm-rust/crates/core/src/ocr/transformation.rs @@ -1,5 +1,5 @@ -use crate::providers::auth::CredentialPlacement; -use crate::providers::auth::http::apply_credential; +use crate::auth::CredentialPlacement; +use crate::auth::http::apply_credential; use crate::{AuthError, Error}; use serde_json::{Map, Value}; diff --git a/litellm-rust/crates/core/src/ocr/types.rs b/litellm-rust/crates/core/src/ocr/types.rs index e582f982ce2..b4cbb8119fd 100644 --- a/litellm-rust/crates/core/src/ocr/types.rs +++ b/litellm-rust/crates/core/src/ocr/types.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use crate::providers::auth::azure::AzureAuthInputs; +use crate::auth::azure::AzureAuthInputs; #[derive(Clone, Debug, Default)] pub enum OcrAuthInputs { diff --git a/litellm-rust/crates/core/src/providers/azure_ai/auth.rs b/litellm-rust/crates/core/src/providers/azure_ai/auth.rs index 1b7f875c5c5..aec715d2d39 100644 --- a/litellm-rust/crates/core/src/providers/azure_ai/auth.rs +++ b/litellm-rust/crates/core/src/providers/azure_ai/auth.rs @@ -1,12 +1,12 @@ use std::sync::OnceLock; -use crate::error::{AuthError, Error}; -use crate::providers::auth::azure::{AzureAuthInputs, AzureAuthService}; -use crate::providers::auth::http::apply_credential; -use crate::providers::auth::{ +use crate::auth::azure::{AzureAuthInputs, AzureAuthService}; +use crate::auth::http::apply_credential; +use crate::auth::{ CredentialPlacement, CredentialPlanKind, CredentialRef, CredentialRule, ExistingHeaderBehavior, ProviderAuthPolicy, ResolvedCredential, SecretValue, }; +use crate::error::{AuthError, Error}; pub const AZURE_AI_API_KEY_ENV: &str = "AZURE_AI_API_KEY"; pub const AZURE_AI_API_BASE_ENV: &str = "AZURE_AI_API_BASE"; @@ -27,7 +27,7 @@ const AZURE_AI_AUTH_POLICY: ProviderAuthPolicy = ProviderAuthPolicy { rules: AZURE_AI_AUTH_RULES, accepted_existing_headers: &["Authorization"], existing_header_behavior: ExistingHeaderBehavior::Preserve, - scope: Some(crate::providers::auth::azure::DEFAULT_AZURE_SCOPE), + scope: Some(crate::auth::azure::DEFAULT_AZURE_SCOPE), audience: None, }; const DOCUMENT_INTELLIGENCE_AUTH_RULES: &[CredentialRule] = &[ @@ -44,7 +44,7 @@ const DOCUMENT_INTELLIGENCE_AUTH_POLICY: ProviderAuthPolicy = ProviderAuthPolicy rules: DOCUMENT_INTELLIGENCE_AUTH_RULES, accepted_existing_headers: &["Authorization", "Ocp-Apim-Subscription-Key"], existing_header_behavior: ExistingHeaderBehavior::Preserve, - scope: Some(crate::providers::auth::azure::DEFAULT_AZURE_SCOPE), + scope: Some(crate::auth::azure::DEFAULT_AZURE_SCOPE), audience: None, }; @@ -208,7 +208,7 @@ async fn authenticate_with_policy( async fn resolve_entra_credential( auth_inputs: Option<&AzureAuthInputs>, env_lookup: &(dyn Fn(&str) -> Option + Sync), -) -> Result, AuthError> { +) -> Result, AuthError> { match auth_inputs { Some(inputs) => auth_service().resolve(inputs, env_lookup).await, None => Ok(None), @@ -244,8 +244,8 @@ fn auth_service() -> &'static AzureAuthService { #[cfg(test)] mod tests { + use crate::auth::azure::AzureAuthInputs; use crate::error::{AuthError, Error}; - use crate::providers::auth::azure::AzureAuthInputs; use serde_json::json; use super::{ diff --git a/litellm-rust/crates/core/src/providers/azure_ai/ocr/transformation.rs b/litellm-rust/crates/core/src/providers/azure_ai/ocr/transformation.rs index d9b24ae4f82..a26e030e09b 100644 --- a/litellm-rust/crates/core/src/providers/azure_ai/ocr/transformation.rs +++ b/litellm-rust/crates/core/src/providers/azure_ai/ocr/transformation.rs @@ -1,11 +1,11 @@ use serde_json::{Map, Value, json}; use std::collections::BTreeSet; +use crate::auth::CredentialPlacement; +use crate::auth::azure::AzureAuthInputs; use crate::error::{AuthError, Error, json_type_name}; use crate::ocr::transformation::{OcrAuthFuture, OcrProviderConfig, OcrResponseHandling}; use crate::ocr::types::{OcrAuthInputs, OcrRequestData, OcrResponseData}; -use crate::providers::auth::CredentialPlacement; -use crate::providers::auth::azure::AzureAuthInputs; use crate::providers::azure_ai::auth; use crate::providers::mistral::ocr::transformation::MISTRAL_OCR_CONFIG; diff --git a/litellm-rust/crates/core/src/providers/mistral/auth.rs b/litellm-rust/crates/core/src/providers/mistral/auth.rs new file mode 100644 index 00000000000..4f9ce4f11d1 --- /dev/null +++ b/litellm-rust/crates/core/src/providers/mistral/auth.rs @@ -0,0 +1,102 @@ +use crate::auth::{ + CredentialPlacement, CredentialPlanKind, CredentialRule, ExistingHeaderBehavior, + ProviderAuthPolicy, ResolvedCredential, SecretValue, +}; +use crate::error::{AuthError, Error}; + +pub const MISTRAL_API_KEY_ENV: &str = "MISTRAL_API_KEY"; + +const AUTH_RULES: &[CredentialRule] = &[CredentialRule { + kind: CredentialPlanKind::Static, + placement: CredentialPlacement::Bearer, +}]; +const AUTH_POLICY: ProviderAuthPolicy = ProviderAuthPolicy { + rules: AUTH_RULES, + accepted_existing_headers: &["Authorization"], + existing_header_behavior: ExistingHeaderBehavior::Preserve, + scope: None, + audience: None, +}; + +pub fn resolve_api_key( + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> Result { + api_key + .map(str::trim) + .filter(|key| !key.is_empty()) + .map(str::to_string) + .or_else(|| env_lookup(MISTRAL_API_KEY_ENV).filter(|key| !key.trim().is_empty())) + .ok_or(Error::Auth(AuthError::MissingApiKey { + provider: "Mistral", + })) +} + +pub fn validate_environment( + headers: Vec<(String, String)>, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> Result, Error> { + if AUTH_POLICY.has_existing_credential(&headers) { + return Ok(headers); + } + let credential = + ResolvedCredential::Static(SecretValue::new(resolve_api_key(api_key, env_lookup)?)); + AUTH_POLICY + .apply(headers, CredentialPlanKind::Static, &credential) + .map_err(Error::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_key_precedes_environment_key() { + let env_lookup = |name: &str| (name == MISTRAL_API_KEY_ENV).then(|| "sk-env".to_string()); + + assert_eq!( + resolve_api_key(Some("sk-param"), &env_lookup).unwrap(), + "sk-param" + ); + } + + #[test] + fn blank_explicit_key_uses_environment_key() { + let env_lookup = |name: &str| (name == MISTRAL_API_KEY_ENV).then(|| "sk-env".to_string()); + + assert_eq!(resolve_api_key(Some(" "), &env_lookup).unwrap(), "sk-env"); + } + + #[test] + fn missing_key_is_typed() { + let error = resolve_api_key(None, &|_| None).expect_err("missing key should error"); + + assert_eq!( + error, + Error::Auth(AuthError::MissingApiKey { + provider: "Mistral", + }) + ); + } + + #[test] + fn static_key_is_applied_as_bearer() { + let headers = validate_environment(Vec::new(), Some("sk-param"), &|_| None).unwrap(); + + assert_eq!( + headers, + vec![("Authorization".to_string(), "Bearer sk-param".to_string())] + ); + } + + #[test] + fn existing_authorization_is_preserved_without_key_lookup() { + let headers = vec![("authorization".to_string(), "Bearer existing".to_string())]; + + assert_eq!( + validate_environment(headers.clone(), None, &|_| None).unwrap(), + headers + ); + } +} diff --git a/litellm-rust/crates/core/src/providers/mistral/mod.rs b/litellm-rust/crates/core/src/providers/mistral/mod.rs index 3621ff6a2fd..47c3f6b6c66 100644 --- a/litellm-rust/crates/core/src/providers/mistral/mod.rs +++ b/litellm-rust/crates/core/src/providers/mistral/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod ocr; diff --git a/litellm-rust/crates/core/src/providers/mistral/ocr/auth.rs b/litellm-rust/crates/core/src/providers/mistral/ocr/auth.rs deleted file mode 100644 index bd25286e78b..00000000000 --- a/litellm-rust/crates/core/src/providers/mistral/ocr/auth.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::error::{AuthError, Error}; - -pub const MISTRAL_API_KEY_ENV: &str = "MISTRAL_API_KEY"; - -pub fn resolve_api_key( - api_key: Option<&str>, - env_lookup: &dyn Fn(&str) -> Option, -) -> Result { - api_key - .map(str::trim) - .filter(|key| !key.is_empty()) - .map(str::to_string) - .or_else(|| env_lookup(MISTRAL_API_KEY_ENV).filter(|key| !key.trim().is_empty())) - .ok_or(Error::Auth(AuthError::MissingApiKey { - provider: "Mistral", - })) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn resolve_api_key_prefers_param_then_env() { - let no_env = |_: &str| None; - assert_eq!( - resolve_api_key(Some("sk-param"), &no_env).unwrap(), - "sk-param" - ); - - let with_env = |key: &str| (key == MISTRAL_API_KEY_ENV).then(|| "sk-env".to_string()); - assert_eq!(resolve_api_key(None, &with_env).unwrap(), "sk-env"); - assert_eq!(resolve_api_key(Some(" "), &with_env).unwrap(), "sk-env"); - } - - #[test] - fn resolve_api_key_errors_when_absent() { - let err = resolve_api_key(None, &|_| None).expect_err("missing key should error"); - assert_eq!( - err, - Error::Auth(AuthError::MissingApiKey { - provider: "Mistral", - }) - ); - } -} diff --git a/litellm-rust/crates/core/src/providers/mistral/ocr/mod.rs b/litellm-rust/crates/core/src/providers/mistral/ocr/mod.rs index e05c637bf42..f239b6921fa 100644 --- a/litellm-rust/crates/core/src/providers/mistral/ocr/mod.rs +++ b/litellm-rust/crates/core/src/providers/mistral/ocr/mod.rs @@ -1,2 +1 @@ -pub mod auth; pub mod transformation; diff --git a/litellm-rust/crates/core/src/providers/mistral/ocr/transformation.rs b/litellm-rust/crates/core/src/providers/mistral/ocr/transformation.rs index 5ddab9e33ad..d1ebbacacf4 100644 --- a/litellm-rust/crates/core/src/providers/mistral/ocr/transformation.rs +++ b/litellm-rust/crates/core/src/providers/mistral/ocr/transformation.rs @@ -3,7 +3,7 @@ use crate::ocr::transformation::OcrProviderConfig; use crate::ocr::types::{OcrRequestData, OcrResponseData}; use serde_json::{Map, Value}; -use super::auth::resolve_api_key; +use crate::providers::mistral::auth; const SUPPORTED_OCR_PARAMS: &[&str] = &[ "pages", @@ -131,7 +131,16 @@ impl OcrProviderConfig for MistralOcrConfig { api_key: Option<&str>, env_lookup: &dyn Fn(&str) -> Option, ) -> Result { - resolve_api_key(api_key, env_lookup) + auth::resolve_api_key(api_key, env_lookup) + } + + fn validate_environment( + &self, + headers: Vec<(String, String)>, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, + ) -> Result, Error> { + auth::validate_environment(headers, api_key, env_lookup) } } diff --git a/litellm-rust/crates/core/src/providers/mod.rs b/litellm-rust/crates/core/src/providers/mod.rs index dfb5c283382..c0c2c69831b 100644 --- a/litellm-rust/crates/core/src/providers/mod.rs +++ b/litellm-rust/crates/core/src/providers/mod.rs @@ -1,5 +1,4 @@ pub mod anthropic; -pub mod auth; pub mod azure_ai; #[cfg(feature = "bedrock-auth")] pub mod bedrock; diff --git a/litellm-rust/crates/core/src/providers/reducto/auth.rs b/litellm-rust/crates/core/src/providers/reducto/auth.rs new file mode 100644 index 00000000000..c969e8c70e6 --- /dev/null +++ b/litellm-rust/crates/core/src/providers/reducto/auth.rs @@ -0,0 +1,92 @@ +use crate::auth::{ + CredentialPlacement, CredentialPlanKind, CredentialRule, ExistingHeaderBehavior, + ProviderAuthPolicy, ResolvedCredential, SecretValue, +}; +use crate::error::{AuthError, Error}; + +pub const REDUCTO_API_KEY_ENV: &str = "REDUCTO_API_KEY"; + +const MISSING_KEY_MESSAGE: &str = "Missing REDUCTO_API_KEY - set it in the environment or pass api_key to litellm.ocr()/litellm.aocr()"; +const AUTH_RULES: &[CredentialRule] = &[CredentialRule { + kind: CredentialPlanKind::Static, + placement: CredentialPlacement::Bearer, +}]; +const AUTH_POLICY: ProviderAuthPolicy = ProviderAuthPolicy { + rules: AUTH_RULES, + accepted_existing_headers: &["Authorization"], + existing_header_behavior: ExistingHeaderBehavior::Preserve, + scope: None, + audience: None, +}; + +pub fn resolve_api_key( + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> Result { + api_key + .map(str::trim) + .filter(|key| !key.is_empty()) + .map(str::to_string) + .or_else(|| { + env_lookup(REDUCTO_API_KEY_ENV) + .map(|key| key.trim().to_string()) + .filter(|key| !key.is_empty()) + }) + .ok_or_else(|| Error::Auth(AuthError::Message(MISSING_KEY_MESSAGE.to_string()))) +} + +pub fn validate_environment( + headers: Vec<(String, String)>, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, +) -> Result, Error> { + if AUTH_POLICY.has_existing_credential(&headers) { + return Ok(headers); + } + let credential = + ResolvedCredential::Static(SecretValue::new(resolve_api_key(api_key, env_lookup)?)); + AUTH_POLICY + .apply(headers, CredentialPlanKind::Static, &credential) + .map_err(Error::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_key_precedes_environment_key() { + let key = resolve_api_key(Some("passed-key"), &|_| Some("env-key".to_string())) + .expect("explicit key resolves"); + + assert_eq!(key, "passed-key"); + } + + #[test] + fn blank_explicit_key_uses_environment_key() { + let env_lookup = + |name: &str| (name == REDUCTO_API_KEY_ENV).then(|| " env-key ".to_string()); + + assert_eq!(resolve_api_key(Some(" "), &env_lookup).unwrap(), "env-key"); + } + + #[test] + fn static_key_is_applied_as_bearer() { + let headers = validate_environment(Vec::new(), Some("passed-key"), &|_| None).unwrap(); + + assert_eq!( + headers, + vec![("Authorization".to_string(), "Bearer passed-key".to_string())] + ); + } + + #[test] + fn existing_authorization_is_preserved_without_key_lookup() { + let headers = vec![("authorization".to_string(), "Bearer existing".to_string())]; + + assert_eq!( + validate_environment(headers.clone(), None, &|_| None).unwrap(), + headers + ); + } +} diff --git a/litellm-rust/crates/core/src/providers/reducto/mod.rs b/litellm-rust/crates/core/src/providers/reducto/mod.rs index 3621ff6a2fd..47c3f6b6c66 100644 --- a/litellm-rust/crates/core/src/providers/reducto/mod.rs +++ b/litellm-rust/crates/core/src/providers/reducto/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod ocr; diff --git a/litellm-rust/crates/core/src/providers/reducto/ocr/auth.rs b/litellm-rust/crates/core/src/providers/reducto/ocr/auth.rs deleted file mode 100644 index 3473fd3d25c..00000000000 --- a/litellm-rust/crates/core/src/providers/reducto/ocr/auth.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::error::{AuthError, Error}; - -pub const REDUCTO_API_KEY_ENV: &str = "REDUCTO_API_KEY"; - -const MISSING_KEY_MESSAGE: &str = "Missing REDUCTO_API_KEY - set it in the environment or pass api_key to litellm.ocr()/litellm.aocr()"; - -pub fn resolve_api_key( - api_key: Option<&str>, - env_lookup: &dyn Fn(&str) -> Option, -) -> Result { - api_key - .map(str::trim) - .filter(|key| !key.is_empty()) - .map(str::to_string) - .or_else(|| { - env_lookup(REDUCTO_API_KEY_ENV) - .map(|key| key.trim().to_string()) - .filter(|key| !key.is_empty()) - }) - .ok_or_else(|| Error::Auth(AuthError::Message(MISSING_KEY_MESSAGE.to_string()))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn resolve_api_key_prefers_explicit_value() { - let key = resolve_api_key(Some("passed-key"), &|_| Some("env-key".to_string())) - .expect("explicit key resolves"); - - assert_eq!(key, "passed-key"); - } - - #[test] - fn resolve_api_key_uses_environment_fallback() { - let env_lookup = - |name: &str| (name == REDUCTO_API_KEY_ENV).then(|| " env-key ".to_string()); - - assert_eq!(resolve_api_key(None, &env_lookup).unwrap(), "env-key"); - } -} diff --git a/litellm-rust/crates/core/src/providers/reducto/ocr/mod.rs b/litellm-rust/crates/core/src/providers/reducto/ocr/mod.rs index 9b0ba7f97fb..8acee8f770c 100644 --- a/litellm-rust/crates/core/src/providers/reducto/ocr/mod.rs +++ b/litellm-rust/crates/core/src/providers/reducto/ocr/mod.rs @@ -1,4 +1,3 @@ -pub mod auth; pub mod transformation; #[cfg(test)] diff --git a/litellm-rust/crates/core/src/providers/reducto/ocr/tests.rs b/litellm-rust/crates/core/src/providers/reducto/ocr/tests.rs index b631e790ea1..42af123c530 100644 --- a/litellm-rust/crates/core/src/providers/reducto/ocr/tests.rs +++ b/litellm-rust/crates/core/src/providers/reducto/ocr/tests.rs @@ -1,9 +1,9 @@ use rstest::{fixture, rstest}; use serde_json::{Value, json}; -use super::auth::resolve_api_key; use super::transformation::*; use crate::ocr::transformation::OcrProviderConfig; +use crate::providers::reducto::auth::resolve_api_key; #[fixture] fn parse_response() -> Value { @@ -53,14 +53,9 @@ fn parse_response() -> Value { fn test_parse_v3_file_upload_and_response_mapping(parse_response: Value) { let source = classify_document_source("data:application/pdf;base64,JVBERi0xLjQ=") .expect("PDF data URI should be valid"); - let upload = build_upload_request( - source, - "Bearer test-key", - Some("https://platform.reducto.ai"), - ) - .expect("data URI should require upload"); + let upload = build_upload_request(source, Some("https://platform.reducto.ai")) + .expect("data URI should require upload"); assert_eq!(upload.url, "https://platform.reducto.ai/upload"); - assert_eq!(upload.authorization, "Bearer test-key"); assert_eq!(upload.file_name, "document"); assert_eq!(upload.mime_type, "application/pdf"); assert_eq!(upload.bytes, b"%PDF-1.4"); @@ -114,7 +109,7 @@ fn test_parse_v3_reducto_id_passthrough_skips_upload(parse_response: Value) { "document_url": "reducto://already-uploaded.pdf", }); let source = extract_document_source(&document).expect("Reducto ID should be valid"); - assert!(build_upload_request(source.clone(), "Bearer test-key", None).is_none()); + assert!(build_upload_request(source.clone(), None).is_none()); assert_eq!( source, ReductoDocumentSource::FileId("reducto://already-uploaded.pdf".to_string()) @@ -165,14 +160,9 @@ fn test_parse_legacy_wraps_enhance_under_options() { fn test_parse_v3_image_data_uri_upload_uses_image_mime() { let source = classify_document_source("data:image/png;base64,iVBORw0KGgo=") .expect("PNG data URI should be valid"); - let upload = build_upload_request( - source, - "Bearer programmatic-key", - Some("https://custom.reducto.test/"), - ) - .expect("data URI should require upload"); + let upload = build_upload_request(source, Some("https://custom.reducto.test/")) + .expect("data URI should require upload"); assert_eq!(upload.url, "https://custom.reducto.test/upload"); - assert_eq!(upload.authorization, "Bearer programmatic-key"); assert_eq!(upload.mime_type, "image/png"); assert_eq!(upload.bytes, b"\x89PNG\r\n\x1a\n"); } diff --git a/litellm-rust/crates/core/src/providers/reducto/ocr/transformation.rs b/litellm-rust/crates/core/src/providers/reducto/ocr/transformation.rs index 6f9fceddee4..d75681fa950 100644 --- a/litellm-rust/crates/core/src/providers/reducto/ocr/transformation.rs +++ b/litellm-rust/crates/core/src/providers/reducto/ocr/transformation.rs @@ -8,7 +8,7 @@ use crate::error::{Error, json_type_name}; use crate::ocr::transformation::OcrProviderConfig; use crate::ocr::types::{OcrRequestData, OcrResponseData}; -use super::auth::resolve_api_key; +use crate::providers::reducto::auth; pub const REDUCTO_API_BASE: &str = "https://platform.reducto.ai"; pub const REDUCTO_ID_PREFIX: &str = "reducto://"; @@ -27,7 +27,6 @@ pub enum ReductoDocumentSource { #[derive(Clone, PartialEq, Eq)] pub struct ReductoUploadRequest { pub url: String, - pub authorization: String, pub file_name: &'static str, pub bytes: Vec, pub mime_type: String, @@ -124,7 +123,6 @@ pub fn classify_document_source(source: &str) -> Result, ) -> Option { let ReductoDocumentSource::Upload { bytes, mime_type } = source else { @@ -133,7 +131,6 @@ pub fn build_upload_request( Some(ReductoUploadRequest { url: upload_url(api_base), - authorization: authorization.to_string(), file_name: "document", bytes, mime_type, @@ -344,7 +341,16 @@ impl OcrProviderConfig for ReductoParseV3Config { api_key: Option<&str>, env_lookup: &dyn Fn(&str) -> Option, ) -> Result { - resolve_api_key(api_key, env_lookup) + auth::resolve_api_key(api_key, env_lookup) + } + + fn validate_environment( + &self, + headers: Vec<(String, String)>, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, + ) -> Result, Error> { + auth::validate_environment(headers, api_key, env_lookup) } } @@ -386,6 +392,15 @@ impl OcrProviderConfig for ReductoParseLegacyConfig { api_key: Option<&str>, env_lookup: &dyn Fn(&str) -> Option, ) -> Result { - resolve_api_key(api_key, env_lookup) + auth::resolve_api_key(api_key, env_lookup) + } + + fn validate_environment( + &self, + headers: Vec<(String, String)>, + api_key: Option<&str>, + env_lookup: &dyn Fn(&str) -> Option, + ) -> Result, Error> { + auth::validate_environment(headers, api_key, env_lookup) } }