mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
OCR transformations, BaseOcrConfig with its response and connection types, the OCR error, and the HTTP pieces (custom_httpx: http_handler, transport, media, llm_http_handler with OcrClient and the request/response handler) now live in litellm-llms at their Python paths. Provider code no longer reaches into the route: it gets the caller's hooks through a route-neutral CallHooks trait that core implements over its host, and core dispatches to llm_http_handler::ocr with the concrete config, the way Python calls base_llm_http_handler.ocr(provider_config=...). Core keeps the route: entrypoint, request types, credential fallback, provider dispatch, the machine and hook glue. ocr/mod.rs no longer re-exports anything, provider constants moved next to their only users, and provider tests that drive the whole route moved to core's route test files. Twenty-two of those were exact copies of tests already there and were dropped; every one still runs once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
6.2 KiB
Rust
171 lines
6.2 KiB
Rust
use litellm_core::{Error, audio_transcription, chat_completions, messages, responses};
|
|
use litellm_llms::{
|
|
base_llm::ocr::error::Error as OcrError, custom_httpx::transport::Error as TransportError,
|
|
};
|
|
use pyo3::{
|
|
exceptions::{PyRuntimeError, PyValueError},
|
|
prelude::*,
|
|
};
|
|
|
|
pyo3::create_exception!(
|
|
_native,
|
|
RustBridgeDeclined,
|
|
pyo3::exceptions::PyException,
|
|
"The route declined before calling the provider, so the host may retry on its own path."
|
|
);
|
|
|
|
pyo3::create_exception!(
|
|
_native,
|
|
RustUpstreamError,
|
|
pyo3::exceptions::PyException,
|
|
"The provider call was already issued and failed. Args are (status, message); status is 0 when there was no HTTP response."
|
|
);
|
|
|
|
fn auth_is_value_error(error: &litellm_auth::Error) -> bool {
|
|
!matches!(error, litellm_auth::Error::MissingApiKey { .. })
|
|
}
|
|
|
|
pub(crate) fn messages_error_to_pyerr(error: messages::Error) -> PyErr {
|
|
core_error_to_pyerr(error.into())
|
|
}
|
|
|
|
pub(crate) fn audio_transcription_error_to_pyerr(error: audio_transcription::Error) -> PyErr {
|
|
core_error_to_pyerr(error.into())
|
|
}
|
|
|
|
pub(crate) fn responses_error_to_pyerr(error: responses::Error) -> PyErr {
|
|
core_error_to_pyerr(error.into())
|
|
}
|
|
|
|
pub(crate) fn core_error_to_pyerr(error: Error) -> PyErr {
|
|
let value_error = match &error {
|
|
Error::Ocr(error) => {
|
|
error.is_request()
|
|
|| matches!(
|
|
error,
|
|
OcrError::Auth(_)
|
|
| OcrError::InvalidProvider(_)
|
|
| OcrError::InvalidRequest(_)
|
|
| OcrError::MissingField(_)
|
|
| OcrError::MissingDocumentUrl
|
|
)
|
|
}
|
|
Error::Messages(error) => match error {
|
|
messages::Error::Auth(source) => auth_is_value_error(source),
|
|
_ => error.is_request(),
|
|
},
|
|
Error::AudioTranscription(error) => match error {
|
|
audio_transcription::Error::Auth(source) => auth_is_value_error(source),
|
|
audio_transcription::Error::InvalidProvider(_)
|
|
| audio_transcription::Error::InvalidRequest(_)
|
|
| audio_transcription::Error::Headers(_)
|
|
| audio_transcription::Error::InvalidType { .. }
|
|
| audio_transcription::Error::MissingField(_)
|
|
| audio_transcription::Error::Aws(_) => true,
|
|
_ => false,
|
|
},
|
|
Error::ChatCompletions(error) => match error {
|
|
chat_completions::Error::Auth(source) => auth_is_value_error(source),
|
|
chat_completions::Error::InvalidProvider(_)
|
|
| chat_completions::Error::InvalidRequest(_)
|
|
| chat_completions::Error::Headers(_)
|
|
| chat_completions::Error::InvalidType { .. }
|
|
| chat_completions::Error::MissingField(_)
|
|
| chat_completions::Error::Aws(_) => true,
|
|
_ => false,
|
|
},
|
|
Error::Responses(error) => match error {
|
|
responses::Error::Auth(source) => auth_is_value_error(source),
|
|
responses::Error::InvalidProvider(_)
|
|
| responses::Error::InvalidRequest(_)
|
|
| responses::Error::Headers(_) => true,
|
|
_ => false,
|
|
},
|
|
};
|
|
if value_error {
|
|
PyValueError::new_err(error.to_string())
|
|
} else {
|
|
PyRuntimeError::new_err(error.to_string())
|
|
}
|
|
}
|
|
|
|
/// Map a route error for a route whose host keeps a Python implementation.
|
|
///
|
|
/// The distinction the host needs is whether the provider was already called.
|
|
/// Everything raised before the request goes out is safe for the host to retry
|
|
/// on its own path; anything after it is not, because the provider has already
|
|
/// done the work and billed for it.
|
|
pub(crate) fn chat_completions_error_to_pyerr(error: chat_completions::Error) -> PyErr {
|
|
use chat_completions::Error;
|
|
match error {
|
|
Error::Unsupported(_)
|
|
| Error::Auth(_)
|
|
| Error::Aws(_)
|
|
| Error::InvalidProvider(_)
|
|
| Error::InvalidRequest(_)
|
|
| Error::InvalidType { .. }
|
|
| Error::MissingField(_)
|
|
| Error::Headers(_)
|
|
| Error::Transport(TransportError::Connect(_)) => {
|
|
RustBridgeDeclined::new_err(error.to_string())
|
|
}
|
|
Error::Transport(TransportError::Http { status, body }) => {
|
|
RustUpstreamError::new_err((status, body))
|
|
}
|
|
Error::Transport(TransportError::Network(message)) | Error::InvalidResponse(message) => {
|
|
RustUpstreamError::new_err((0u16, message))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn transport_status_and_dispatch_certainty_survive_python_mapping() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let connect = chat_completions_error_to_pyerr(
|
|
TransportError::Connect("unreachable".into()).into(),
|
|
);
|
|
assert!(connect.is_instance_of::<RustBridgeDeclined>(py));
|
|
let network =
|
|
chat_completions_error_to_pyerr(TransportError::Network("timed out".into()).into());
|
|
assert!(network.is_instance_of::<RustUpstreamError>(py));
|
|
let upstream = chat_completions_error_to_pyerr(
|
|
TransportError::Http {
|
|
status: 429,
|
|
body: "slow down".into(),
|
|
}
|
|
.into(),
|
|
);
|
|
assert_eq!(
|
|
upstream
|
|
.value(py)
|
|
.getattr("args")
|
|
.unwrap()
|
|
.extract::<(u16, String)>()
|
|
.unwrap(),
|
|
(429, "slow down".into())
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn missing_api_key_stays_a_runtime_error_while_other_auth_failures_are_value_errors() {
|
|
Python::initialize();
|
|
Python::attach(|py| {
|
|
let missing = messages_error_to_pyerr(messages::Error::Auth(
|
|
litellm_auth::Error::MissingApiKey {
|
|
provider: "Anthropic",
|
|
environment_variable: "ANTHROPIC_API_KEY",
|
|
},
|
|
));
|
|
assert!(missing.is_instance_of::<PyRuntimeError>(py));
|
|
let invalid =
|
|
messages_error_to_pyerr(messages::Error::Auth(litellm_auth::Error::InvalidHeader));
|
|
assert!(invalid.is_instance_of::<PyValueError>(py));
|
|
});
|
|
}
|
|
}
|