mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
fix(rust): read only the runtime defaults a call falls back to
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
27008cc355
commit
c3d8b6aa7d
3 changed files with 131 additions and 20 deletions
|
|
@ -2,6 +2,7 @@ use crate::cache::{CacheCall, Cached, PythonCache, Selection};
|
|||
use litellm_host_python::{PythonHostCalls, PythonOwned};
|
||||
|
||||
use bytes::Bytes;
|
||||
use litellm_auth::VertexParams;
|
||||
use litellm_host_python::{InvokeError, PythonBinding, from_py, to_py};
|
||||
use litellm_http::transport::Error as TransportError;
|
||||
use litellm_inference_messages::{
|
||||
|
|
@ -23,6 +24,7 @@ use crate::{
|
|||
errors::{RustUpstreamError, route_error_to_pyerr},
|
||||
marshal::{optional_timeout, project_optional_fields, public_response, python_timeout_seconds},
|
||||
routes::RequestView,
|
||||
runtime_defaults::RuntimeDefault,
|
||||
};
|
||||
|
||||
const ROUTE_HOST_MODULE: &str = "litellm.rust_bridge.messages.route_host";
|
||||
|
|
@ -143,7 +145,20 @@ impl MessagesPythonHost {
|
|||
let extra_headers = self.merged_headers(py, arguments)?;
|
||||
let provider_specific_header = self.provider_specific_header(py, arguments)?;
|
||||
let litellm_params = project_litellm_params(argument)?;
|
||||
let runtime_defaults = crate::runtime_defaults::read(py)?;
|
||||
let supplied = [VertexParams::PROJECT, VertexParams::LOCATION]
|
||||
.iter()
|
||||
.flat_map(|spec| spec.wire.iter().copied())
|
||||
.filter_map(|name| argument(name).map(|value| value.map(|_| name)).transpose())
|
||||
.collect::<PyResult<Vec<_>>>()?;
|
||||
let wanted = api_base
|
||||
.is_none()
|
||||
.then_some(RuntimeDefault::ApiBase)
|
||||
.into_iter()
|
||||
.chain(RuntimeDefault::vertex_fallbacks(|spec| {
|
||||
!spec.wire.iter().any(|name| supplied.contains(name))
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
let runtime_defaults = crate::runtime_defaults::read(py, &wanted)?;
|
||||
Ok(messages_body(body).and_then(|body| {
|
||||
Ok(MessagesCall {
|
||||
body,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use super::{document::FileDocumentInput, errors::to_pyerr as ocr_error_to_pyerr}
|
|||
use crate::{
|
||||
credentials::{self, CallerTokenProvider},
|
||||
marshal::{project_optional_fields, python_timeout_seconds, request_input_sources},
|
||||
runtime_defaults,
|
||||
runtime_defaults::{self, RuntimeDefault},
|
||||
};
|
||||
|
||||
/// What the host keeps after projection: the caller's token callable that answers the
|
||||
|
|
@ -124,8 +124,16 @@ pub(super) fn project_request(
|
|||
let names = specs.iter().map(|spec| spec.name).collect::<Vec<_>>();
|
||||
let optional_params =
|
||||
project_optional_fields(names.iter().copied(), |name| present(kwargs, bound, name))?;
|
||||
let runtime_defaults =
|
||||
runtime_defaults::read(bound.py()).map_err(|error| settings_error(bound.py(), error))?;
|
||||
let wanted = RuntimeDefault::vertex_fallbacks(|spec| {
|
||||
spec.wire.iter().all(|name| names.contains(name))
|
||||
&& !spec
|
||||
.wire
|
||||
.iter()
|
||||
.any(|name| optional_params.contains_key(*name))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let runtime_defaults = runtime_defaults::read(bound.py(), &wanted)
|
||||
.map_err(|error| settings_error(bound.py(), error))?;
|
||||
let input_sources = request_input_sources(
|
||||
kwargs,
|
||||
names
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use litellm_auth::{ParamSpec, VertexParams};
|
||||
use litellm_llms::base_llm::connection::RuntimeDefaults;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
|
|
@ -6,23 +7,64 @@ use crate::{
|
|||
python_settings::{PythonSettings, Snapshot},
|
||||
};
|
||||
|
||||
/// The `litellm.<name>` module defaults, read once per call into the typed runtime-defaults
|
||||
/// layer that provider resolvers rank between explicit arguments and the environment.
|
||||
pub(crate) fn read(py: Python<'_>) -> PyResult<RuntimeDefaults> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum RuntimeDefault {
|
||||
ApiBase,
|
||||
VertexProject,
|
||||
VertexLocation,
|
||||
}
|
||||
|
||||
impl RuntimeDefault {
|
||||
const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::ApiBase => "api_base",
|
||||
Self::VertexProject => "vertex_project",
|
||||
Self::VertexLocation => "vertex_location",
|
||||
}
|
||||
}
|
||||
|
||||
/// The Vertex defaults whose spec the call leaves `unset`.
|
||||
pub(crate) fn vertex_fallbacks(
|
||||
unset: impl Fn(&ParamSpec) -> bool,
|
||||
) -> impl Iterator<Item = Self> {
|
||||
[
|
||||
(Self::VertexProject, VertexParams::PROJECT),
|
||||
(Self::VertexLocation, VertexParams::LOCATION),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(move |(_, spec)| unset(spec))
|
||||
.map(|(default, _)| default)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `litellm.<name>` module defaults the call falls back to, read once per call into the
|
||||
/// typed runtime-defaults layer that provider resolvers rank between explicit arguments and
|
||||
/// the environment. A default the call does not fall back to is never read, so a malformed
|
||||
/// one cannot fail an unrelated call.
|
||||
pub(crate) fn read(py: Python<'_>, wanted: &[RuntimeDefault]) -> PyResult<RuntimeDefaults> {
|
||||
if wanted.is_empty() {
|
||||
return Ok(RuntimeDefaults::default());
|
||||
}
|
||||
match PythonSettings::ProviderDefaults.read_or_unset(py)? {
|
||||
Some(snapshot) => project(&snapshot).map_err(PyErr::from),
|
||||
Some(snapshot) => project(&snapshot, wanted).map_err(PyErr::from),
|
||||
None => Ok(RuntimeDefaults::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn project(snapshot: &Snapshot<'_>) -> Result<RuntimeDefaults, ProjectionError> {
|
||||
let read = |name: &'static str| {
|
||||
snapshot.read(&FieldSpec::new(name, |field| field.falsy_optional_string()))
|
||||
fn project(
|
||||
snapshot: &Snapshot<'_>,
|
||||
wanted: &[RuntimeDefault],
|
||||
) -> Result<RuntimeDefaults, ProjectionError> {
|
||||
let read = |default: RuntimeDefault| match wanted.contains(&default) {
|
||||
true => snapshot.read(&FieldSpec::new(default.name(), |field| {
|
||||
field.falsy_optional_string()
|
||||
})),
|
||||
false => Ok(None),
|
||||
};
|
||||
Ok(RuntimeDefaults {
|
||||
api_base: read("api_base")?,
|
||||
vertex_project: read("vertex_project")?,
|
||||
vertex_location: read("vertex_location")?,
|
||||
api_base: read(RuntimeDefault::ApiBase)?,
|
||||
vertex_project: read(RuntimeDefault::VertexProject)?,
|
||||
vertex_location: read(RuntimeDefault::VertexLocation)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -32,11 +74,20 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
fn projected(source: &std::ffi::CStr) -> Result<RuntimeDefaults, String> {
|
||||
const EVERY: [RuntimeDefault; 3] = [
|
||||
RuntimeDefault::ApiBase,
|
||||
RuntimeDefault::VertexProject,
|
||||
RuntimeDefault::VertexLocation,
|
||||
];
|
||||
|
||||
fn projected(
|
||||
source: &std::ffi::CStr,
|
||||
wanted: &[RuntimeDefault],
|
||||
) -> Result<RuntimeDefaults, String> {
|
||||
Python::initialize();
|
||||
Python::attach(|py| {
|
||||
let value = py.eval(source, None, None).unwrap();
|
||||
project(&PythonSettings::ProviderDefaults.snapshot(value))
|
||||
project(&PythonSettings::ProviderDefaults.snapshot(value), wanted)
|
||||
.map_err(|error| PyErr::from(error).value(py).to_string())
|
||||
})
|
||||
}
|
||||
|
|
@ -58,13 +109,43 @@ mod tests {
|
|||
#[case] source: &std::ffi::CStr,
|
||||
#[case] expected: RuntimeDefaults,
|
||||
) {
|
||||
assert_eq!(projected(source).unwrap(), expected);
|
||||
assert_eq!(projected(source, &EVERY).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::nothing_wanted(&[], RuntimeDefaults::default())]
|
||||
#[case::only_the_api_base(
|
||||
&[RuntimeDefault::ApiBase],
|
||||
RuntimeDefaults { api_base: Some("https://runtime.test".into()), ..Default::default() }
|
||||
)]
|
||||
fn a_malformed_default_the_call_does_not_use_is_never_read(
|
||||
#[case] wanted: &[RuntimeDefault],
|
||||
#[case] expected: RuntimeDefaults,
|
||||
) {
|
||||
let source = c"__import__('types').SimpleNamespace(api_base='https://runtime.test', vertex_project=5, vertex_location=5)";
|
||||
assert_eq!(projected(source, wanted).unwrap(), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::both_unset(&[], vec![RuntimeDefault::VertexProject, RuntimeDefault::VertexLocation])]
|
||||
#[case::project_on_the_call(&["vertex_ai_project"], vec![RuntimeDefault::VertexLocation])]
|
||||
#[case::both_on_the_call(&["vertex_project", "vertex_location"], vec![])]
|
||||
fn vertex_fallbacks_skip_the_settings_the_call_supplies(
|
||||
#[case] supplied: &[&str],
|
||||
#[case] expected: Vec<RuntimeDefault>,
|
||||
) {
|
||||
let fallbacks = RuntimeDefault::vertex_fallbacks(|spec| {
|
||||
!spec.wire.iter().any(|name| supplied.contains(name))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(fallbacks, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_misconfigured_module_default_names_the_setting() {
|
||||
let error = projected(
|
||||
c"__import__('types').SimpleNamespace(api_base=1, vertex_project=None, vertex_location=None)",
|
||||
&EVERY,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("provider_defaults.api_base"), "{error}");
|
||||
|
|
@ -81,12 +162,19 @@ mod tests {
|
|||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let first =
|
||||
project(&PythonSettings::ProviderDefaults.snapshot(namespace.clone())).unwrap();
|
||||
let first = project(
|
||||
&PythonSettings::ProviderDefaults.snapshot(namespace.clone()),
|
||||
&EVERY,
|
||||
)
|
||||
.unwrap();
|
||||
namespace
|
||||
.setattr("api_base", "https://second.test")
|
||||
.unwrap();
|
||||
let second = project(&PythonSettings::ProviderDefaults.snapshot(namespace)).unwrap();
|
||||
let second = project(
|
||||
&PythonSettings::ProviderDefaults.snapshot(namespace),
|
||||
&EVERY,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(first.api_base.as_deref(), second.api_base.as_deref()),
|
||||
(Some("https://first.test"), Some("https://second.test"))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue