mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(rust): resolve OCR provider env fallbacks through the secret manager
Python reads every provider credential fallback (MISTRAL_API_KEY, AZURE_AI_API_KEY, AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT, Azure AD and Vertex env, ...) through get_secret_str, which consults the configured key_management_system before os.environ. Native OCR read std::env directly, so a key held only in the vault went missing and a stale env copy silently won. OcrClient now carries an injected secret Lookup that the connection exposes to providers and auth crates; the bridge backs it with settings.secret -> get_secret_str, pure Rust keeps the process env. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c0705f31b4
commit
0d76359dc9
18 changed files with 226 additions and 41 deletions
|
|
@ -22,7 +22,7 @@ pub(crate) async fn perform_ocr_request(
|
|||
) -> Result<LiteLLMOcrResponse, Error> {
|
||||
request.response_format()?;
|
||||
let config = request.config;
|
||||
let request = prepare_request(request, caller_document, client.settings());
|
||||
let request = prepare_request(request, caller_document, client);
|
||||
let hooks = OcrCallHooks::new(host.clone(), &request, config);
|
||||
config.ocr(client, &request, &hooks).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use litellm_auth::{InputSource, SecretValue, Sourced};
|
||||
use litellm_llms::base_llm::ocr::{
|
||||
settings::OcrSettings,
|
||||
transformation::{OcrConnection, OcrCredentialInputs, PreparedOcrRequest, credential_env},
|
||||
handler::OcrClient,
|
||||
transformation::{OcrConnection, OcrCredentialInputs, PreparedOcrRequest},
|
||||
};
|
||||
|
||||
use super::provider_config::OcrProvider;
|
||||
|
|
@ -10,7 +10,7 @@ use crate::ocr::types::{LiteLLMOcrRequest, ResolvedOcrRequest};
|
|||
pub(crate) fn prepare_request(
|
||||
request: ResolvedOcrRequest,
|
||||
caller_document: bool,
|
||||
settings: &OcrSettings,
|
||||
client: &OcrClient,
|
||||
) -> PreparedOcrRequest {
|
||||
let credentials = request.credentials.clone();
|
||||
let api_base_env = match request.config.provider() {
|
||||
|
|
@ -23,14 +23,14 @@ pub(crate) fn prepare_request(
|
|||
request
|
||||
.config
|
||||
.get_api_key_env_var()
|
||||
.and_then(credential_env)
|
||||
.and_then(|name| client.secrets().get(name))
|
||||
.map(|value| Sourced::new(SecretValue::new(value), InputSource::Environment))
|
||||
})
|
||||
});
|
||||
let dynamic_api_base = credentials.dynamic_api_base.or_else(|| {
|
||||
credentials.api_base.clone().or_else(|| {
|
||||
api_base_env
|
||||
.and_then(credential_env)
|
||||
.and_then(|name| client.secrets().get(name))
|
||||
.map(|value| Sourced::new(value, InputSource::Environment))
|
||||
})
|
||||
});
|
||||
|
|
@ -53,7 +53,12 @@ pub(crate) fn prepare_request(
|
|||
PreparedOcrRequest {
|
||||
model,
|
||||
document,
|
||||
connection: OcrConnection::new(resolved, transport, settings.clone()),
|
||||
connection: OcrConnection::new(
|
||||
resolved,
|
||||
transport,
|
||||
client.settings().clone(),
|
||||
client.secrets().clone(),
|
||||
),
|
||||
caller_document,
|
||||
optional_params,
|
||||
input_sources,
|
||||
|
|
@ -63,7 +68,11 @@ pub(crate) fn prepare_request(
|
|||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn prepare_request_for_test(request: ResolvedOcrRequest) -> PreparedOcrRequest {
|
||||
prepare_request(request, true, &OcrSettings::default())
|
||||
prepare_request(
|
||||
request,
|
||||
true,
|
||||
&OcrClient::for_test(reqwest::Client::new(), reqwest::Client::new()),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -174,6 +174,30 @@ async fn facade_retains_native_response_when_requested() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_key_fallback_reads_the_injected_secret_source() {
|
||||
let (base, seen, server) = mock_server(vec![MockResponse::json(json!({"pages":[]}))]).await;
|
||||
let request = decode_request(OcrWireRequest {
|
||||
model: "mistral/model".into(),
|
||||
document: json!({"type":"document_url","document_url":"data:application/pdf;base64,YWJj"}),
|
||||
api_key: None,
|
||||
api_base: Some(base.clone()),
|
||||
custom_llm_provider: None,
|
||||
extra_headers: None,
|
||||
optional_params: Default::default(),
|
||||
input_sources: Default::default(),
|
||||
timeout_seconds: Some(2.0),
|
||||
})
|
||||
.unwrap();
|
||||
let client = ocr_client().with_secrets(Arc::new(|name: &str| {
|
||||
(name == "MISTRAL_API_KEY").then(|| "from-secret-manager".to_string())
|
||||
}));
|
||||
|
||||
crate::ocr::client::perform(&client, request).await.unwrap();
|
||||
server.await.unwrap();
|
||||
assert!(seen.lock().unwrap()[0].contains("authorization: Bearer from-secret-manager"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ocr_client_uses_the_injected_http_pool_configuration() {
|
||||
let (base, seen, server) = mock_server(vec![MockResponse::json(json!({"pages":[]}))]).await;
|
||||
|
|
@ -187,6 +211,7 @@ async fn ocr_client_uses_the_injected_http_pool_configuration() {
|
|||
UrlPolicy::default(),
|
||||
VertexAuth::default(),
|
||||
OcrSettings::default(),
|
||||
Arc::new(litellm_core_utils::settings::ProcessEnvironment),
|
||||
)
|
||||
.unwrap();
|
||||
crate::ocr::client::perform(&client, wire_request("mistral/model", &base, json!({})))
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl BaseOcrConfig for AzureAICohereParseConfig {
|
|||
) -> Result<String, Error> {
|
||||
let base = super::transformation::AzureAiOcrConfig::resolve_api_base(
|
||||
request.connection.api_base.as_deref(),
|
||||
&crate::base_llm::ocr::transformation::credential_env,
|
||||
&|name: &str| request.connection.secret(name),
|
||||
)?;
|
||||
self.get_complete_url(&base)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use crate::base_llm::ocr::{
|
|||
BaseOcrConfig, DecodedOcrResponse, LiteLLMOcrResponse, OCR_INLINE_MAX_BYTES,
|
||||
OCR_POLL_RETRY_SECS, OcrConnection, OcrCredentialInputs, OcrDocument, OcrPage,
|
||||
OcrPageDimensions, OcrResponseContext, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest,
|
||||
ResolvedOcrCredentials, credential_env, decode_and_normalize_response, decode_response,
|
||||
ResolvedOcrCredentials, decode_and_normalize_response, decode_response,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -181,8 +181,10 @@ impl BaseOcrConfig for AzureDocumentIntelligenceOcrConfig {
|
|||
&request.input_sources,
|
||||
)?
|
||||
};
|
||||
self.resolve_headers(&request.connection, &config, &credential_env)
|
||||
.await
|
||||
self.resolve_headers(&request.connection, &config, &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
|
|
@ -192,7 +194,7 @@ impl BaseOcrConfig for AzureDocumentIntelligenceOcrConfig {
|
|||
_environment: &Self::Environment,
|
||||
) -> Result<String, Error> {
|
||||
let endpoint = nonblank(request.connection.api_base.clone())
|
||||
.or_else(|| nonblank(credential_env(AZURE_DI_ENDPOINT_ENV)))
|
||||
.or_else(|| nonblank(request.connection.secret(AZURE_DI_ENDPOINT_ENV)))
|
||||
.ok_or_else(|| Error::Auth(litellm_auth::Error::ProviderAuthentication("Missing Azure Document Intelligence API Base - Set AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT or pass api_base".into())))?;
|
||||
self.build_ocr_url(
|
||||
&endpoint,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
handler::OcrClient,
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OcrConnection, OcrDocument, OcrRequestContext,
|
||||
OcrResponseFormat, PreparedOcrRequest, credential_env,
|
||||
OcrResponseFormat, PreparedOcrRequest,
|
||||
},
|
||||
},
|
||||
mistral::ocr::transformation::{MistralOcrConfig, MistralOcrRequest},
|
||||
|
|
@ -57,8 +57,10 @@ impl BaseOcrConfig for AzureAiOcrConfig {
|
|||
&request.input_sources,
|
||||
)?
|
||||
};
|
||||
self.resolve_headers(&request.connection, &config, &credential_env)
|
||||
.await
|
||||
self.resolve_headers(&request.connection, &config, &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
|
|
@ -67,7 +69,9 @@ impl BaseOcrConfig for AzureAiOcrConfig {
|
|||
_optional_params: &Self::OcrParams,
|
||||
_environment: &Self::Environment,
|
||||
) -> Result<String, Error> {
|
||||
self.build_ocr_url(request.connection.api_base.as_deref(), &credential_env)
|
||||
self.build_ocr_url(request.connection.api_base.as_deref(), &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
}
|
||||
|
||||
fn transform_ocr_request(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::base_llm::ocr::{
|
||||
error::Error,
|
||||
settings::OcrSettings,
|
||||
settings::{OcrSettings, Secrets},
|
||||
transformation::{
|
||||
BaseOcrConfig, DecodedOcrResponse, LiteLLMOcrResponse, OcrDocument, OcrResponseContext,
|
||||
PreparedOcrRequest, decode_request_value, decode_response,
|
||||
|
|
@ -35,6 +35,7 @@ pub struct OcrClient {
|
|||
document_fetcher: MediaFetcher,
|
||||
vertex_auth: VertexAuth,
|
||||
settings: OcrSettings,
|
||||
secrets: Secrets,
|
||||
}
|
||||
|
||||
impl OcrClient {
|
||||
|
|
@ -44,6 +45,7 @@ impl OcrClient {
|
|||
url_policy: UrlPolicy,
|
||||
vertex_auth: VertexAuth,
|
||||
settings: OcrSettings,
|
||||
secrets: Secrets,
|
||||
) -> Result<Self, litellm_http::Error> {
|
||||
Ok(Self {
|
||||
provider_http: pool.client(config, ClientVariant::Provider)?,
|
||||
|
|
@ -51,6 +53,7 @@ impl OcrClient {
|
|||
document_fetcher: MediaFetcher::new(pool, config, url_policy)?,
|
||||
vertex_auth,
|
||||
settings,
|
||||
secrets,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +77,10 @@ impl OcrClient {
|
|||
&self.settings
|
||||
}
|
||||
|
||||
pub fn secrets(&self) -> &Secrets {
|
||||
&self.secrets
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn for_test(provider_http: reqwest::Client, document_http: reqwest::Client) -> Self {
|
||||
Self {
|
||||
|
|
@ -85,6 +92,7 @@ impl OcrClient {
|
|||
document_fetcher: MediaFetcher::for_test(document_http),
|
||||
vertex_auth: VertexAuth::default(),
|
||||
settings: OcrSettings::default(),
|
||||
secrets: std::sync::Arc::new(litellm_core_utils::settings::ProcessEnvironment),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +100,11 @@ impl OcrClient {
|
|||
pub fn with_settings(self, settings: OcrSettings) -> Self {
|
||||
Self { settings, ..self }
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn with_secrets(self, secrets: Secrets) -> Self {
|
||||
Self { secrets, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Rust counterpart of `BaseLLMHTTPHandler.async_ocr`: prepare the provider request,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use std::time::Duration;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use litellm_core_utils::settings::Lookup;
|
||||
|
||||
pub type Secrets = Arc<dyn Lookup + Send + Sync>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct OcrSettings {
|
||||
pub request_timeout: Duration,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::{collections::BTreeMap, future::Future, time::Duration};
|
||||
use std::{collections::BTreeMap, future::Future, sync::Arc, time::Duration};
|
||||
|
||||
use litellm_auth::{InputSource, SecretValue, Sourced, TokenProviderHandle};
|
||||
use litellm_core_utils::{
|
||||
call_arguments::CallArguments,
|
||||
serde_compat::{FiniteF64, LaxI64},
|
||||
settings::ProcessEnvironment,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Serialize,
|
||||
|
|
@ -15,7 +16,7 @@ use serde_with::serde_as;
|
|||
use crate::base_llm::ocr::{
|
||||
error::Error,
|
||||
handler::{CallHooks, OcrClient, read_response_bytes, transform_request_body},
|
||||
settings::OcrSettings,
|
||||
settings::{OcrSettings, Secrets},
|
||||
};
|
||||
|
||||
pub const OCR_RESPONSE_MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
|
@ -160,6 +161,7 @@ pub struct OcrConnection {
|
|||
pub timeout: Duration,
|
||||
pub max_response_bytes: usize,
|
||||
pub settings: OcrSettings,
|
||||
pub secrets: Secrets,
|
||||
}
|
||||
|
||||
impl OcrConnection {
|
||||
|
|
@ -167,6 +169,7 @@ impl OcrConnection {
|
|||
credentials: ResolvedOcrCredentials,
|
||||
transport: OcrTransportConfig,
|
||||
settings: OcrSettings,
|
||||
secrets: Secrets,
|
||||
) -> Self {
|
||||
let api_key_source = credentials
|
||||
.api_key
|
||||
|
|
@ -191,8 +194,13 @@ impl OcrConnection {
|
|||
.unwrap_or(settings.request_timeout),
|
||||
max_response_bytes: transport.max_response_bytes,
|
||||
settings,
|
||||
secrets,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret(&self, name: &str) -> Option<String> {
|
||||
self.secrets.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OcrConnection {
|
||||
|
|
@ -201,6 +209,7 @@ impl Default for OcrConnection {
|
|||
ResolvedOcrCredentials::default(),
|
||||
OcrTransportConfig::default(),
|
||||
OcrSettings::default(),
|
||||
Arc::new(ProcessEnvironment),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -563,10 +572,6 @@ pub fn decode_and_normalize_response<T: DeserializeOwned>(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn credential_env(name: &str) -> Option<String> {
|
||||
std::env::var(name).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
|
@ -587,6 +592,7 @@ mod tests {
|
|||
..OcrTransportConfig::default()
|
||||
},
|
||||
settings.clone(),
|
||||
Arc::new(ProcessEnvironment),
|
||||
)
|
||||
.timeout
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::base_llm::ocr::{
|
|||
handler::OcrClient,
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OCR_INLINE_MAX_BYTES, OcrConnection, OcrDocument,
|
||||
OcrPage, OcrPageImage, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest, credential_env,
|
||||
OcrPage, OcrPageImage, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest,
|
||||
decode_and_normalize_response, decode_response_value,
|
||||
},
|
||||
};
|
||||
|
|
@ -122,7 +122,9 @@ impl BaseOcrConfig for CohereParseConfig {
|
|||
request: &PreparedOcrRequest,
|
||||
_client: &OcrClient,
|
||||
) -> Result<Self::Environment, Error> {
|
||||
self.resolve_headers(&request.connection, &credential_env)
|
||||
self.resolve_headers(&request.connection, &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::base_llm::ocr::{
|
|||
handler::OcrClient,
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OcrConnection, OcrDocument, OcrPage, OcrResponseFormat,
|
||||
OcrUsageInfo, PreparedOcrRequest, credential_env, decode_and_normalize_response,
|
||||
OcrUsageInfo, PreparedOcrRequest, decode_and_normalize_response,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -84,7 +84,9 @@ impl BaseOcrConfig for MistralOcrConfig {
|
|||
request: &PreparedOcrRequest,
|
||||
_client: &OcrClient,
|
||||
) -> Result<Self::Environment, Error> {
|
||||
self.resolve_headers(&request.connection, &credential_env)
|
||||
self.resolve_headers(&request.connection, &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::base_llm::ocr::{
|
|||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OCR_INLINE_MAX_BYTES, OcrConnection, OcrDocument,
|
||||
OcrPage, OcrRequestContext, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest,
|
||||
credential_env, decode_and_normalize_response,
|
||||
decode_and_normalize_response,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -110,7 +110,9 @@ impl BaseOcrConfig for ReductoParseV3Config {
|
|||
request: &PreparedOcrRequest,
|
||||
_client: &OcrClient,
|
||||
) -> Result<Self::Environment, Error> {
|
||||
resolve_headers(&request.connection, &credential_env)
|
||||
resolve_headers(&request.connection, &|name: &str| {
|
||||
request.connection.secret(name)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_complete_url(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::base_llm::ocr::{
|
|||
handler::OcrClient,
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OcrDocument, OcrPage, OcrPageDimensions, OcrPageImage,
|
||||
OcrRequestContext, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest, credential_env,
|
||||
OcrRequestContext, OcrResponseFormat, OcrUsageInfo, PreparedOcrRequest,
|
||||
decode_and_normalize_response, decode_response_value,
|
||||
},
|
||||
};
|
||||
|
|
@ -126,8 +126,9 @@ impl BaseOcrConfig for VertexAIDeepSeekOCRConfig {
|
|||
&request.optional_params,
|
||||
&request.input_sources,
|
||||
)?;
|
||||
let location = vertex::get_vertex_ai_location(&config, &credential_env)
|
||||
.unwrap_or_else(|| DEFAULT_LOCATION.to_string());
|
||||
let location =
|
||||
vertex::get_vertex_ai_location(&config, &|name: &str| request.connection.secret(name))
|
||||
.unwrap_or_else(|| DEFAULT_LOCATION.to_string());
|
||||
self.get_complete_url(
|
||||
request.connection.api_base.as_deref(),
|
||||
&environment.project_id,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
handler::OcrClient,
|
||||
transformation::{
|
||||
BaseOcrConfig, LiteLLMOcrResponse, OcrConnection, OcrDocument, OcrEnvironment,
|
||||
OcrRequestContext, OcrResponseFormat, PreparedOcrRequest, credential_env,
|
||||
OcrRequestContext, OcrResponseFormat, PreparedOcrRequest,
|
||||
},
|
||||
},
|
||||
mistral::ocr::transformation::{MistralOcrConfig, MistralOcrRequest},
|
||||
|
|
@ -65,8 +65,9 @@ impl BaseOcrConfig for VertexAiOcrConfig {
|
|||
&request.optional_params,
|
||||
&request.input_sources,
|
||||
)?;
|
||||
let location = vertex::get_vertex_ai_location(&config, &credential_env)
|
||||
.unwrap_or_else(|| DEFAULT_LOCATION.to_string());
|
||||
let location =
|
||||
vertex::get_vertex_ai_location(&config, &|name: &str| request.connection.secret(name))
|
||||
.unwrap_or_else(|| DEFAULT_LOCATION.to_string());
|
||||
self.build_ocr_url(
|
||||
request.connection.api_base.as_deref(),
|
||||
&environment.project_id,
|
||||
|
|
@ -139,7 +140,7 @@ impl VertexAiOcrConfig {
|
|||
.as_ref()
|
||||
.map(litellm_auth::SecretValue::expose),
|
||||
config,
|
||||
&credential_env,
|
||||
&|name: &str| connection.secret(name),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use litellm_core_utils::settings::Lookup;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
const MODULE: &str = "litellm.rust_bridge.settings";
|
||||
|
|
@ -29,6 +30,23 @@ impl PythonSettings {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PythonSecrets;
|
||||
|
||||
impl Lookup for PythonSecrets {
|
||||
fn get(&self, name: &str) -> Option<String> {
|
||||
Python::attach(|py| {
|
||||
py.import(MODULE)
|
||||
.and_then(|module| module.getattr("secret")?.call1((name,)))
|
||||
.and_then(|value| value.extract::<Option<String>>())
|
||||
.unwrap_or_else(|error| {
|
||||
let _ =
|
||||
PythonSettings::warn(py, &format!("reading secret {name} failed: {error}"));
|
||||
None
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const CONTRACT: &str = include_str!("../python_settings.json");
|
||||
|
||||
|
|
@ -36,9 +54,10 @@ pub(crate) const CONTRACT: &str = include_str!("../python_settings.json");
|
|||
mod tests {
|
||||
use std::{collections::BTreeSet, ffi::CString};
|
||||
|
||||
use litellm_core_utils::settings::Lookup;
|
||||
use pyo3::{prelude::*, types::PyDict};
|
||||
|
||||
use super::{CONTRACT, PythonSettings};
|
||||
use super::{CONTRACT, PythonSecrets, PythonSettings};
|
||||
|
||||
#[test]
|
||||
fn every_settings_group_is_in_the_python_contract() {
|
||||
|
|
@ -62,4 +81,48 @@ mod tests {
|
|||
assert_eq!(read, declared);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secrets_come_from_the_python_secret_reader_and_a_failed_read_is_unset() {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
py.run(
|
||||
c"
|
||||
import sys
|
||||
import types
|
||||
settings = types.ModuleType('litellm.rust_bridge.settings')
|
||||
settings.warnings = []
|
||||
def secret(name):
|
||||
if name == 'BROKEN':
|
||||
raise RuntimeError('vault down')
|
||||
return {'MISTRAL_API_KEY': 'from-vault'}.get(name)
|
||||
settings.secret = secret
|
||||
settings.warn = settings.warnings.append
|
||||
sys.modules.setdefault('litellm', types.ModuleType('litellm'))
|
||||
sys.modules.setdefault('litellm.rust_bridge', types.ModuleType('litellm.rust_bridge'))
|
||||
sys.modules['litellm.rust_bridge.settings'] = settings
|
||||
",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
PythonSecrets.get("MISTRAL_API_KEY").as_deref(),
|
||||
Some("from-vault")
|
||||
);
|
||||
assert_eq!(PythonSecrets.get("ABSENT"), None);
|
||||
assert_eq!(PythonSecrets.get("BROKEN"), None);
|
||||
Python::attach(|py| {
|
||||
let warnings: Vec<String> = py
|
||||
.import("litellm.rust_bridge.settings")
|
||||
.unwrap()
|
||||
.getattr("warnings")
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(warnings.len(), 1);
|
||||
assert!(warnings[0].contains("BROKEN") && warnings[0].contains("vault down"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ mod errors;
|
|||
mod host;
|
||||
mod project;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use host::OcrRouteHost;
|
||||
use litellm_auth_gcp::VertexAuth;
|
||||
|
|
@ -16,7 +16,7 @@ use pyo3::{
|
|||
types::{PyDict, PyTuple},
|
||||
};
|
||||
|
||||
use crate::{errors::RustBridgeDeclined, http};
|
||||
use crate::{errors::RustBridgeDeclined, http, python_settings::PythonSecrets};
|
||||
|
||||
const SURFACE: LegacySurface = LegacySurface {
|
||||
call_type: "ocr",
|
||||
|
|
@ -45,6 +45,7 @@ fn run_ocr(
|
|||
http::url_policy(py)?,
|
||||
VERTEX_AUTH.clone(),
|
||||
OcrSettings::from_environment(&ProcessEnvironment),
|
||||
Arc::new(PythonSecrets),
|
||||
)
|
||||
.map_err(|error| RustBridgeDeclined::new_err(error.to_string()))?;
|
||||
run_legacy_call(
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ def warn(message: str) -> None:
|
|||
verbose_logger.warning("%s", message)
|
||||
|
||||
|
||||
def secret(name: str) -> str | None:
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
return get_secret_str(name)
|
||||
|
||||
|
||||
def url_policy() -> UrlPolicy:
|
||||
import litellm
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import logging
|
|||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.integrations.custom_secret_manager import CustomSecretManager
|
||||
from litellm.llms.custom_httpx.http_handler import default_user_agent
|
||||
from litellm.rust_bridge import settings
|
||||
from litellm.types.secret_managers.main import KeyManagementSettings, KeyManagementSystem
|
||||
|
||||
CONTRACT_PATH: Final = Path(__file__).parents[3] / "litellm-rust/crates/python-bridge/python_settings.json"
|
||||
|
||||
|
|
@ -73,3 +76,46 @@ def test_warn_reaches_the_litellm_logger(caplog: pytest.LogCaptureFixture) -> No
|
|||
settings.warn("ssl_ecdh_curve 'secp521r1' is not supported")
|
||||
|
||||
assert [record.getMessage() for record in caplog.records] == ["ssl_ecdh_curve 'secp521r1' is not supported"]
|
||||
|
||||
|
||||
class _VaultSecrets(CustomSecretManager):
|
||||
def __init__(self, secrets: dict[str, str]) -> None:
|
||||
super().__init__(secret_manager_name="rust_bridge_settings_test")
|
||||
self.secrets = secrets
|
||||
|
||||
async def async_read_secret(
|
||||
self,
|
||||
secret_name: str,
|
||||
optional_params: dict[str, object] | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> str | None:
|
||||
return self.secrets.get(secret_name)
|
||||
|
||||
def sync_read_secret(
|
||||
self,
|
||||
secret_name: str,
|
||||
optional_params: dict[str, object] | None = None,
|
||||
timeout: float | httpx.Timeout | None = None,
|
||||
) -> str | None:
|
||||
return self.secrets.get(secret_name)
|
||||
|
||||
|
||||
def test_secret_prefers_the_secret_manager_and_falls_back_to_the_environment_on_a_miss(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "stale-env-key")
|
||||
monkeypatch.setenv("REDUCTO_API_KEY", "env-only-key")
|
||||
monkeypatch.setattr(litellm, "secret_manager_client", _VaultSecrets({"MISTRAL_API_KEY": "vault-key"}))
|
||||
monkeypatch.setattr(litellm, "_key_management_system", KeyManagementSystem.CUSTOM)
|
||||
monkeypatch.setattr(litellm, "_key_management_settings", KeyManagementSettings(access_mode="read_only"))
|
||||
|
||||
assert settings.secret("MISTRAL_API_KEY") == "vault-key"
|
||||
assert settings.secret("REDUCTO_API_KEY") == "env-only-key"
|
||||
assert settings.secret("ABSENT_KEY") is None
|
||||
|
||||
|
||||
def test_secret_reads_the_environment_without_a_secret_manager(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "env-key")
|
||||
monkeypatch.setattr(litellm, "secret_manager_client", None)
|
||||
|
||||
assert settings.secret("MISTRAL_API_KEY") == "env-key"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue