litellm/litellm-rust/crates/python-bridge/src/secrets/provider.rs
ryan-crabbe-berri db9eda19b4
fix(http_handler): name the timeout phase and limit that fired instead of "after None seconds" (#48250)
* fix(http_handler): name the timeout phase and the limit that fired instead of 'after None seconds'

Every HTTP verb on both handlers raised litellm.Timeout with the per-call
timeout argument interpolated into the message, which is None whenever the
caller relied on the client default, and always said "Connection" even for a
read timeout. One helper now reads the limit httpx actually enforced from the
failed request and names the phase that fired

Fixes #14635

* fix(rust_bridge): name the timeout phase in the vault timeout message to match the httpx handler

* fix(http_handler): drop the unreachable timeout fallbacks httpx already rules out

* refactor(rust_bridge): drop a doc comment that restated the timeout helper
2026-10-26 16:04:33 -07:00

153 lines
5.5 KiB
Rust

use std::time::Duration;
use super::operations::PythonReadRequest;
use litellm_secrets::{KeyManagementSystem, SecretValue};
use litellm_secrets_types::{
AwsOperationContext, CyberarkOperationContext, GoogleOperationContext,
HashicorpOperationContext, SecretOperationContext,
};
use pyo3::{exceptions::PyValueError, prelude::*, types::PyDict};
pub(super) fn read_request(
system: KeyManagementSystem,
secret_name: String,
optional_params: Option<&Bound<'_, PyAny>>,
timeout: Option<&Bound<'_, PyAny>>,
primary_secret_name: Option<String>,
synchronous: bool,
) -> PyResult<PythonReadRequest> {
let context = match system {
KeyManagementSystem::AwsSecretManager => {
let ignored = primary_secret_name
.as_ref()
.is_some_and(|value| !value.is_empty())
|| (synchronous
&& litellm_secrets::aws::secret_manager::is_bootstrap_key(&secret_name));
SecretOperationContext::Aws(if ignored {
AwsOperationContext::default()
} else {
aws_context(optional_params, timeout)?
})
}
KeyManagementSystem::HashicorpVault => {
SecretOperationContext::Hashicorp(vault_context(optional_params)?)
}
KeyManagementSystem::Cyberark => {
SecretOperationContext::Cyberark(CyberarkOperationContext::default())
}
KeyManagementSystem::GoogleSecretManager => {
SecretOperationContext::Google(GoogleOperationContext::default())
}
_ => {
return Err(PyValueError::new_err(
"secret manager does not support provider reads",
));
}
};
Ok(PythonReadRequest {
secret_name,
primary_secret_name,
context,
synchronous,
})
}
fn string_field(params: Option<&Bound<'_, PyDict>>, name: &str) -> PyResult<Option<String>> {
let value = params
.map(|params| params.get_item(name))
.transpose()?
.flatten();
match value {
Some(value) if value.is_truthy()? => value.extract().map(Some),
_ => Ok(None),
}
}
fn aws_context(
params: Option<&Bound<'_, PyAny>>,
timeout: Option<&Bound<'_, PyAny>>,
) -> PyResult<AwsOperationContext> {
let params = params
.filter(|value| !value.is_none())
.map(|value| value.cast::<PyDict>())
.transpose()?;
Ok(AwsOperationContext {
access_key_id: string_field(params, "aws_access_key_id")?.map(SecretValue::new),
secret_access_key: string_field(params, "aws_secret_access_key")?.map(SecretValue::new),
session_token: string_field(params, "aws_session_token")?.map(SecretValue::new),
region_name: string_field(params, "aws_region_name")?,
role_name: string_field(params, "aws_role_name")?,
session_name: string_field(params, "aws_session_name")?,
external_id: string_field(params, "aws_external_id")?.map(SecretValue::new),
profile_name: string_field(params, "aws_profile_name")?,
web_identity_token: string_field(params, "aws_web_identity_token")?.map(SecretValue::new),
sts_endpoint: string_field(params, "aws_sts_endpoint")?,
bedrock_runtime_endpoint: string_field(params, "aws_bedrock_runtime_endpoint")?,
timeout: read_timeout(timeout)?,
})
}
fn read_timeout(value: Option<&Bound<'_, PyAny>>) -> PyResult<Option<Duration>> {
read_timeout_seconds(value)?
.map(|value| {
Duration::try_from_secs_f64(value).map_err(|_| PyValueError::new_err("invalid timeout"))
})
.transpose()
}
pub(super) fn read_timeout_seconds(value: Option<&Bound<'_, PyAny>>) -> PyResult<Option<f64>> {
let Some(value) = value.filter(|value| !value.is_none()) else {
return Ok(None);
};
match value.extract::<f64>() {
Ok(value) => Ok(Some(value)),
Err(_) => value.getattr("read")?.extract::<Option<f64>>(),
}
}
fn vault_context(params: Option<&Bound<'_, PyAny>>) -> PyResult<HashicorpOperationContext> {
let params = params.and_then(|value| value.cast::<PyDict>().ok());
let nested = params
.map(|params| params.get_item("secret_manager_settings"))
.transpose()?
.flatten();
let source = nested
.as_ref()
.and_then(|value| value.cast::<PyDict>().ok())
.or(params);
Ok(HashicorpOperationContext {
namespace: vault_field(source, "namespace")?,
mount: vault_field(source, "mount")?,
path_prefix: vault_field(source, "path_prefix")?,
data_key: vault_field(source, "data")?,
timeout: None,
})
}
fn vault_field(params: Option<&Bound<'_, PyDict>>, name: &str) -> PyResult<Option<String>> {
let value = params
.map(|params| params.get_item(name))
.transpose()?
.flatten();
match value {
Some(value) if value.is_none() => Ok(None),
Some(value) => value.str()?.extract().map(Some),
None => Ok(None),
}
}
pub(super) fn mutation_context(
system: KeyManagementSystem,
optional_params: Option<&Bound<'_, PyAny>>,
timeout: Option<&Bound<'_, PyAny>>,
) -> PyResult<SecretOperationContext> {
if system == KeyManagementSystem::HashicorpVault {
return Ok(SecretOperationContext::Hashicorp(
HashicorpOperationContext {
timeout: read_timeout(timeout)?,
..vault_context(optional_params)?
},
));
}
Ok(SecretOperationContext::Default)
}