mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
wip
This commit is contained in:
parent
89c70c4084
commit
b3b369c8ef
29 changed files with 266 additions and 156 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::error::AuthError;
|
||||
use crate::AuthError;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CredentialPlacement {
|
||||
|
|
@ -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,
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use veil::Redact;
|
||||
|
||||
#[derive(Clone, Redact)]
|
||||
#[derive(Redact, Clone)]
|
||||
pub struct SecretValue(#[redact(with = "[REDACTED]")] String);
|
||||
|
||||
impl SecretValue {
|
||||
|
|
@ -5,7 +5,7 @@ use std::time::SystemTime;
|
|||
|
||||
use veil::Redact;
|
||||
|
||||
use crate::error::AuthError;
|
||||
use crate::AuthError;
|
||||
|
||||
use super::secret::SecretValue;
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod audio_transcription;
|
||||
pub mod auth;
|
||||
pub mod call_lifecycle;
|
||||
pub mod chat_completions;
|
||||
pub mod constants;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
102
litellm-rust/crates/core/src/providers/mistral/auth.rs
Normal file
102
litellm-rust/crates/core/src/providers/mistral/auth.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub mod auth;
|
||||
pub mod ocr;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
pub mod auth;
|
||||
pub mod transformation;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
pub mod anthropic;
|
||||
pub mod auth;
|
||||
pub mod azure_ai;
|
||||
#[cfg(feature = "bedrock-auth")]
|
||||
pub mod bedrock;
|
||||
|
|
|
|||
92
litellm-rust/crates/core/src/providers/reducto/auth.rs
Normal file
92
litellm-rust/crates/core/src/providers/reducto/auth.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub mod auth;
|
||||
pub mod ocr;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
pub mod auth;
|
||||
pub mod transformation;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue