mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat: make rust OCR async-first
This commit is contained in:
parent
3818d6401c
commit
725deeed19
13 changed files with 1398 additions and 195 deletions
118
litellm-rust/Cargo.lock
generated
118
litellm-rust/Cargo.lock
generated
|
|
@ -137,12 +137,24 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -152,6 +164,21 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
|
|
@ -168,12 +195,34 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
|
|
@ -192,8 +241,10 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
|
|
@ -238,6 +289,31 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
|
@ -293,6 +369,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
|
@ -445,6 +522,16 @@ dependencies = [
|
|||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
|
|
@ -511,7 +598,9 @@ dependencies = [
|
|||
"litellm-core",
|
||||
"litellm-providers",
|
||||
"pyo3",
|
||||
"pyo3-async-runtimes",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -633,6 +722,19 @@ dependencies = [
|
|||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-async-runtimes"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "977dc837525cfd22919ba6a831413854beb7c99a256c03bf8624ad707e45810e"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pyo3",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.23.5"
|
||||
|
|
@ -815,9 +917,8 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
|
|
@ -1218,6 +1319,19 @@ dependencies = [
|
|||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ repository = "https://github.com/BerriAI/litellm"
|
|||
litellm-core = { path = "crates/core" }
|
||||
litellm-providers = { path = "crates/providers" }
|
||||
pyo3 = "0.23.5"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
pyo3-async-runtimes = { version = "0.23.0", features = ["tokio-runtime"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "http2"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "2.0"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ pub enum CoreError {
|
|||
MissingField(&'static str),
|
||||
#[error("invalid response: {0}")]
|
||||
InvalidResponse(String),
|
||||
#[error("invalid provider: {0}")]
|
||||
InvalidProvider(String),
|
||||
#[error("{0}")]
|
||||
Auth(String),
|
||||
#[error("OCR request failed with status {status}: {body}")]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod error;
|
||||
pub mod ocr;
|
||||
pub mod providers;
|
||||
pub mod realtime;
|
||||
|
||||
pub use error::{CoreError, CoreResult};
|
||||
pub use providers::LlmProvider;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::CoreResult;
|
|||
|
||||
use super::types::{OcrRequestData, OcrResponseData};
|
||||
|
||||
pub trait OcrProviderConfig {
|
||||
pub trait OcrProviderConfig: Sync {
|
||||
fn supported_ocr_params(&self) -> &'static [&'static str];
|
||||
|
||||
fn map_ocr_params(&self, non_default_params: &Map<String, Value>) -> Map<String, Value> {
|
||||
|
|
|
|||
3
litellm-rust/crates/core/src/providers/mod.rs
Normal file
3
litellm-rust/crates/core/src/providers/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod provider;
|
||||
|
||||
pub use provider::LlmProvider;
|
||||
659
litellm-rust/crates/core/src/providers/provider.rs
Normal file
659
litellm-rust/crates/core/src/providers/provider.rs
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::CoreError;
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LlmProvider {
|
||||
Openai,
|
||||
Chatgpt,
|
||||
OpenaiLike,
|
||||
JinaAi,
|
||||
Xai,
|
||||
Zai,
|
||||
CustomOpenai,
|
||||
TextCompletionOpenai,
|
||||
Cohere,
|
||||
CohereChat,
|
||||
Clarifai,
|
||||
Anthropic,
|
||||
AnthropicText,
|
||||
Bytez,
|
||||
Replicate,
|
||||
Reducto,
|
||||
Runwayml,
|
||||
AwsPolly,
|
||||
Huggingface,
|
||||
TogetherAi,
|
||||
Openrouter,
|
||||
Datarobot,
|
||||
VertexAi,
|
||||
VertexAiBeta,
|
||||
Gemini,
|
||||
Ai21,
|
||||
Baseten,
|
||||
BlackForestLabs,
|
||||
Azure,
|
||||
AzureText,
|
||||
AzureAi,
|
||||
Sagemaker,
|
||||
SagemakerChat,
|
||||
SagemakerNova,
|
||||
Bedrock,
|
||||
Vllm,
|
||||
NlpCloud,
|
||||
Petals,
|
||||
Oobabooga,
|
||||
Ollama,
|
||||
OllamaChat,
|
||||
Deepinfra,
|
||||
Perplexity,
|
||||
Mistral,
|
||||
Milvus,
|
||||
Groq,
|
||||
A2a,
|
||||
Gigachat,
|
||||
NvidiaNim,
|
||||
NvidiaRiva,
|
||||
Soniox,
|
||||
Cerebras,
|
||||
Ai21Chat,
|
||||
Volcengine,
|
||||
Codestral,
|
||||
TextCompletionCodestral,
|
||||
Dashscope,
|
||||
Modelscope,
|
||||
Moonshot,
|
||||
Publicai,
|
||||
V0,
|
||||
Morph,
|
||||
LambdaAi,
|
||||
Inception,
|
||||
TextCompletionInception,
|
||||
Deepseek,
|
||||
Sambanova,
|
||||
Maritalk,
|
||||
Voyage,
|
||||
Cloudflare,
|
||||
Xinference,
|
||||
FireworksAi,
|
||||
Friendliai,
|
||||
FeatherlessAi,
|
||||
Watsonx,
|
||||
WatsonxText,
|
||||
Triton,
|
||||
Predibase,
|
||||
Databricks,
|
||||
Empower,
|
||||
Github,
|
||||
Ragflow,
|
||||
Compactifai,
|
||||
DockerModelRunner,
|
||||
Custom,
|
||||
LitellmProxy,
|
||||
HostedVllm,
|
||||
Llamafile,
|
||||
LmStudio,
|
||||
Galadriel,
|
||||
Nebius,
|
||||
Infinity,
|
||||
Deepgram,
|
||||
Elevenlabs,
|
||||
Novita,
|
||||
AiohttpOpenai,
|
||||
Langfuse,
|
||||
Humanloop,
|
||||
Topaz,
|
||||
Sap,
|
||||
Assemblyai,
|
||||
CharityEngine,
|
||||
GithubCopilot,
|
||||
Snowflake,
|
||||
GradientAi,
|
||||
MetaLlama,
|
||||
Nscale,
|
||||
PgVector,
|
||||
S3Vectors,
|
||||
Helicone,
|
||||
Hyperbolic,
|
||||
Recraft,
|
||||
FalAi,
|
||||
Stability,
|
||||
Heroku,
|
||||
Aiml,
|
||||
Cometapi,
|
||||
Oci,
|
||||
AutoRouter,
|
||||
VercelAiGateway,
|
||||
Dotprompt,
|
||||
Manus,
|
||||
Wandb,
|
||||
Ovhcloud,
|
||||
Scaleway,
|
||||
Lemonade,
|
||||
AmazonNova,
|
||||
A2aAgent,
|
||||
Langgraph,
|
||||
Langflow,
|
||||
Minimax,
|
||||
Synthetic,
|
||||
Apertis,
|
||||
NanoGpt,
|
||||
Poe,
|
||||
Chutes,
|
||||
Neosantara,
|
||||
Parasail,
|
||||
XiaomiMimo,
|
||||
Tensormesh,
|
||||
Libertai,
|
||||
Pinstripes,
|
||||
Darkbloom,
|
||||
LitellmAgent,
|
||||
Cursor,
|
||||
BedrockMantle,
|
||||
}
|
||||
|
||||
impl LlmProvider {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
LlmProvider::Openai => "openai",
|
||||
LlmProvider::Chatgpt => "chatgpt",
|
||||
LlmProvider::OpenaiLike => "openai_like",
|
||||
LlmProvider::JinaAi => "jina_ai",
|
||||
LlmProvider::Xai => "xai",
|
||||
LlmProvider::Zai => "zai",
|
||||
LlmProvider::CustomOpenai => "custom_openai",
|
||||
LlmProvider::TextCompletionOpenai => "text-completion-openai",
|
||||
LlmProvider::Cohere => "cohere",
|
||||
LlmProvider::CohereChat => "cohere_chat",
|
||||
LlmProvider::Clarifai => "clarifai",
|
||||
LlmProvider::Anthropic => "anthropic",
|
||||
LlmProvider::AnthropicText => "anthropic_text",
|
||||
LlmProvider::Bytez => "bytez",
|
||||
LlmProvider::Replicate => "replicate",
|
||||
LlmProvider::Reducto => "reducto",
|
||||
LlmProvider::Runwayml => "runwayml",
|
||||
LlmProvider::AwsPolly => "aws_polly",
|
||||
LlmProvider::Huggingface => "huggingface",
|
||||
LlmProvider::TogetherAi => "together_ai",
|
||||
LlmProvider::Openrouter => "openrouter",
|
||||
LlmProvider::Datarobot => "datarobot",
|
||||
LlmProvider::VertexAi => "vertex_ai",
|
||||
LlmProvider::VertexAiBeta => "vertex_ai_beta",
|
||||
LlmProvider::Gemini => "gemini",
|
||||
LlmProvider::Ai21 => "ai21",
|
||||
LlmProvider::Baseten => "baseten",
|
||||
LlmProvider::BlackForestLabs => "black_forest_labs",
|
||||
LlmProvider::Azure => "azure",
|
||||
LlmProvider::AzureText => "azure_text",
|
||||
LlmProvider::AzureAi => "azure_ai",
|
||||
LlmProvider::Sagemaker => "sagemaker",
|
||||
LlmProvider::SagemakerChat => "sagemaker_chat",
|
||||
LlmProvider::SagemakerNova => "sagemaker_nova",
|
||||
LlmProvider::Bedrock => "bedrock",
|
||||
LlmProvider::Vllm => "vllm",
|
||||
LlmProvider::NlpCloud => "nlp_cloud",
|
||||
LlmProvider::Petals => "petals",
|
||||
LlmProvider::Oobabooga => "oobabooga",
|
||||
LlmProvider::Ollama => "ollama",
|
||||
LlmProvider::OllamaChat => "ollama_chat",
|
||||
LlmProvider::Deepinfra => "deepinfra",
|
||||
LlmProvider::Perplexity => "perplexity",
|
||||
LlmProvider::Mistral => "mistral",
|
||||
LlmProvider::Milvus => "milvus",
|
||||
LlmProvider::Groq => "groq",
|
||||
LlmProvider::A2a => "a2a",
|
||||
LlmProvider::Gigachat => "gigachat",
|
||||
LlmProvider::NvidiaNim => "nvidia_nim",
|
||||
LlmProvider::NvidiaRiva => "nvidia_riva",
|
||||
LlmProvider::Soniox => "soniox",
|
||||
LlmProvider::Cerebras => "cerebras",
|
||||
LlmProvider::Ai21Chat => "ai21_chat",
|
||||
LlmProvider::Volcengine => "volcengine",
|
||||
LlmProvider::Codestral => "codestral",
|
||||
LlmProvider::TextCompletionCodestral => "text-completion-codestral",
|
||||
LlmProvider::Dashscope => "dashscope",
|
||||
LlmProvider::Modelscope => "modelscope",
|
||||
LlmProvider::Moonshot => "moonshot",
|
||||
LlmProvider::Publicai => "publicai",
|
||||
LlmProvider::V0 => "v0",
|
||||
LlmProvider::Morph => "morph",
|
||||
LlmProvider::LambdaAi => "lambda_ai",
|
||||
LlmProvider::Inception => "inception",
|
||||
LlmProvider::TextCompletionInception => "text-completion-inception",
|
||||
LlmProvider::Deepseek => "deepseek",
|
||||
LlmProvider::Sambanova => "sambanova",
|
||||
LlmProvider::Maritalk => "maritalk",
|
||||
LlmProvider::Voyage => "voyage",
|
||||
LlmProvider::Cloudflare => "cloudflare",
|
||||
LlmProvider::Xinference => "xinference",
|
||||
LlmProvider::FireworksAi => "fireworks_ai",
|
||||
LlmProvider::Friendliai => "friendliai",
|
||||
LlmProvider::FeatherlessAi => "featherless_ai",
|
||||
LlmProvider::Watsonx => "watsonx",
|
||||
LlmProvider::WatsonxText => "watsonx_text",
|
||||
LlmProvider::Triton => "triton",
|
||||
LlmProvider::Predibase => "predibase",
|
||||
LlmProvider::Databricks => "databricks",
|
||||
LlmProvider::Empower => "empower",
|
||||
LlmProvider::Github => "github",
|
||||
LlmProvider::Ragflow => "ragflow",
|
||||
LlmProvider::Compactifai => "compactifai",
|
||||
LlmProvider::DockerModelRunner => "docker_model_runner",
|
||||
LlmProvider::Custom => "custom",
|
||||
LlmProvider::LitellmProxy => "litellm_proxy",
|
||||
LlmProvider::HostedVllm => "hosted_vllm",
|
||||
LlmProvider::Llamafile => "llamafile",
|
||||
LlmProvider::LmStudio => "lm_studio",
|
||||
LlmProvider::Galadriel => "galadriel",
|
||||
LlmProvider::Nebius => "nebius",
|
||||
LlmProvider::Infinity => "infinity",
|
||||
LlmProvider::Deepgram => "deepgram",
|
||||
LlmProvider::Elevenlabs => "elevenlabs",
|
||||
LlmProvider::Novita => "novita",
|
||||
LlmProvider::AiohttpOpenai => "aiohttp_openai",
|
||||
LlmProvider::Langfuse => "langfuse",
|
||||
LlmProvider::Humanloop => "humanloop",
|
||||
LlmProvider::Topaz => "topaz",
|
||||
LlmProvider::Sap => "sap",
|
||||
LlmProvider::Assemblyai => "assemblyai",
|
||||
LlmProvider::CharityEngine => "charity_engine",
|
||||
LlmProvider::GithubCopilot => "github_copilot",
|
||||
LlmProvider::Snowflake => "snowflake",
|
||||
LlmProvider::GradientAi => "gradient_ai",
|
||||
LlmProvider::MetaLlama => "meta_llama",
|
||||
LlmProvider::Nscale => "nscale",
|
||||
LlmProvider::PgVector => "pg_vector",
|
||||
LlmProvider::S3Vectors => "s3_vectors",
|
||||
LlmProvider::Helicone => "helicone",
|
||||
LlmProvider::Hyperbolic => "hyperbolic",
|
||||
LlmProvider::Recraft => "recraft",
|
||||
LlmProvider::FalAi => "fal_ai",
|
||||
LlmProvider::Stability => "stability",
|
||||
LlmProvider::Heroku => "heroku",
|
||||
LlmProvider::Aiml => "aiml",
|
||||
LlmProvider::Cometapi => "cometapi",
|
||||
LlmProvider::Oci => "oci",
|
||||
LlmProvider::AutoRouter => "auto_router",
|
||||
LlmProvider::VercelAiGateway => "vercel_ai_gateway",
|
||||
LlmProvider::Dotprompt => "dotprompt",
|
||||
LlmProvider::Manus => "manus",
|
||||
LlmProvider::Wandb => "wandb",
|
||||
LlmProvider::Ovhcloud => "ovhcloud",
|
||||
LlmProvider::Scaleway => "scaleway",
|
||||
LlmProvider::Lemonade => "lemonade",
|
||||
LlmProvider::AmazonNova => "amazon_nova",
|
||||
LlmProvider::A2aAgent => "a2a_agent",
|
||||
LlmProvider::Langgraph => "langgraph",
|
||||
LlmProvider::Langflow => "langflow",
|
||||
LlmProvider::Minimax => "minimax",
|
||||
LlmProvider::Synthetic => "synthetic",
|
||||
LlmProvider::Apertis => "apertis",
|
||||
LlmProvider::NanoGpt => "nano-gpt",
|
||||
LlmProvider::Poe => "poe",
|
||||
LlmProvider::Chutes => "chutes",
|
||||
LlmProvider::Neosantara => "neosantara",
|
||||
LlmProvider::Parasail => "parasail",
|
||||
LlmProvider::XiaomiMimo => "xiaomi_mimo",
|
||||
LlmProvider::Tensormesh => "tensormesh",
|
||||
LlmProvider::Libertai => "libertai",
|
||||
LlmProvider::Pinstripes => "pinstripes",
|
||||
LlmProvider::Darkbloom => "darkbloom",
|
||||
LlmProvider::LitellmAgent => "litellm_agent",
|
||||
LlmProvider::Cursor => "cursor",
|
||||
LlmProvider::BedrockMantle => "bedrock_mantle",
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub const ALL: [LlmProvider; 146] = [
|
||||
LlmProvider::Openai,
|
||||
LlmProvider::Chatgpt,
|
||||
LlmProvider::OpenaiLike,
|
||||
LlmProvider::JinaAi,
|
||||
LlmProvider::Xai,
|
||||
LlmProvider::Zai,
|
||||
LlmProvider::CustomOpenai,
|
||||
LlmProvider::TextCompletionOpenai,
|
||||
LlmProvider::Cohere,
|
||||
LlmProvider::CohereChat,
|
||||
LlmProvider::Clarifai,
|
||||
LlmProvider::Anthropic,
|
||||
LlmProvider::AnthropicText,
|
||||
LlmProvider::Bytez,
|
||||
LlmProvider::Replicate,
|
||||
LlmProvider::Reducto,
|
||||
LlmProvider::Runwayml,
|
||||
LlmProvider::AwsPolly,
|
||||
LlmProvider::Huggingface,
|
||||
LlmProvider::TogetherAi,
|
||||
LlmProvider::Openrouter,
|
||||
LlmProvider::Datarobot,
|
||||
LlmProvider::VertexAi,
|
||||
LlmProvider::VertexAiBeta,
|
||||
LlmProvider::Gemini,
|
||||
LlmProvider::Ai21,
|
||||
LlmProvider::Baseten,
|
||||
LlmProvider::BlackForestLabs,
|
||||
LlmProvider::Azure,
|
||||
LlmProvider::AzureText,
|
||||
LlmProvider::AzureAi,
|
||||
LlmProvider::Sagemaker,
|
||||
LlmProvider::SagemakerChat,
|
||||
LlmProvider::SagemakerNova,
|
||||
LlmProvider::Bedrock,
|
||||
LlmProvider::Vllm,
|
||||
LlmProvider::NlpCloud,
|
||||
LlmProvider::Petals,
|
||||
LlmProvider::Oobabooga,
|
||||
LlmProvider::Ollama,
|
||||
LlmProvider::OllamaChat,
|
||||
LlmProvider::Deepinfra,
|
||||
LlmProvider::Perplexity,
|
||||
LlmProvider::Mistral,
|
||||
LlmProvider::Milvus,
|
||||
LlmProvider::Groq,
|
||||
LlmProvider::A2a,
|
||||
LlmProvider::Gigachat,
|
||||
LlmProvider::NvidiaNim,
|
||||
LlmProvider::NvidiaRiva,
|
||||
LlmProvider::Soniox,
|
||||
LlmProvider::Cerebras,
|
||||
LlmProvider::Ai21Chat,
|
||||
LlmProvider::Volcengine,
|
||||
LlmProvider::Codestral,
|
||||
LlmProvider::TextCompletionCodestral,
|
||||
LlmProvider::Dashscope,
|
||||
LlmProvider::Modelscope,
|
||||
LlmProvider::Moonshot,
|
||||
LlmProvider::Publicai,
|
||||
LlmProvider::V0,
|
||||
LlmProvider::Morph,
|
||||
LlmProvider::LambdaAi,
|
||||
LlmProvider::Inception,
|
||||
LlmProvider::TextCompletionInception,
|
||||
LlmProvider::Deepseek,
|
||||
LlmProvider::Sambanova,
|
||||
LlmProvider::Maritalk,
|
||||
LlmProvider::Voyage,
|
||||
LlmProvider::Cloudflare,
|
||||
LlmProvider::Xinference,
|
||||
LlmProvider::FireworksAi,
|
||||
LlmProvider::Friendliai,
|
||||
LlmProvider::FeatherlessAi,
|
||||
LlmProvider::Watsonx,
|
||||
LlmProvider::WatsonxText,
|
||||
LlmProvider::Triton,
|
||||
LlmProvider::Predibase,
|
||||
LlmProvider::Databricks,
|
||||
LlmProvider::Empower,
|
||||
LlmProvider::Github,
|
||||
LlmProvider::Ragflow,
|
||||
LlmProvider::Compactifai,
|
||||
LlmProvider::DockerModelRunner,
|
||||
LlmProvider::Custom,
|
||||
LlmProvider::LitellmProxy,
|
||||
LlmProvider::HostedVllm,
|
||||
LlmProvider::Llamafile,
|
||||
LlmProvider::LmStudio,
|
||||
LlmProvider::Galadriel,
|
||||
LlmProvider::Nebius,
|
||||
LlmProvider::Infinity,
|
||||
LlmProvider::Deepgram,
|
||||
LlmProvider::Elevenlabs,
|
||||
LlmProvider::Novita,
|
||||
LlmProvider::AiohttpOpenai,
|
||||
LlmProvider::Langfuse,
|
||||
LlmProvider::Humanloop,
|
||||
LlmProvider::Topaz,
|
||||
LlmProvider::Sap,
|
||||
LlmProvider::Assemblyai,
|
||||
LlmProvider::CharityEngine,
|
||||
LlmProvider::GithubCopilot,
|
||||
LlmProvider::Snowflake,
|
||||
LlmProvider::GradientAi,
|
||||
LlmProvider::MetaLlama,
|
||||
LlmProvider::Nscale,
|
||||
LlmProvider::PgVector,
|
||||
LlmProvider::S3Vectors,
|
||||
LlmProvider::Helicone,
|
||||
LlmProvider::Hyperbolic,
|
||||
LlmProvider::Recraft,
|
||||
LlmProvider::FalAi,
|
||||
LlmProvider::Stability,
|
||||
LlmProvider::Heroku,
|
||||
LlmProvider::Aiml,
|
||||
LlmProvider::Cometapi,
|
||||
LlmProvider::Oci,
|
||||
LlmProvider::AutoRouter,
|
||||
LlmProvider::VercelAiGateway,
|
||||
LlmProvider::Dotprompt,
|
||||
LlmProvider::Manus,
|
||||
LlmProvider::Wandb,
|
||||
LlmProvider::Ovhcloud,
|
||||
LlmProvider::Scaleway,
|
||||
LlmProvider::Lemonade,
|
||||
LlmProvider::AmazonNova,
|
||||
LlmProvider::A2aAgent,
|
||||
LlmProvider::Langgraph,
|
||||
LlmProvider::Langflow,
|
||||
LlmProvider::Minimax,
|
||||
LlmProvider::Synthetic,
|
||||
LlmProvider::Apertis,
|
||||
LlmProvider::NanoGpt,
|
||||
LlmProvider::Poe,
|
||||
LlmProvider::Chutes,
|
||||
LlmProvider::Neosantara,
|
||||
LlmProvider::Parasail,
|
||||
LlmProvider::XiaomiMimo,
|
||||
LlmProvider::Tensormesh,
|
||||
LlmProvider::Libertai,
|
||||
LlmProvider::Pinstripes,
|
||||
LlmProvider::Darkbloom,
|
||||
LlmProvider::LitellmAgent,
|
||||
LlmProvider::Cursor,
|
||||
LlmProvider::BedrockMantle,
|
||||
];
|
||||
}
|
||||
|
||||
impl fmt::Display for LlmProvider {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for LlmProvider {
|
||||
type Err = CoreError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
LlmProvider::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|provider| provider.as_str() == value)
|
||||
.ok_or_else(|| CoreError::InvalidProvider(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PYTHON_LLM_PROVIDER_VALUES: &[&str] = &[
|
||||
"openai",
|
||||
"chatgpt",
|
||||
"openai_like",
|
||||
"jina_ai",
|
||||
"xai",
|
||||
"zai",
|
||||
"custom_openai",
|
||||
"text-completion-openai",
|
||||
"cohere",
|
||||
"cohere_chat",
|
||||
"clarifai",
|
||||
"anthropic",
|
||||
"anthropic_text",
|
||||
"bytez",
|
||||
"replicate",
|
||||
"reducto",
|
||||
"runwayml",
|
||||
"aws_polly",
|
||||
"huggingface",
|
||||
"together_ai",
|
||||
"openrouter",
|
||||
"datarobot",
|
||||
"vertex_ai",
|
||||
"vertex_ai_beta",
|
||||
"gemini",
|
||||
"ai21",
|
||||
"baseten",
|
||||
"black_forest_labs",
|
||||
"azure",
|
||||
"azure_text",
|
||||
"azure_ai",
|
||||
"sagemaker",
|
||||
"sagemaker_chat",
|
||||
"sagemaker_nova",
|
||||
"bedrock",
|
||||
"vllm",
|
||||
"nlp_cloud",
|
||||
"petals",
|
||||
"oobabooga",
|
||||
"ollama",
|
||||
"ollama_chat",
|
||||
"deepinfra",
|
||||
"perplexity",
|
||||
"mistral",
|
||||
"milvus",
|
||||
"groq",
|
||||
"a2a",
|
||||
"gigachat",
|
||||
"nvidia_nim",
|
||||
"nvidia_riva",
|
||||
"soniox",
|
||||
"cerebras",
|
||||
"ai21_chat",
|
||||
"volcengine",
|
||||
"codestral",
|
||||
"text-completion-codestral",
|
||||
"dashscope",
|
||||
"modelscope",
|
||||
"moonshot",
|
||||
"publicai",
|
||||
"v0",
|
||||
"morph",
|
||||
"lambda_ai",
|
||||
"inception",
|
||||
"text-completion-inception",
|
||||
"deepseek",
|
||||
"sambanova",
|
||||
"maritalk",
|
||||
"voyage",
|
||||
"cloudflare",
|
||||
"xinference",
|
||||
"fireworks_ai",
|
||||
"friendliai",
|
||||
"featherless_ai",
|
||||
"watsonx",
|
||||
"watsonx_text",
|
||||
"triton",
|
||||
"predibase",
|
||||
"databricks",
|
||||
"empower",
|
||||
"github",
|
||||
"ragflow",
|
||||
"compactifai",
|
||||
"docker_model_runner",
|
||||
"custom",
|
||||
"litellm_proxy",
|
||||
"hosted_vllm",
|
||||
"llamafile",
|
||||
"lm_studio",
|
||||
"galadriel",
|
||||
"nebius",
|
||||
"infinity",
|
||||
"deepgram",
|
||||
"elevenlabs",
|
||||
"novita",
|
||||
"aiohttp_openai",
|
||||
"langfuse",
|
||||
"humanloop",
|
||||
"topaz",
|
||||
"sap",
|
||||
"assemblyai",
|
||||
"charity_engine",
|
||||
"github_copilot",
|
||||
"snowflake",
|
||||
"gradient_ai",
|
||||
"meta_llama",
|
||||
"nscale",
|
||||
"pg_vector",
|
||||
"s3_vectors",
|
||||
"helicone",
|
||||
"hyperbolic",
|
||||
"recraft",
|
||||
"fal_ai",
|
||||
"stability",
|
||||
"heroku",
|
||||
"aiml",
|
||||
"cometapi",
|
||||
"oci",
|
||||
"auto_router",
|
||||
"vercel_ai_gateway",
|
||||
"dotprompt",
|
||||
"manus",
|
||||
"wandb",
|
||||
"ovhcloud",
|
||||
"scaleway",
|
||||
"lemonade",
|
||||
"amazon_nova",
|
||||
"a2a_agent",
|
||||
"langgraph",
|
||||
"langflow",
|
||||
"minimax",
|
||||
"synthetic",
|
||||
"apertis",
|
||||
"nano-gpt",
|
||||
"poe",
|
||||
"chutes",
|
||||
"neosantara",
|
||||
"parasail",
|
||||
"xiaomi_mimo",
|
||||
"tensormesh",
|
||||
"libertai",
|
||||
"pinstripes",
|
||||
"darkbloom",
|
||||
"litellm_agent",
|
||||
"cursor",
|
||||
"bedrock_mantle",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn provider_values_match_python_enum_order() {
|
||||
assert_eq!(LlmProvider::ALL.len(), 146);
|
||||
assert_eq!(
|
||||
LlmProvider::ALL
|
||||
.iter()
|
||||
.map(|provider| provider.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
PYTHON_LLM_PROVIDER_VALUES
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_round_trips_all_providers() {
|
||||
for provider in LlmProvider::ALL {
|
||||
assert_eq!(LlmProvider::from_str(provider.as_str()), Ok(provider));
|
||||
assert_eq!(provider.to_string(), provider.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_rejects_unknown_provider() {
|
||||
assert_eq!(
|
||||
LlmProvider::from_str("not-a-provider"),
|
||||
Err(CoreError::InvalidProvider("not-a-provider".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,14 @@
|
|||
//! resolve the API key, build the URL + body via the pure transforms, POST it,
|
||||
//! and normalize the response. The HTTP client is built once and reused.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use litellm_core::error::CoreError;
|
||||
use litellm_core::ocr::transformation::OcrProviderConfig;
|
||||
use litellm_core::CoreResult;
|
||||
use litellm_core::LlmProvider;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::mistral::ocr::transformation as mistral;
|
||||
|
|
@ -25,11 +27,11 @@ const OCR_TIMEOUT_SECS: u64 = 600;
|
|||
/// forwarding sensitive payloads across the host boundary.
|
||||
const ERROR_BODY_MAX_CHARS: usize = 256;
|
||||
|
||||
/// Process-wide blocking HTTP client (connection pool + TLS reused across calls).
|
||||
fn http_client() -> &'static reqwest::blocking::Client {
|
||||
static CLIENT: OnceLock<reqwest::blocking::Client> = OnceLock::new();
|
||||
/// Process-wide async HTTP client (connection pool + TLS reused across calls).
|
||||
fn http_client() -> &'static reqwest::Client {
|
||||
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
CLIENT.get_or_init(|| {
|
||||
reqwest::blocking::Client::builder()
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(OCR_TIMEOUT_SECS))
|
||||
.build()
|
||||
.expect("failed to build reqwest client")
|
||||
|
|
@ -44,39 +46,69 @@ fn truncate_error_body(body: &str) -> String {
|
|||
format!("{truncated}... (truncated)")
|
||||
}
|
||||
|
||||
fn ocr_config_for(provider: LlmProvider) -> Option<&'static dyn OcrProviderConfig> {
|
||||
match provider {
|
||||
LlmProvider::Mistral => Some(&MISTRAL_OCR_CONFIG),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn string_headers(extra_headers: Option<Map<String, Value>>) -> Vec<(String, String)> {
|
||||
extra_headers
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| value.as_str().map(|value| (key, value.to_string())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub struct OcrRequest<'a> {
|
||||
pub model: &'a str,
|
||||
pub document: Value,
|
||||
pub api_key: Option<&'a str>,
|
||||
pub api_base: Option<&'a str>,
|
||||
pub custom_llm_provider: &'a str,
|
||||
pub extra_headers: Option<Map<String, Value>>,
|
||||
pub optional_params: Map<String, Value>,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Perform a Mistral OCR call end to end and return the normalized response as
|
||||
/// JSON (the shape the Python `OCRResponse` model expects).
|
||||
///
|
||||
/// Blocking: intended to be called with the GIL released from the Python bridge.
|
||||
pub fn run_ocr(
|
||||
model: &str,
|
||||
document: Value,
|
||||
api_key: Option<&str>,
|
||||
api_base: Option<&str>,
|
||||
optional_params: Map<String, Value>,
|
||||
timeout: Option<Duration>,
|
||||
) -> CoreResult<Value> {
|
||||
let config = &MISTRAL_OCR_CONFIG;
|
||||
/// Async: intended to be awaited directly by the Python bridge's async entrypoint.
|
||||
pub async fn ocr(request: OcrRequest<'_>) -> CoreResult<Value> {
|
||||
let model = request.model;
|
||||
let provider = LlmProvider::from_str(request.custom_llm_provider)?;
|
||||
let config =
|
||||
ocr_config_for(provider).ok_or_else(|| CoreError::InvalidProvider(provider.to_string()))?;
|
||||
|
||||
let api_key = mistral::resolve_api_key(api_key, &|key| std::env::var(key).ok())?;
|
||||
let url = mistral::complete_url(api_base);
|
||||
let filtered_params = config.map_ocr_params(&optional_params);
|
||||
// TODO: key and URL resolution are still Mistral-specific while Mistral is
|
||||
// the only Rust OCR provider. Move these onto the trait when another OCR
|
||||
// provider is added here.
|
||||
let api_key = mistral::resolve_api_key(request.api_key, &|key| std::env::var(key).ok())?;
|
||||
let url = mistral::complete_url(request.api_base);
|
||||
let filtered_params = config.map_ocr_params(&request.optional_params);
|
||||
let body = config
|
||||
.transform_ocr_request(model, document, filtered_params)?
|
||||
.transform_ocr_request(model, request.document, filtered_params)?
|
||||
.data;
|
||||
|
||||
let mut request = http_client().post(&url).bearer_auth(&api_key).json(&body);
|
||||
if let Some(duration) = timeout {
|
||||
request = request.timeout(duration);
|
||||
let mut request_builder = http_client().post(&url).bearer_auth(&api_key).json(&body);
|
||||
for (key, value) in string_headers(request.extra_headers) {
|
||||
request_builder = request_builder.header(&key, value);
|
||||
}
|
||||
if let Some(duration) = request.timeout {
|
||||
request_builder = request_builder.timeout(duration);
|
||||
}
|
||||
|
||||
let response = request
|
||||
let response = request_builder
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| CoreError::Network(err.to_string()))?;
|
||||
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|err| CoreError::Network(err.to_string()))?;
|
||||
|
||||
if !status.is_success() {
|
||||
|
|
@ -97,6 +129,7 @@ pub fn run_ocr(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn truncate_error_body_passes_short_strings_through() {
|
||||
|
|
@ -124,4 +157,27 @@ mod tests {
|
|||
let truncated = truncate_error_body(&body);
|
||||
assert!(truncated.is_char_boundary(truncated.len()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocr_registry_supports_only_mistral() {
|
||||
assert!(ocr_config_for(LlmProvider::Mistral).is_some());
|
||||
assert!(ocr_config_for(LlmProvider::Openai).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_headers_keeps_only_string_values() {
|
||||
let headers = json!({
|
||||
"x-trace-id": "trace-1",
|
||||
"x-number": 42,
|
||||
"x-bool": true
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
assert_eq!(
|
||||
string_headers(Some(headers)),
|
||||
vec![("x-trace-id".to_string(), "trace-1".to_string())]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ crate-type = ["cdylib"]
|
|||
litellm-core.workspace = true
|
||||
litellm-providers.workspace = true
|
||||
pyo3 = { workspace = true, features = ["extension-module"] }
|
||||
pyo3-async-runtimes.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use litellm_core::error::CoreError;
|
||||
use litellm_providers::ocr::run_ocr;
|
||||
use pyo3::exceptions::{PyRuntimeError, PyValueError};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyAny, PyDict};
|
||||
|
|
@ -9,6 +8,13 @@ use serde_json::{Map, Value};
|
|||
|
||||
mod gil;
|
||||
|
||||
type MarshaledOcrInputs = (
|
||||
Value,
|
||||
Option<Map<String, Value>>,
|
||||
Map<String, Value>,
|
||||
Option<Duration>,
|
||||
);
|
||||
|
||||
fn py_to_json(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult<Value> {
|
||||
let json = py.import("json")?;
|
||||
let encoded: String = json.call_method1("dumps", (value,))?.extract()?;
|
||||
|
|
@ -28,53 +34,93 @@ fn json_to_py(py: Python<'_>, value: Value) -> PyResult<Py<PyAny>> {
|
|||
fn core_error_to_pyerr(err: CoreError) -> PyErr {
|
||||
match err {
|
||||
CoreError::Auth(message) => PyValueError::new_err(message),
|
||||
CoreError::InvalidType { .. } | CoreError::MissingField(_) => {
|
||||
PyValueError::new_err(err.to_string())
|
||||
}
|
||||
CoreError::InvalidProvider(_)
|
||||
| CoreError::InvalidType { .. }
|
||||
| CoreError::MissingField(_) => PyValueError::new_err(err.to_string()),
|
||||
other => PyRuntimeError::new_err(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_object_to_map(
|
||||
py: Python<'_>,
|
||||
name: &'static str,
|
||||
value: Option<Py<PyAny>>,
|
||||
) -> PyResult<Map<String, Value>> {
|
||||
match value {
|
||||
Some(value) => match py_to_json(py, value.bind(py))? {
|
||||
Value::Object(map) => Ok(map),
|
||||
_ => Err(PyValueError::new_err(format!("{name} must be a dict"))),
|
||||
},
|
||||
None => Ok(Map::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_timeout(timeout_seconds: Option<f64>) -> Option<Duration> {
|
||||
timeout_seconds.and_then(|secs| {
|
||||
if secs.is_finite() && secs > 0.0 {
|
||||
Some(Duration::from_secs_f64(secs))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn marshal_inputs(
|
||||
py: Python<'_>,
|
||||
document: Py<PyAny>,
|
||||
extra_headers: Option<Py<PyAny>>,
|
||||
optional_params: Option<Py<PyAny>>,
|
||||
timeout_seconds: Option<f64>,
|
||||
) -> PyResult<MarshaledOcrInputs> {
|
||||
let document = py_to_json(py, document.bind(py))?;
|
||||
let extra_headers = match extra_headers {
|
||||
Some(headers) => Some(optional_object_to_map(py, "extra_headers", Some(headers))?),
|
||||
None => None,
|
||||
};
|
||||
let optional_params = optional_object_to_map(py, "optional_params", optional_params)?;
|
||||
let timeout = optional_timeout(timeout_seconds);
|
||||
|
||||
Ok((document, extra_headers, optional_params, timeout))
|
||||
}
|
||||
|
||||
/// Perform a Mistral OCR call end to end and return the response as a dict.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (model, document, api_key=None, api_base=None, optional_params=None, timeout_seconds=None))]
|
||||
#[pyo3(signature = (model, document, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, timeout_seconds=None))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn ocr(
|
||||
py: Python<'_>,
|
||||
model: String,
|
||||
document: Py<PyAny>,
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
extra_headers: Option<Py<PyAny>>,
|
||||
optional_params: Option<Py<PyAny>>,
|
||||
timeout_seconds: Option<f64>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let document = py_to_json(py, document.bind(py))?;
|
||||
let custom_llm_provider = custom_llm_provider.unwrap_or_else(|| "mistral".to_string());
|
||||
let (document, extra_headers, optional_params, timeout) = marshal_inputs(
|
||||
py,
|
||||
document,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout_seconds,
|
||||
)?;
|
||||
|
||||
let optional_params = match optional_params {
|
||||
Some(params) => match py_to_json(py, params.bind(py))? {
|
||||
Value::Object(map) => map,
|
||||
_ => return Err(PyValueError::new_err("optional_params must be a dict")),
|
||||
},
|
||||
None => Map::new(),
|
||||
};
|
||||
|
||||
let timeout = timeout_seconds.and_then(|secs| {
|
||||
if secs.is_finite() && secs > 0.0 {
|
||||
Some(Duration::from_secs_f64(secs))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Release the GIL during the blocking HTTP call (counted for observability).
|
||||
// Release the GIL while the sync API waits on async Rust work.
|
||||
let result = gil::release_gil(py, || {
|
||||
run_ocr(
|
||||
&model,
|
||||
document,
|
||||
api_key.as_deref(),
|
||||
api_base.as_deref(),
|
||||
optional_params,
|
||||
timeout,
|
||||
)
|
||||
pyo3_async_runtimes::tokio::get_runtime().block_on(litellm_providers::ocr::ocr(
|
||||
litellm_providers::ocr::OcrRequest {
|
||||
model: &model,
|
||||
document,
|
||||
api_key: api_key.as_deref(),
|
||||
api_base: api_base.as_deref(),
|
||||
custom_llm_provider: &custom_llm_provider,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
match result {
|
||||
|
|
@ -83,8 +129,50 @@ fn ocr(
|
|||
}
|
||||
}
|
||||
|
||||
/// Perform an OCR call end to end and return an asyncio awaitable.
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (model, document, api_key=None, api_base=None, custom_llm_provider=None, extra_headers=None, optional_params=None, timeout_seconds=None))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn aocr(
|
||||
py: Python<'_>,
|
||||
model: String,
|
||||
document: Py<PyAny>,
|
||||
api_key: Option<String>,
|
||||
api_base: Option<String>,
|
||||
custom_llm_provider: Option<String>,
|
||||
extra_headers: Option<Py<PyAny>>,
|
||||
optional_params: Option<Py<PyAny>>,
|
||||
timeout_seconds: Option<f64>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let custom_llm_provider = custom_llm_provider.unwrap_or_else(|| "mistral".to_string());
|
||||
let (document, extra_headers, optional_params, timeout) = marshal_inputs(
|
||||
py,
|
||||
document,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout_seconds,
|
||||
)?;
|
||||
|
||||
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
||||
let value = litellm_providers::ocr::ocr(litellm_providers::ocr::OcrRequest {
|
||||
model: &model,
|
||||
document,
|
||||
api_key: api_key.as_deref(),
|
||||
api_base: api_base.as_deref(),
|
||||
custom_llm_provider: &custom_llm_provider,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout,
|
||||
})
|
||||
.await
|
||||
.map_err(core_error_to_pyerr)?;
|
||||
|
||||
Python::with_gil(|py| json_to_py(py, value))
|
||||
})
|
||||
}
|
||||
|
||||
/// Bridge GIL accounting, e.g. `{"releases": 12}`. Lets the Python side observe
|
||||
/// how often the bridge has dropped the GIL for blocking work.
|
||||
/// how often the sync bridge has dropped the GIL while awaiting Rust work.
|
||||
#[pyfunction]
|
||||
fn gil_stats(py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let stats = PyDict::new(py);
|
||||
|
|
@ -95,6 +183,7 @@ fn gil_stats(py: Python<'_>) -> PyResult<Py<PyAny>> {
|
|||
#[pymodule]
|
||||
fn litellm_python_bridge(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(ocr, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(aocr, module)?)?;
|
||||
module.add_function(wrap_pyfunction!(gil_stats, module)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ Main OCR function for LiteLLM.
|
|||
|
||||
import asyncio
|
||||
import base64
|
||||
import contextvars
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from functools import partial
|
||||
from dataclasses import dataclass
|
||||
from io import IOBase
|
||||
from typing import Any, Callable, Coroutine, Dict, Optional, Union, cast
|
||||
|
||||
|
|
@ -20,7 +19,13 @@ from litellm.constants import request_timeout
|
|||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.ocr.transformation import BaseOCRConfig, OCRResponse
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.ocr.rust_bridge import RustOcr, load_rust_ocr, rust_ocr_enabled
|
||||
from litellm.ocr.rust_bridge import (
|
||||
RustAocr,
|
||||
RustOcr,
|
||||
load_rust_aocr,
|
||||
load_rust_ocr,
|
||||
rust_ocr_enabled,
|
||||
)
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.utils import ProviderConfigManager, client
|
||||
|
||||
|
|
@ -29,6 +34,21 @@ base_llm_http_handler = BaseLLMHTTPHandler()
|
|||
#################################################
|
||||
|
||||
|
||||
@dataclass
|
||||
class _PreparedOCRRequest:
|
||||
model: str
|
||||
document: dict[str, object]
|
||||
api_key: Optional[str]
|
||||
api_base: Optional[str]
|
||||
custom_llm_provider: str
|
||||
extra_headers: Optional[dict[str, object]]
|
||||
provider_config: BaseOCRConfig
|
||||
optional_params: dict[str, object]
|
||||
litellm_params: dict[str, object]
|
||||
effective_timeout: Union[float, httpx.Timeout]
|
||||
litellm_logging_obj: LiteLLMLoggingObj
|
||||
|
||||
|
||||
def _timeout_to_seconds(
|
||||
timeout: Optional[Union[float, httpx.Timeout]],
|
||||
) -> Optional[float]:
|
||||
|
|
@ -45,6 +65,107 @@ def _timeout_to_seconds(
|
|||
return float(timeout)
|
||||
|
||||
|
||||
def _prepare_ocr_request(
|
||||
model: str,
|
||||
document: Dict[str, Any],
|
||||
api_key: Optional[str],
|
||||
api_base: Optional[str],
|
||||
timeout: Optional[Union[float, httpx.Timeout]],
|
||||
custom_llm_provider: Optional[str],
|
||||
extra_headers: Optional[Dict[str, Any]],
|
||||
kwargs: dict[str, object],
|
||||
) -> _PreparedOCRRequest:
|
||||
litellm_logging_obj = cast(LiteLLMLoggingObj, kwargs.pop("litellm_logging_obj"))
|
||||
litellm_call_id = cast(Optional[str], kwargs.get("litellm_call_id", None))
|
||||
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError(
|
||||
f"document must be a dict with 'type' and URL/file field, got {type(document)}"
|
||||
)
|
||||
|
||||
doc_type = document.get("type")
|
||||
|
||||
if doc_type == "file":
|
||||
document = convert_file_document_to_url_document(document)
|
||||
doc_type = document.get("type")
|
||||
|
||||
if doc_type not in ["document_url", "image_url"]:
|
||||
raise ValueError(
|
||||
f"Invalid document type: {doc_type}. "
|
||||
"Must be 'document_url', 'image_url', or 'file'"
|
||||
)
|
||||
|
||||
(
|
||||
model,
|
||||
custom_llm_provider,
|
||||
dynamic_api_key,
|
||||
dynamic_api_base,
|
||||
) = litellm.get_llm_provider(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=api_base,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
if dynamic_api_key:
|
||||
api_key = dynamic_api_key
|
||||
if dynamic_api_base:
|
||||
api_base = dynamic_api_base
|
||||
|
||||
ocr_provider_config = ProviderConfigManager.get_provider_ocr_config(
|
||||
model=model,
|
||||
provider=litellm.LlmProviders(custom_llm_provider),
|
||||
)
|
||||
|
||||
if ocr_provider_config is None:
|
||||
raise ValueError(f"OCR is not supported for provider: {custom_llm_provider}")
|
||||
|
||||
verbose_logger.debug(f"OCR call - model: {model}, provider: {custom_llm_provider}")
|
||||
|
||||
litellm_params = GenericLiteLLMParams(**kwargs)
|
||||
|
||||
supported_params = ocr_provider_config.get_supported_ocr_params(model=model)
|
||||
non_default_params = {}
|
||||
for param in supported_params:
|
||||
if param in kwargs:
|
||||
non_default_params[param] = kwargs.pop(param)
|
||||
|
||||
optional_params = ocr_provider_config.map_ocr_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params={},
|
||||
model=model,
|
||||
)
|
||||
|
||||
verbose_logger.debug(f"OCR optional_params after mapping: {optional_params}")
|
||||
|
||||
effective_timeout = timeout or request_timeout
|
||||
|
||||
litellm_logging_obj.update_from_kwargs(
|
||||
kwargs=kwargs,
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
litellm_params={
|
||||
"litellm_call_id": litellm_call_id,
|
||||
"api_base": api_base,
|
||||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
return _PreparedOCRRequest(
|
||||
model=model,
|
||||
document=cast(dict[str, object], document),
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=cast(Optional[dict[str, object]], extra_headers),
|
||||
provider_config=ocr_provider_config,
|
||||
optional_params=cast(dict[str, object], optional_params),
|
||||
litellm_params=dict(litellm_params),
|
||||
effective_timeout=effective_timeout,
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
)
|
||||
|
||||
|
||||
def _run_rust_ocr(
|
||||
rust_ocr: RustOcr,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
|
|
@ -54,6 +175,8 @@ def _run_rust_ocr(
|
|||
document: dict[str, object],
|
||||
api_key: Optional[str],
|
||||
api_base: Optional[str],
|
||||
custom_llm_provider: str,
|
||||
extra_headers: Optional[dict[str, object]],
|
||||
optional_params: dict[str, object],
|
||||
litellm_params: dict[str, object],
|
||||
timeout_seconds: Optional[float],
|
||||
|
|
@ -68,7 +191,7 @@ def _run_rust_ocr(
|
|||
"""
|
||||
resolved_api_key = api_key or resolve_api_key("MISTRAL_API_KEY")
|
||||
resolved_headers = provider_config.validate_environment(
|
||||
headers={},
|
||||
headers=extra_headers or {},
|
||||
model=model,
|
||||
api_key=resolved_api_key,
|
||||
api_base=api_base,
|
||||
|
|
@ -99,6 +222,64 @@ def _run_rust_ocr(
|
|||
document=document,
|
||||
api_key=resolved_api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _run_rust_aocr(
|
||||
rust_aocr: RustAocr,
|
||||
logging_obj: LiteLLMLoggingObj,
|
||||
provider_config: BaseOCRConfig,
|
||||
resolve_api_key: Callable[[str], Optional[str]],
|
||||
model: str,
|
||||
document: dict[str, object],
|
||||
api_key: Optional[str],
|
||||
api_base: Optional[str],
|
||||
custom_llm_provider: str,
|
||||
extra_headers: Optional[dict[str, object]],
|
||||
optional_params: dict[str, object],
|
||||
litellm_params: dict[str, object],
|
||||
timeout_seconds: Optional[float],
|
||||
) -> OCRResponse:
|
||||
resolved_api_key = api_key or resolve_api_key("MISTRAL_API_KEY")
|
||||
resolved_headers = provider_config.validate_environment(
|
||||
headers=extra_headers or {},
|
||||
model=model,
|
||||
api_key=resolved_api_key,
|
||||
api_base=api_base,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
resolved_complete_url = provider_config.get_complete_url(
|
||||
api_base=api_base,
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
logging_obj.pre_call(
|
||||
input="OCR document processing",
|
||||
api_key=resolved_api_key,
|
||||
additional_args={
|
||||
"complete_input_dict": {
|
||||
"model": model,
|
||||
"document": document,
|
||||
**optional_params,
|
||||
},
|
||||
"api_base": resolved_complete_url,
|
||||
"headers": resolved_headers,
|
||||
},
|
||||
)
|
||||
return OCRResponse.model_validate(
|
||||
await rust_aocr(
|
||||
model=model,
|
||||
document=document,
|
||||
api_key=resolved_api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
optional_params=optional_params,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
|
|
@ -176,17 +357,7 @@ async def aocr(
|
|||
"""
|
||||
local_vars = locals()
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
kwargs["aocr"] = True
|
||||
|
||||
# Get custom llm provider
|
||||
if custom_llm_provider is None:
|
||||
_, custom_llm_provider, _, _ = litellm.get_llm_provider(
|
||||
model=model, api_base=api_base
|
||||
)
|
||||
|
||||
func = partial(
|
||||
ocr,
|
||||
prepared = _prepare_ocr_request(
|
||||
model=model,
|
||||
document=document,
|
||||
api_key=api_key,
|
||||
|
|
@ -194,17 +365,56 @@ async def aocr(
|
|||
timeout=timeout,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
**kwargs,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
func_with_context = partial(ctx.run, func)
|
||||
init_response = await loop.run_in_executor(None, func_with_context)
|
||||
if prepared.custom_llm_provider == "mistral" and rust_ocr_enabled():
|
||||
rust_aocr = load_rust_aocr()
|
||||
if rust_aocr is None:
|
||||
verbose_logger.debug(
|
||||
"Async Rust OCR bridge unavailable; falling back to Python path"
|
||||
)
|
||||
else:
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
if asyncio.iscoroutine(init_response):
|
||||
response = await init_response
|
||||
else:
|
||||
response = init_response
|
||||
response = await _run_rust_aocr(
|
||||
rust_aocr=rust_aocr,
|
||||
logging_obj=prepared.litellm_logging_obj,
|
||||
provider_config=prepared.provider_config,
|
||||
resolve_api_key=get_secret_str,
|
||||
model=prepared.model,
|
||||
document=prepared.document,
|
||||
api_key=prepared.api_key,
|
||||
api_base=prepared.api_base,
|
||||
custom_llm_provider=prepared.custom_llm_provider,
|
||||
extra_headers=prepared.extra_headers,
|
||||
optional_params=prepared.optional_params,
|
||||
litellm_params=prepared.litellm_params,
|
||||
timeout_seconds=_timeout_to_seconds(prepared.effective_timeout),
|
||||
)
|
||||
if response is None:
|
||||
raise ValueError(
|
||||
f"Got an unexpected None response from the OCR API: {response}"
|
||||
)
|
||||
return response
|
||||
|
||||
response = base_llm_http_handler.ocr(
|
||||
model=prepared.model,
|
||||
document=prepared.document,
|
||||
optional_params=prepared.optional_params,
|
||||
timeout=prepared.effective_timeout,
|
||||
logging_obj=prepared.litellm_logging_obj,
|
||||
api_key=prepared.api_key,
|
||||
api_base=prepared.api_base,
|
||||
custom_llm_provider=prepared.custom_llm_provider,
|
||||
aocr=True,
|
||||
headers=prepared.extra_headers,
|
||||
provider_config=prepared.provider_config,
|
||||
litellm_params=prepared.litellm_params,
|
||||
)
|
||||
|
||||
if asyncio.iscoroutine(response):
|
||||
response = await response
|
||||
|
||||
if response is None:
|
||||
raise ValueError(
|
||||
|
|
@ -297,94 +507,20 @@ def ocr(
|
|||
"""
|
||||
local_vars = locals()
|
||||
try:
|
||||
litellm_logging_obj = cast(LiteLLMLoggingObj, kwargs.pop("litellm_logging_obj"))
|
||||
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None)
|
||||
_is_async = kwargs.pop("aocr", False) is True
|
||||
|
||||
# Validate document parameter format
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError(
|
||||
f"document must be a dict with 'type' and URL/file field, got {type(document)}"
|
||||
)
|
||||
|
||||
doc_type = document.get("type")
|
||||
|
||||
# Handle file type: convert to document_url/image_url with base64 data URI
|
||||
if doc_type == "file":
|
||||
document = convert_file_document_to_url_document(document)
|
||||
doc_type = document.get("type")
|
||||
|
||||
if doc_type not in ["document_url", "image_url"]:
|
||||
raise ValueError(
|
||||
f"Invalid document type: {doc_type}. "
|
||||
"Must be 'document_url', 'image_url', or 'file'"
|
||||
)
|
||||
|
||||
(
|
||||
model,
|
||||
custom_llm_provider,
|
||||
dynamic_api_key,
|
||||
dynamic_api_base,
|
||||
) = litellm.get_llm_provider(
|
||||
prepared = _prepare_ocr_request(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=api_base,
|
||||
document=document,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
# Update with dynamic values if available
|
||||
if dynamic_api_key:
|
||||
api_key = dynamic_api_key
|
||||
if dynamic_api_base:
|
||||
api_base = dynamic_api_base
|
||||
|
||||
ocr_provider_config: Optional[BaseOCRConfig] = (
|
||||
ProviderConfigManager.get_provider_ocr_config(
|
||||
model=model,
|
||||
provider=litellm.LlmProviders(custom_llm_provider),
|
||||
)
|
||||
)
|
||||
|
||||
if ocr_provider_config is None:
|
||||
raise ValueError(
|
||||
f"OCR is not supported for provider: {custom_llm_provider}"
|
||||
)
|
||||
|
||||
verbose_logger.debug(
|
||||
f"OCR call - model: {model}, provider: {custom_llm_provider}"
|
||||
)
|
||||
|
||||
litellm_params = GenericLiteLLMParams(**kwargs)
|
||||
|
||||
supported_params = ocr_provider_config.get_supported_ocr_params(model=model)
|
||||
non_default_params = {}
|
||||
for param in supported_params:
|
||||
if param in kwargs:
|
||||
non_default_params[param] = kwargs.pop(param)
|
||||
|
||||
optional_params = ocr_provider_config.map_ocr_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params={},
|
||||
model=model,
|
||||
)
|
||||
|
||||
verbose_logger.debug(f"OCR optional_params after mapping: {optional_params}")
|
||||
|
||||
effective_timeout = timeout or request_timeout
|
||||
|
||||
litellm_logging_obj.update_from_kwargs(
|
||||
api_base=api_base,
|
||||
kwargs=kwargs,
|
||||
model=model,
|
||||
optional_params=optional_params,
|
||||
litellm_params={
|
||||
"litellm_call_id": litellm_call_id,
|
||||
"api_base": api_base,
|
||||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
extra_headers=extra_headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
# Optional Rust path: hand the whole Mistral OCR call to the Rust bridge.
|
||||
if custom_llm_provider == "mistral" and rust_ocr_enabled():
|
||||
if prepared.custom_llm_provider == "mistral" and rust_ocr_enabled():
|
||||
rust_ocr = load_rust_ocr()
|
||||
if rust_ocr is None:
|
||||
verbose_logger.debug(
|
||||
|
|
@ -395,31 +531,33 @@ def ocr(
|
|||
|
||||
return _run_rust_ocr(
|
||||
rust_ocr=rust_ocr,
|
||||
logging_obj=litellm_logging_obj,
|
||||
provider_config=ocr_provider_config,
|
||||
logging_obj=prepared.litellm_logging_obj,
|
||||
provider_config=prepared.provider_config,
|
||||
resolve_api_key=get_secret_str,
|
||||
model=model,
|
||||
document=document,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
optional_params=optional_params,
|
||||
litellm_params=dict(litellm_params),
|
||||
timeout_seconds=_timeout_to_seconds(effective_timeout),
|
||||
model=prepared.model,
|
||||
document=prepared.document,
|
||||
api_key=prepared.api_key,
|
||||
api_base=prepared.api_base,
|
||||
custom_llm_provider=prepared.custom_llm_provider,
|
||||
extra_headers=prepared.extra_headers,
|
||||
optional_params=prepared.optional_params,
|
||||
litellm_params=prepared.litellm_params,
|
||||
timeout_seconds=_timeout_to_seconds(prepared.effective_timeout),
|
||||
)
|
||||
|
||||
response = base_llm_http_handler.ocr(
|
||||
model=model,
|
||||
document=document,
|
||||
optional_params=optional_params,
|
||||
timeout=effective_timeout,
|
||||
logging_obj=litellm_logging_obj,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model=prepared.model,
|
||||
document=prepared.document,
|
||||
optional_params=prepared.optional_params,
|
||||
timeout=prepared.effective_timeout,
|
||||
logging_obj=prepared.litellm_logging_obj,
|
||||
api_key=prepared.api_key,
|
||||
api_base=prepared.api_base,
|
||||
custom_llm_provider=prepared.custom_llm_provider,
|
||||
aocr=_is_async,
|
||||
headers=extra_headers,
|
||||
provider_config=ocr_provider_config,
|
||||
litellm_params=dict(litellm_params),
|
||||
headers=prepared.extra_headers,
|
||||
provider_config=prepared.provider_config,
|
||||
litellm_params=prepared.litellm_params,
|
||||
)
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ can import it statically without forming an import cycle.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final, Protocol, cast
|
||||
from typing import Awaitable, Final, Protocol, cast
|
||||
|
||||
|
||||
class RustOcr(Protocol):
|
||||
|
|
@ -23,11 +23,29 @@ class RustOcr(Protocol):
|
|||
document: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> dict[str, object]: ...
|
||||
|
||||
|
||||
class RustAocr(Protocol):
|
||||
"""Signature of the compiled ``litellm_python_bridge.aocr`` entrypoint."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
model: str,
|
||||
document: dict[str, object],
|
||||
api_key: str | None,
|
||||
api_base: str | None,
|
||||
custom_llm_provider: str,
|
||||
extra_headers: dict[str, object] | None,
|
||||
optional_params: dict[str, object],
|
||||
timeout_seconds: float | None,
|
||||
) -> Awaitable[dict[str, object]]: ...
|
||||
|
||||
|
||||
class _Unset:
|
||||
"""Sentinel type so ``ocr=None`` can clear a prior injection while omission preserves it."""
|
||||
|
||||
|
|
@ -36,21 +54,27 @@ _UNSET: Final[_Unset] = _Unset()
|
|||
|
||||
_rust_ocr_enabled = False
|
||||
_rust_ocr_impl: RustOcr | None = None
|
||||
_rust_aocr_impl: RustAocr | None = None
|
||||
|
||||
|
||||
def use_litellm_rust(
|
||||
enabled: bool = True, *, ocr: RustOcr | None | _Unset = _UNSET
|
||||
enabled: bool = True,
|
||||
*,
|
||||
ocr: RustOcr | None | _Unset = _UNSET,
|
||||
aocr: RustAocr | None | _Unset = _UNSET,
|
||||
) -> None:
|
||||
"""Route supported OCR calls through the Rust ``litellm_python_bridge`` extension.
|
||||
|
||||
``ocr`` injects the bridge callable; when omitted the compiled extension is
|
||||
loaded on demand and any previously injected bridge is preserved. Pass
|
||||
``ocr=None`` explicitly to clear a prior injection.
|
||||
``ocr`` and ``aocr`` inject bridge callables; when omitted the compiled
|
||||
extension is loaded on demand and any previously injected bridge is
|
||||
preserved. Pass ``None`` explicitly to clear a prior injection.
|
||||
"""
|
||||
global _rust_ocr_enabled, _rust_ocr_impl
|
||||
global _rust_ocr_enabled, _rust_ocr_impl, _rust_aocr_impl
|
||||
_rust_ocr_enabled = enabled
|
||||
if not isinstance(ocr, _Unset):
|
||||
_rust_ocr_impl = ocr
|
||||
if not isinstance(aocr, _Unset):
|
||||
_rust_aocr_impl = aocr
|
||||
|
||||
|
||||
def rust_ocr_enabled() -> bool:
|
||||
|
|
@ -72,3 +96,14 @@ def load_rust_ocr() -> RustOcr | None:
|
|||
except ImportError:
|
||||
return None
|
||||
return cast(RustOcr, litellm_python_bridge.ocr)
|
||||
|
||||
|
||||
def load_rust_aocr() -> RustAocr | None:
|
||||
"""Return the async Rust OCR callable, or ``None`` when unavailable."""
|
||||
if _rust_aocr_impl is not None:
|
||||
return _rust_aocr_impl
|
||||
try:
|
||||
import litellm_python_bridge
|
||||
except ImportError:
|
||||
return None
|
||||
return cast(RustAocr, getattr(litellm_python_bridge, "aocr", None))
|
||||
|
|
|
|||
|
|
@ -35,7 +35,15 @@ class RecordingBridge:
|
|||
self.calls = []
|
||||
|
||||
def __call__(
|
||||
self, model, document, api_key, api_base, optional_params, timeout_seconds
|
||||
self,
|
||||
model,
|
||||
document,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout_seconds,
|
||||
):
|
||||
self.calls.append(
|
||||
{
|
||||
|
|
@ -43,6 +51,40 @@ class RecordingBridge:
|
|||
"document": document,
|
||||
"api_key": api_key,
|
||||
"api_base": api_base,
|
||||
"custom_llm_provider": custom_llm_provider,
|
||||
"extra_headers": extra_headers,
|
||||
"optional_params": optional_params,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}
|
||||
)
|
||||
return dict(FAKE_OCR_RESPONSE)
|
||||
|
||||
|
||||
class RecordingAsyncBridge:
|
||||
"""A fake async ``RustAocr`` callable that records the args it was handed."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
model,
|
||||
document,
|
||||
api_key,
|
||||
api_base,
|
||||
custom_llm_provider,
|
||||
extra_headers,
|
||||
optional_params,
|
||||
timeout_seconds,
|
||||
):
|
||||
self.calls.append(
|
||||
{
|
||||
"model": model,
|
||||
"document": document,
|
||||
"api_key": api_key,
|
||||
"api_base": api_base,
|
||||
"custom_llm_provider": custom_llm_provider,
|
||||
"extra_headers": extra_headers,
|
||||
"optional_params": optional_params,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}
|
||||
|
|
@ -70,7 +112,7 @@ class FakeOCRConfig:
|
|||
def validate_environment(
|
||||
self, *, headers, model, api_key, api_base, litellm_params
|
||||
):
|
||||
return {"authorization": f"Bearer {api_key}"}
|
||||
return {"Authorization": f"Bearer {api_key}", **headers}
|
||||
|
||||
def get_complete_url(self, *, api_base, model, optional_params, litellm_params):
|
||||
return f"{api_base or 'https://api.mistral.ai/v1'}/ocr"
|
||||
|
|
@ -79,9 +121,9 @@ class FakeOCRConfig:
|
|||
@pytest.fixture(autouse=True)
|
||||
def _reset_rust_flag():
|
||||
"""Keep the global toggle isolated between tests."""
|
||||
rust_bridge.use_litellm_rust(False, ocr=None)
|
||||
rust_bridge.use_litellm_rust(False, ocr=None, aocr=None)
|
||||
yield
|
||||
rust_bridge.use_litellm_rust(False, ocr=None)
|
||||
rust_bridge.use_litellm_rust(False, ocr=None, aocr=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -92,6 +134,14 @@ def fake_bridge():
|
|||
return bridge
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_async_bridge():
|
||||
"""Enable the async Rust path with an injected recording bridge."""
|
||||
bridge = RecordingAsyncBridge()
|
||||
litellm.use_litellm_rust(True, aocr=bridge)
|
||||
return bridge
|
||||
|
||||
|
||||
def test_use_litellm_rust_toggles_flag():
|
||||
assert rust_bridge.rust_ocr_enabled() is False
|
||||
litellm.use_litellm_rust()
|
||||
|
|
@ -106,6 +156,12 @@ def test_load_rust_ocr_returns_injected_impl():
|
|||
assert rust_bridge.load_rust_ocr() is bridge
|
||||
|
||||
|
||||
def test_load_rust_aocr_returns_injected_impl():
|
||||
bridge = RecordingAsyncBridge()
|
||||
litellm.use_litellm_rust(True, aocr=bridge)
|
||||
assert rust_bridge.load_rust_aocr() is bridge
|
||||
|
||||
|
||||
def test_toggle_without_ocr_arg_preserves_injected_impl():
|
||||
"""Regression: routine enable/disable calls must not clobber a prior injection.
|
||||
|
||||
|
|
@ -114,20 +170,25 @@ def test_toggle_without_ocr_arg_preserves_injected_impl():
|
|||
a caller toggled the flag without re-passing ``ocr=``.
|
||||
"""
|
||||
bridge = RecordingBridge()
|
||||
litellm.use_litellm_rust(True, ocr=bridge)
|
||||
async_bridge = RecordingAsyncBridge()
|
||||
litellm.use_litellm_rust(True, ocr=bridge, aocr=async_bridge)
|
||||
|
||||
litellm.use_litellm_rust(False)
|
||||
assert rust_bridge.load_rust_ocr() is bridge
|
||||
assert rust_bridge.load_rust_aocr() is async_bridge
|
||||
litellm.use_litellm_rust(True)
|
||||
assert rust_bridge.load_rust_ocr() is bridge
|
||||
assert rust_bridge.load_rust_aocr() is async_bridge
|
||||
|
||||
|
||||
def test_explicit_ocr_none_clears_injected_impl():
|
||||
bridge = RecordingBridge()
|
||||
litellm.use_litellm_rust(True, ocr=bridge)
|
||||
async_bridge = RecordingAsyncBridge()
|
||||
litellm.use_litellm_rust(True, ocr=bridge, aocr=async_bridge)
|
||||
|
||||
litellm.use_litellm_rust(True, ocr=None)
|
||||
litellm.use_litellm_rust(True, ocr=None, aocr=None)
|
||||
assert rust_bridge.load_rust_ocr() is None
|
||||
assert rust_bridge.load_rust_aocr() is None
|
||||
|
||||
|
||||
def test_load_rust_ocr_none_when_extension_absent():
|
||||
|
|
@ -135,6 +196,7 @@ def test_load_rust_ocr_none_when_extension_absent():
|
|||
caller degrades to the Python path instead of raising ImportError."""
|
||||
litellm.use_litellm_rust(True) # no impl injected; extension isn't built in CI
|
||||
assert rust_bridge.load_rust_ocr() is None
|
||||
assert rust_bridge.load_rust_aocr() is None
|
||||
|
||||
|
||||
def test_load_rust_ocr_uses_compiled_extension(monkeypatch):
|
||||
|
|
@ -143,10 +205,12 @@ def test_load_rust_ocr_uses_compiled_extension(monkeypatch):
|
|||
built in CI, so stand in a fake module via ``sys.modules``."""
|
||||
fake_module = types.ModuleType("litellm_python_bridge")
|
||||
fake_module.ocr = lambda **kwargs: dict(FAKE_OCR_RESPONSE) # type: ignore[attr-defined]
|
||||
fake_module.aocr = lambda **kwargs: dict(FAKE_OCR_RESPONSE) # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "litellm_python_bridge", fake_module)
|
||||
|
||||
litellm.use_litellm_rust(True) # enabled, no impl injected -> import the extension
|
||||
assert rust_bridge.load_rust_ocr() is fake_module.ocr
|
||||
assert rust_bridge.load_rust_aocr() is fake_module.aocr
|
||||
|
||||
|
||||
def test_timeout_to_seconds_handles_float_timeout_and_none():
|
||||
|
|
@ -168,6 +232,8 @@ def test_run_rust_ocr_forwards_args_and_wraps_response():
|
|||
document=DOCUMENT,
|
||||
api_key="sk-test",
|
||||
api_base="https://proxy.internal",
|
||||
custom_llm_provider="mistral",
|
||||
extra_headers={"x-trace-id": "trace-1"},
|
||||
optional_params={"include_image_base64": True},
|
||||
litellm_params={},
|
||||
timeout_seconds=12.5,
|
||||
|
|
@ -181,6 +247,8 @@ def test_run_rust_ocr_forwards_args_and_wraps_response():
|
|||
"document": DOCUMENT,
|
||||
"api_key": "sk-test",
|
||||
"api_base": "https://proxy.internal",
|
||||
"custom_llm_provider": "mistral",
|
||||
"extra_headers": {"x-trace-id": "trace-1"},
|
||||
"optional_params": {"include_image_base64": True},
|
||||
"timeout_seconds": 12.5,
|
||||
}
|
||||
|
|
@ -202,6 +270,8 @@ def test_run_rust_ocr_resolves_key_via_secret_manager_when_missing():
|
|||
document=DOCUMENT,
|
||||
api_key=None,
|
||||
api_base=None,
|
||||
custom_llm_provider="mistral",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
timeout_seconds=None,
|
||||
|
|
@ -227,6 +297,8 @@ def test_run_rust_ocr_prefers_explicit_key_over_resolver():
|
|||
document=DOCUMENT,
|
||||
api_key="sk-explicit",
|
||||
api_base=None,
|
||||
custom_llm_provider="mistral",
|
||||
extra_headers=None,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
timeout_seconds=None,
|
||||
|
|
@ -249,6 +321,8 @@ def test_run_rust_ocr_runs_pre_call_logging():
|
|||
document=DOCUMENT,
|
||||
api_key="sk-test",
|
||||
api_base="https://api.mistral.ai/v1",
|
||||
custom_llm_provider="mistral",
|
||||
extra_headers={"x-trace-id": "trace-1"},
|
||||
optional_params={"include_image_base64": True},
|
||||
litellm_params={},
|
||||
timeout_seconds=None,
|
||||
|
|
@ -262,7 +336,10 @@ def test_run_rust_ocr_runs_pre_call_logging():
|
|||
assert complete_input["include_image_base64"] is True
|
||||
# The logged request mirrors what Rust sends: resolved URL + headers.
|
||||
assert additional_args["api_base"] == "https://api.mistral.ai/v1/ocr"
|
||||
assert additional_args["headers"] == {"authorization": "Bearer sk-test"}
|
||||
assert additional_args["headers"] == {
|
||||
"Authorization": "Bearer sk-test",
|
||||
"x-trace-id": "trace-1",
|
||||
}
|
||||
|
||||
|
||||
def test_ocr_routes_to_rust_when_enabled(fake_bridge):
|
||||
|
|
@ -270,6 +347,7 @@ def test_ocr_routes_to_rust_when_enabled(fake_bridge):
|
|||
model=MODEL,
|
||||
document=DOCUMENT,
|
||||
api_key="sk-test",
|
||||
extra_headers={"x-trace-id": "trace-1"},
|
||||
include_image_base64=True,
|
||||
)
|
||||
|
||||
|
|
@ -281,10 +359,34 @@ def test_ocr_routes_to_rust_when_enabled(fake_bridge):
|
|||
assert call["model"] == "mistral-ocr-latest"
|
||||
assert call["document"] == DOCUMENT
|
||||
assert call["api_key"] == "sk-test"
|
||||
assert call["custom_llm_provider"] == "mistral"
|
||||
assert call["extra_headers"] == {"x-trace-id": "trace-1"}
|
||||
# Raw OCR params ride along in optional_params; Rust filters to supported keys.
|
||||
assert call["optional_params"].get("include_image_base64") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aocr_routes_to_async_rust_when_enabled(fake_async_bridge):
|
||||
response = await litellm.aocr(
|
||||
model=MODEL,
|
||||
document=DOCUMENT,
|
||||
api_key="sk-test",
|
||||
extra_headers={"x-trace-id": "trace-1"},
|
||||
include_image_base64=True,
|
||||
)
|
||||
|
||||
assert isinstance(response, OCRResponse)
|
||||
assert response.pages[0].markdown == "hello world"
|
||||
assert len(fake_async_bridge.calls) == 1
|
||||
call = fake_async_bridge.calls[0]
|
||||
assert call["model"] == "mistral-ocr-latest"
|
||||
assert call["document"] == DOCUMENT
|
||||
assert call["api_key"] == "sk-test"
|
||||
assert call["custom_llm_provider"] == "mistral"
|
||||
assert call["extra_headers"] == {"x-trace-id": "trace-1"}
|
||||
assert call["optional_params"].get("include_image_base64") is True
|
||||
|
||||
|
||||
def test_ocr_forwards_timeout_to_rust(fake_bridge):
|
||||
"""Caller-supplied timeout must flow into the Rust bridge so the fixed 600s
|
||||
client ceiling doesn't silently override shorter deadlines."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue