This commit is contained in:
Yujong Lee 2026-09-09 14:58:26 -07:00
parent 89c70c4084
commit b3b369c8ef
29 changed files with 266 additions and 156 deletions

View file

@ -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<Value, Error> {
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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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,
};

View file

@ -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";

View file

@ -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;

View file

@ -1,4 +1,4 @@
use crate::error::AuthError;
use crate::AuthError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CredentialPlacement {

View file

@ -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,

View file

@ -1,6 +1,6 @@
use veil::Redact;
#[derive(Clone, Redact)]
#[derive(Redact, Clone)]
pub struct SecretValue(#[redact(with = "[REDACTED]")] String);
impl SecretValue {

View file

@ -5,7 +5,7 @@ use std::time::SystemTime;
use veil::Redact;
use crate::error::AuthError;
use crate::AuthError;
use super::secret::SecretValue;

View file

@ -1,4 +1,5 @@
pub mod audio_transcription;
pub mod auth;
pub mod call_lifecycle;
pub mod chat_completions;
pub mod constants;

View file

@ -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};

View file

@ -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 {

View file

@ -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<String> + Sync),
) -> Result<Option<crate::providers::auth::ResolvedCredential>, AuthError> {
) -> Result<Option<crate::auth::ResolvedCredential>, 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::{

View file

@ -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;

View file

@ -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<String>,
) -> Result<String, Error> {
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<String>,
) -> Result<Vec<(String, String)>, 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
);
}
}

View file

@ -1 +1,2 @@
pub mod auth;
pub mod ocr;

View file

@ -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<String>,
) -> Result<String, Error> {
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",
})
);
}
}

View file

@ -1,2 +1 @@
pub mod auth;
pub mod transformation;

View file

@ -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<String>,
) -> Result<String, Error> {
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<String>,
) -> Result<Vec<(String, String)>, Error> {
auth::validate_environment(headers, api_key, env_lookup)
}
}

View file

@ -1,5 +1,4 @@
pub mod anthropic;
pub mod auth;
pub mod azure_ai;
#[cfg(feature = "bedrock-auth")]
pub mod bedrock;

View file

@ -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<String>,
) -> Result<String, Error> {
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<String>,
) -> Result<Vec<(String, String)>, 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
);
}
}

View file

@ -1 +1,2 @@
pub mod auth;
pub mod ocr;

View file

@ -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<String>,
) -> Result<String, Error> {
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");
}
}

View file

@ -1,4 +1,3 @@
pub mod auth;
pub mod transformation;
#[cfg(test)]

View file

@ -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");
}

View file

@ -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<u8>,
pub mime_type: String,
@ -124,7 +123,6 @@ pub fn classify_document_source(source: &str) -> Result<ReductoDocumentSource, E
pub fn build_upload_request(
source: ReductoDocumentSource,
authorization: &str,
api_base: Option<&str>,
) -> Option<ReductoUploadRequest> {
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<String>,
) -> Result<String, Error> {
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<String>,
) -> Result<Vec<(String, String)>, 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<String>,
) -> Result<String, Error> {
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<String>,
) -> Result<Vec<(String, String)>, Error> {
auth::validate_environment(headers, api_key, env_lookup)
}
}