refactor(core): centralize opaque provider params

This commit is contained in:
Yujong Lee 2026-09-15 08:55:02 -07:00
parent 30ad040b25
commit 451e96661e
36 changed files with 315 additions and 179 deletions

View file

@ -40,7 +40,7 @@ async fn bedrock_request_is_signed_and_contains_audio() {
api_base: Some(&api_base),
custom_llm_provider: Some("bedrock"),
extra_headers: None,
optional_params,
optional_params: optional_params.into(),
timeout: None,
})
.await

View file

@ -1,7 +1,8 @@
use crate::Error;
use serde_json::{Map, Value};
use serde_json::Value;
use super::types::{AudioTranscriptionRequestData, AudioTranscriptionResponseData};
use crate::params::OpaqueParams;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AudioTranscriptionAuth {
@ -15,22 +16,16 @@ pub enum AudioTranscriptionAuth {
pub trait AudioTranscriptionProviderConfig: Sync {
fn supported_transcription_params(&self) -> &'static [&'static str];
fn map_transcription_params(&self, params: &Map<String, Value>) -> Map<String, Value> {
params
.iter()
.filter(|(key, _)| {
self.supported_transcription_params()
.contains(&key.as_str())
})
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
#[tracing::instrument(target = "litellm::function_trace", level = "trace", skip_all)]
fn map_transcription_params(&self, params: &OpaqueParams) -> OpaqueParams {
params.retain_supported(self.supported_transcription_params())
}
fn transform_transcription_request(
&self,
model: &str,
audio: Value,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<AudioTranscriptionRequestData, Error>;
fn transform_transcription_response(
@ -43,14 +38,14 @@ pub trait AudioTranscriptionProviderConfig: Sync {
&self,
api_base: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<String, Error>;
fn auth_strategy(
&self,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<AudioTranscriptionAuth, Error>;
}

View file

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use super::transformation::{AudioTranscriptionAuth, AudioTranscriptionProviderConfig};
use crate::params::OpaqueParams;
pub struct AudioTranscriptionRequest<'a> {
pub model: &'a str,
@ -12,7 +13,7 @@ pub struct AudioTranscriptionRequest<'a> {
pub api_base: Option<&'a str>,
pub custom_llm_provider: Option<&'a str>,
pub extra_headers: Option<Map<String, Value>>,
pub optional_params: Map<String, Value>,
pub optional_params: OpaqueParams,
pub timeout: Option<Duration>,
}
@ -26,7 +27,7 @@ pub struct ProviderAudioTranscriptionRequest {
pub(super) upstream_headers: Vec<(String, String)>,
pub(super) auth: AudioTranscriptionAuth,
#[cfg(feature = "bedrock-auth")]
pub(super) optional_params: Map<String, Value>,
pub(super) optional_params: OpaqueParams,
pub(super) timeout: Option<Duration>,
}

View file

@ -16,8 +16,9 @@ pub mod response_utils;
pub mod transformation;
pub mod types;
use serde_json::{Map, Value};
use serde_json::Value;
use crate::params::OpaqueParams;
use handler::execute_chat_completions_provider_call;
use prepare::{parse_messages, resolve_provider_config, resolve_request};
use types::{ChatCompletionsRequest, ChatCompletionsResponse};
@ -38,7 +39,7 @@ pub fn chat_completions_decline_reason(
model: &str,
custom_llm_provider: Option<&str>,
messages: Value,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
) -> Option<&'static str> {
let Ok((_, config)) = resolve_provider_config(model, custom_llm_provider) else {
return Some("provider is not on the rust chat completions path");

View file

@ -22,7 +22,7 @@ fn request<'a>(
model,
messages,
optional_params: match optional_params {
Value::Object(map) => map,
Value::Object(map) => map.into(),
other => panic!("params must be an object, got {other}"),
},
api_key: Some("sk-test"),
@ -489,7 +489,7 @@ fn decline_reason(
params: Value,
) -> Option<&'static str> {
let params = match params {
Value::Object(map) => map,
Value::Object(map) => map.into(),
other => panic!("params must be an object, got {other}"),
};
super::chat_completions_decline_reason(model, provider, messages, &params)
@ -663,7 +663,7 @@ mod round_trip {
model: "anthropic/claude-sonnet-4-5",
messages,
optional_params: match params {
Value::Object(map) => map,
Value::Object(map) => map.into(),
other => panic!("params must be an object, got {other}"),
},
api_key: Some("sk-test"),

View file

@ -1,10 +1,11 @@
use crate::Error;
use serde_json::{Map, Value};
use serde_json::Value;
use super::types::{
ChatCompletionsResponse, ChatMessage, ChatMessageContent, ProviderChatRequestData,
ProviderChatResponseData,
};
use crate::params::OpaqueParams;
/// How the upstream call is authenticated. API-key strategies are resolved in
/// `prepare`; SigV4 needs the serialized body, so the handler signs it.
@ -36,7 +37,7 @@ pub trait ChatCompletionsProviderConfig: Sync {
&self,
api_base: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<String, Error>;
@ -44,7 +45,7 @@ pub trait ChatCompletionsProviderConfig: Sync {
&self,
api_key: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<ChatCompletionsAuth, Error>;
@ -74,7 +75,7 @@ pub trait ChatCompletionsProviderConfig: Sync {
fn unsupported_reason(
&self,
messages: &[ChatMessage],
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
) -> Option<Unsupported> {
unsupported_param(
self.supported_openai_params(),
@ -88,7 +89,7 @@ pub trait ChatCompletionsProviderConfig: Sync {
&self,
model: &str,
messages: Vec<ChatMessage>,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<ProviderChatRequestData, Error>;
fn transform_response(
@ -101,7 +102,7 @@ pub trait ChatCompletionsProviderConfig: Sync {
pub fn unsupported_param(
supported: &'static [(&'static str, &'static str)],
config: &'static [&'static str],
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
) -> Option<Unsupported> {
if optional_params
.get(STREAM_PARAM)

View file

@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use super::transformation::{ChatCompletionsAuth, ChatCompletionsProviderConfig};
use crate::params::OpaqueParams;
/// A `/chat/completions` call as it crosses into the core.
///
@ -14,7 +15,7 @@ use super::transformation::{ChatCompletionsAuth, ChatCompletionsProviderConfig};
pub struct ChatCompletionsRequest<'a> {
pub model: &'a str,
pub messages: Value,
pub optional_params: Map<String, Value>,
pub optional_params: OpaqueParams,
pub api_key: Option<&'a str>,
pub api_base: Option<&'a str>,
pub custom_llm_provider: Option<&'a str>,
@ -26,7 +27,7 @@ pub(super) struct ResolvedChatCompletionsRequest<'a> {
pub(super) model: String,
pub(super) config: &'static dyn ChatCompletionsProviderConfig,
pub(super) messages: Vec<ChatMessage>,
pub(super) optional_params: Map<String, Value>,
pub(super) optional_params: OpaqueParams,
pub(super) api_key: Option<&'a str>,
pub(super) api_base: Option<&'a str>,
pub(super) extra_headers: Option<Map<String, Value>>,
@ -41,7 +42,7 @@ pub(super) struct ProviderChatCompletionsRequest {
pub(super) upstream_headers: Vec<(String, String)>,
pub(super) auth: ChatCompletionsAuth,
#[cfg_attr(not(feature = "bedrock-auth"), allow(dead_code))]
pub(super) optional_params: Map<String, Value>,
pub(super) optional_params: OpaqueParams,
pub(super) timeout: Option<Duration>,
}

View file

@ -10,6 +10,7 @@ pub(crate) mod llms;
mod media;
pub mod messages;
pub mod ocr;
pub mod params;
pub mod providers;
pub mod responses;
mod url_utils;

View file

@ -18,13 +18,17 @@ pub(crate) struct AzureAICohereParseConfig;
impl BaseOcrConfig for AzureAICohereParseConfig {
type ProviderResponse = CohereResponse;
fn get_supported_ocr_params(&self, model: &str) -> &'static [&'static str] {
CohereParseConfig.get_supported_ocr_params(model)
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
client: &OcrClient,
) -> Result<reqwest::Request, OcrError> {
let params = crate::ocr::wire::decode_request_value::<CohereParams>(
serde_json::Value::Object(request.optional_params.clone()),
serde_json::Value::Object(request.optional_params.clone().into()),
"optional_params",
)?;
let config = AzureAuthInputs {

View file

@ -629,6 +629,10 @@ pub(crate) struct AzureDocumentIntelligenceOCRConfig;
impl BaseOcrConfig for AzureDocumentIntelligenceOCRConfig {
type ProviderResponse = AzureDocumentIntelligenceOperation;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&["pages", "features"]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
@ -686,7 +690,8 @@ impl BaseOcrConfig for AzureDocumentIntelligenceOCRConfig {
fn map_ocr_params(
request: &LiteLLMOcrRequest,
) -> Result<DocumentIntelligenceParams, OcrRequestError> {
let params = params::decode_input_params(request.optional_params.clone(), "optional_params")?;
let params =
params::decode_input_params(request.optional_params.clone().into(), "optional_params")?;
let crate::ocr::prepare::ParsedProviderParams {
known: params,
extra_params: _extra_params,

View file

@ -2,14 +2,12 @@ use crate::Error;
use crate::auth::{InputSource, Sourced};
use crate::constants::AZURE_AI_OCR_PATH;
use crate::llms::base_llm::ocr::transformation::BaseOcrConfig;
use crate::llms::mistral::ocr::MistralOcrResponse;
use crate::llms::mistral::ocr::transformation::MistralOCRConfig;
use crate::llms::mistral::ocr::{MistralOcrParams, MistralOcrResponse};
use crate::ocr::OcrClient;
use crate::ocr::document::{inline_remote_document, validate_inline_document};
use crate::ocr::error::{OcrError, OcrRequestError, OcrResponseError};
use crate::ocr::prepare::{
_prepare_ocr_request, ParsedProviderParams, credential_env, transform_request_body,
};
use crate::ocr::prepare::{credential_env, transform_request_body};
use crate::ocr::types::{LiteLLMOcrRequest, LiteLLMOcrResponse, OcrConnection};
use crate::providers::azure_ai::auth::AzureAuthInputs;
use crate::url_utils::ApiUrl;
@ -23,15 +21,16 @@ pub(crate) struct AzureAIOCRConfig;
impl BaseOcrConfig for AzureAIOCRConfig {
type ProviderResponse = MistralOcrResponse;
fn get_supported_ocr_params(&self, model: &str) -> &'static [&'static str] {
MistralOCRConfig.get_supported_ocr_params(model)
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
client: &OcrClient,
) -> Result<reqwest::Request, OcrError> {
let ParsedProviderParams {
known: params,
extra_params: _extra_params,
} = _prepare_ocr_request::<MistralOcrParams>(request)?;
let params = self.map_ocr_params(&request.model, &request.optional_params);
let config = AzureAuthInputs {
azure_ad_token_provider: request.azure_ad_token_provider.clone(),
..AzureAuthInputs::from_sourced_optional_params(

View file

@ -6,10 +6,17 @@ use crate::ocr::OcrClient;
use crate::ocr::error::{OcrError, OcrResponseError};
use crate::ocr::types::{LiteLLMOcrRequest, LiteLLMOcrResponse, OcrResponseFormat};
use crate::ocr::wire::DecodedOcrResponse;
use crate::params::OpaqueParams;
pub(crate) trait BaseOcrConfig: Send + Sync + Sized + 'static {
type ProviderResponse: DeserializeOwned + Send;
fn get_supported_ocr_params(&self, model: &str) -> &'static [&'static str];
fn map_ocr_params(&self, model: &str, params: &OpaqueParams) -> OpaqueParams {
params.retain_supported(self.get_supported_ocr_params(model))
}
fn prepare_request(
&self,
request: &LiteLLMOcrRequest,

View file

@ -290,13 +290,17 @@ impl CohereParseConfig {
impl BaseOcrConfig for CohereParseConfig {
type ProviderResponse = CohereResponse;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&["output_format"]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
client: &OcrClient,
) -> Result<reqwest::Request, OcrError> {
let params = crate::ocr::wire::decode_request_value::<CohereParams>(
serde_json::Value::Object(request.optional_params.clone()),
serde_json::Value::Object(request.optional_params.clone().into()),
"optional_params",
)?;
let headers = validate_environment(&request.connection, &credential_env)?;

View file

@ -1,3 +1,3 @@
pub(crate) mod transformation;
pub(crate) use transformation::{MistralOcrParams, MistralOcrResponse};
pub(crate) use transformation::MistralOcrResponse;

View file

@ -3,50 +3,14 @@ use serde_json::{Map, Value};
use crate::ocr::error::{OcrRequestError, OcrResponseError};
use crate::ocr::types::{LiteLLMOcrResponse, OcrDocument};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum MistralOcrPages {
Range(String),
Indices(Vec<i64>),
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct MistralOcrParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub pages: Option<MistralOcrPages>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_image_base64: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_min_size: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbox_annotation_format: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub document_annotation_format: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub document_annotation_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extract_header: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extract_footer: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub table_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence_scores_granularity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_blocks: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
use crate::params::OpaqueParams;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct MistralOcrRequest {
pub model: String,
pub document: OcrDocument,
#[serde(flatten)]
pub params: MistralOcrParams,
pub params: OpaqueParams,
}
#[derive(Clone, Debug, Default, Deserialize)]
@ -63,7 +27,7 @@ pub(crate) struct MistralOcrResponse {
pub(crate) fn transform_ocr_request(
model: &str,
document: OcrDocument,
params: &MistralOcrParams,
params: &OpaqueParams,
) -> Result<MistralOcrRequest, OcrRequestError> {
Ok(MistralOcrRequest {
model: model.to_string(),
@ -94,7 +58,8 @@ mod mapping_tests {
use serde_json::{Value, json};
fn mapped_params(value: Value) -> Value {
serde_json::to_value(serde_json::from_value::<MistralOcrParams>(value).unwrap()).unwrap()
let params = serde_json::from_value::<OpaqueParams>(value).unwrap();
serde_json::to_value(MistralOCRConfig.map_ocr_params("model", &params)).unwrap()
}
fn document() -> OcrDocument {
@ -168,6 +133,16 @@ mod mapping_tests {
assert!(mapped.get("unsupported_param").is_none());
}
#[rstest]
fn map_ocr_params_preserves_unvalidated_values_and_explicit_null() {
let mapped = mapped_params(json!({
"pages":{"future":"shape"},
"include_image_base64":null
}));
assert_eq!(mapped["pages"], json!({"future":"shape"}));
assert!(mapped.get("include_image_base64").unwrap().is_null());
}
#[rstest]
#[case("table_format", json!("html"))]
#[case("confidence_scores_granularity", json!("word"))]
@ -205,8 +180,7 @@ mod mapping_tests {
#[case("include_blocks", json!(true))]
#[case("id", json!("req-123"))]
fn request_mapping_matches_python(#[case] name: &str, #[case] value: Value) {
let params: MistralOcrParams =
serde_json::from_value(json!({name: value.clone()})).unwrap();
let params: OpaqueParams = serde_json::from_value(json!({name: value.clone()})).unwrap();
let result =
serde_json::to_value(transform_ocr_request("model", document(), &params).unwrap())
.unwrap();
@ -226,7 +200,7 @@ mod mapping_tests {
#[case] name: &str,
#[case] value: Value,
) {
let params: MistralOcrParams = serde_json::from_value(json!({name:value.clone()})).unwrap();
let params: OpaqueParams = serde_json::from_value(json!({name:value.clone()})).unwrap();
let result = serde_json::to_value(
transform_ocr_request("mistral-ocr-latest", document(), &params).unwrap(),
)
@ -237,7 +211,7 @@ mod mapping_tests {
#[rstest]
fn transform_ocr_request_includes_multiple_new_params() {
let params: MistralOcrParams = serde_json::from_value(json!({
let params: OpaqueParams = serde_json::from_value(json!({
"table_format":"html",
"confidence_scores_granularity":"page",
"extract_header":true
@ -312,9 +286,7 @@ use crate::constants::MISTRAL_OCR_API_BASE;
use crate::llms::base_llm::ocr::transformation::BaseOcrConfig;
use crate::ocr::OcrClient;
use crate::ocr::error::OcrError;
use crate::ocr::prepare::{
_prepare_ocr_request, ParsedProviderParams, credential_env, transform_request_body,
};
use crate::ocr::prepare::{credential_env, transform_request_body};
use crate::ocr::types::{LiteLLMOcrRequest, OcrConnection};
use crate::url_utils::ApiUrl;
@ -328,7 +300,7 @@ impl MistralOCRConfig {
&self,
model: &str,
document: crate::ocr::types::OcrDocument,
params: &MistralOcrParams,
params: &OpaqueParams,
) -> Result<MistralOcrRequest, OcrRequestError> {
transform_ocr_request(model, document, params)
}
@ -337,15 +309,30 @@ impl MistralOCRConfig {
impl BaseOcrConfig for MistralOCRConfig {
type ProviderResponse = MistralOcrResponse;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&[
"pages",
"include_image_base64",
"image_limit",
"image_min_size",
"bbox_annotation_format",
"document_annotation_format",
"document_annotation_prompt",
"extract_header",
"extract_footer",
"table_format",
"confidence_scores_granularity",
"include_blocks",
"id",
]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
client: &OcrClient,
) -> Result<reqwest::Request, OcrError> {
let ParsedProviderParams {
known: params,
extra_params: _extra_params,
} = _prepare_ocr_request::<MistralOcrParams>(request)?;
let params = self.map_ocr_params(&request.model, &request.optional_params);
let headers = validate_environment(&request.connection, &credential_env)?;
let url = get_complete_url(request.connection.api_base.as_deref())?;
let body = self.transform_ocr_request(&request.model, request.document.clone(), &params)?;

View file

@ -446,6 +446,10 @@ mod legacy {
impl BaseOcrConfig for ReductoParseLegacyConfig {
type ProviderResponse = ReductoResponse;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&["enhance"]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
@ -499,6 +503,10 @@ mod v3 {
impl BaseOcrConfig for ReductoParseV3Config {
type ProviderResponse = ReductoResponse;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&["formatting", "retrieval", "settings"]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,

View file

@ -229,6 +229,10 @@ pub(crate) struct VertexAIDeepSeekOCRConfig;
impl BaseOcrConfig for VertexAIDeepSeekOCRConfig {
type ProviderResponse = DeepSeekOcrResponse;
fn get_supported_ocr_params(&self, _model: &str) -> &'static [&'static str] {
&["stream", "temperature", "max_tokens", "top_p", "n", "stop"]
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,

View file

@ -2,14 +2,12 @@ use super::common_utils::validate_destination;
use crate::Error;
use crate::auth::vertex::{self, VertexConfig};
use crate::llms::base_llm::ocr::transformation::BaseOcrConfig;
use crate::llms::mistral::ocr::MistralOcrResponse;
use crate::llms::mistral::ocr::transformation::MistralOCRConfig;
use crate::llms::mistral::ocr::{MistralOcrParams, MistralOcrResponse};
use crate::ocr::OcrClient;
use crate::ocr::document::{inline_remote_document, validate_inline_document};
use crate::ocr::error::{OcrError, OcrRequestError, OcrResponseError};
use crate::ocr::prepare::{
_prepare_ocr_request, ParsedProviderParams, credential_env, transform_request_body,
};
use crate::ocr::prepare::{credential_env, transform_request_body};
use crate::ocr::types::{LiteLLMOcrRequest, LiteLLMOcrResponse};
use crate::url_utils::ApiUrl;
const DEFAULT_LOCATION: &str = "us-central1";
@ -20,16 +18,17 @@ pub(crate) struct VertexAIOCRConfig;
impl BaseOcrConfig for VertexAIOCRConfig {
type ProviderResponse = MistralOcrResponse;
fn get_supported_ocr_params(&self, model: &str) -> &'static [&'static str] {
MistralOCRConfig.get_supported_ocr_params(model)
}
async fn prepare_request(
&self,
request: &LiteLLMOcrRequest,
client: &OcrClient,
) -> Result<reqwest::Request, OcrError> {
validate_destination(&request.connection)?;
let ParsedProviderParams {
known: params,
extra_params: _extra_params,
} = _prepare_ocr_request::<MistralOcrParams>(request)?;
let params = self.map_ocr_params(&request.model, &request.optional_params);
let config = VertexConfig::from_sourced_optional_params(
&request.optional_params,
&request.input_sources,

View file

@ -100,7 +100,7 @@ impl CallLifecycleHooks<LiteLLMOcrRequest, LiteLLMOcrRequest, LiteLLMOcrResponse
model: request.model.clone(),
custom_llm_provider: self.provider_name.clone(),
document: request.document,
optional_params: Value::Object(request.optional_params),
optional_params: Value::Object(request.optional_params.into()),
})
.await?;
let Value::Object(optional_params) = changed.optional_params else {
@ -111,7 +111,7 @@ impl CallLifecycleHooks<LiteLLMOcrRequest, LiteLLMOcrRequest, LiteLLMOcrResponse
};
Ok(LiteLLMOcrRequest {
document: changed.document,
optional_params,
optional_params: optional_params.into(),
..request
})
})

View file

@ -18,7 +18,7 @@ pub(crate) fn _prepare_ocr_request<T: DeserializeOwned>(
request: &LiteLLMOcrRequest,
) -> Result<ParsedProviderParams<T>, OcrRequestError> {
super::wire::decode_request_value(
Value::Object(request.optional_params.clone()),
Value::Object(request.optional_params.clone().into()),
"optional_params",
)
}

View file

@ -1,4 +1,13 @@
use crate::Error;
use crate::llms::azure_ai::ocr::cohere_parse_transformation::AzureAICohereParseConfig;
use crate::llms::azure_ai::ocr::document_intelligence::transformation::AzureDocumentIntelligenceOCRConfig;
use crate::llms::azure_ai::ocr::transformation::AzureAIOCRConfig;
use crate::llms::base_llm::ocr::transformation::BaseOcrConfig;
use crate::llms::cohere::ocr::transformation::CohereParseConfig;
use crate::llms::mistral::ocr::transformation::MistralOCRConfig;
use crate::llms::reducto::ocr::transformation::{ReductoParseLegacyConfig, ReductoParseV3Config};
use crate::llms::vertex_ai::ocr::deepseek_transformation::VertexAIDeepSeekOCRConfig;
use crate::llms::vertex_ai::ocr::transformation::VertexAIOCRConfig;
use crate::routing_utils::provider::{CustomLlmProvider, get_custom_llm_provider};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -26,6 +35,24 @@ impl OcrConfigKind {
Self::VertexAi | Self::VertexDeepSeek => OcrProvider::VertexAi,
}
}
pub(crate) fn get_supported_ocr_params(self, model: &str) -> &'static [&'static str] {
match self {
Self::Cohere => CohereParseConfig.get_supported_ocr_params(model),
Self::Mistral => MistralOCRConfig.get_supported_ocr_params(model),
Self::AzureAi => AzureAIOCRConfig::default().get_supported_ocr_params(model),
Self::AzureCohere => {
AzureAICohereParseConfig::default().get_supported_ocr_params(model)
}
Self::AzureDocumentIntelligence => {
AzureDocumentIntelligenceOCRConfig.get_supported_ocr_params(model)
}
Self::ReductoLegacy => ReductoParseLegacyConfig.get_supported_ocr_params(model),
Self::ReductoV3 => ReductoParseV3Config.get_supported_ocr_params(model),
Self::VertexAi => VertexAIOCRConfig::default().get_supported_ocr_params(model),
Self::VertexDeepSeek => VertexAIDeepSeekOCRConfig.get_supported_ocr_params(model),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View file

@ -10,6 +10,7 @@ use super::provider_config::{OcrConfigKind, resolve_provider_config};
use crate::Error;
use crate::auth::{InputSource, TokenProviderHandle};
use crate::constants::OCR_HTTP_TIMEOUT_SECS;
use crate::params::OpaqueParams;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
@ -95,7 +96,7 @@ pub struct LiteLLMOcrRequest {
pub connection: OcrConnection,
pub hooks: Arc<dyn OcrHooks>,
pub litellm_call_id: Option<String>,
pub optional_params: Map<String, Value>,
pub optional_params: OpaqueParams,
pub input_sources: BTreeMap<String, InputSource>,
pub azure_ad_token_provider: Option<TokenProviderHandle>,
pub(crate) config: OcrConfigKind,
@ -106,7 +107,7 @@ impl LiteLLMOcrRequest {
model: String,
document: OcrDocument,
custom_llm_provider: Option<&str>,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<Self, Error> {
let (model, config) = resolve_provider_config(&model, custom_llm_provider)?;

View file

@ -6,6 +6,7 @@ use std::time::Duration;
use super::types::{LiteLLMOcrRequest, OcrConnection, OcrDocument};
use crate::Error;
use crate::auth::InputSource;
use crate::params::OpaqueParams;
use serde::{
Deserialize,
de::{DeserializeOwned, IntoDeserializer},
@ -13,26 +14,6 @@ use serde::{
use serde_json::{Map, Value};
const COMMON_OPTION_FIELDS: &[&str] = &["req_format", "extra_body", "max_response_bytes"];
const MISTRAL_OPTION_FIELDS: &[&str] = &[
"pages",
"include_image_base64",
"image_limit",
"image_min_size",
"bbox_annotation_format",
"document_annotation_format",
"document_annotation_prompt",
"extract_header",
"extract_footer",
"table_format",
"confidence_scores_granularity",
"include_blocks",
"id",
];
const DEEPSEEK_OPTION_FIELDS: &[&str] =
&["stream", "temperature", "max_tokens", "top_p", "n", "stop"];
const DOCUMENT_INTELLIGENCE_OPTION_FIELDS: &[&str] = &["pages", "features"];
const REDUCTO_V3_OPTION_FIELDS: &[&str] = &["formatting", "retrieval", "settings"];
const REDUCTO_LEGACY_OPTION_FIELDS: &[&str] = &["enhance"];
const AZURE_AUTH_OPTION_FIELDS: &[&str] = &[
"azure_ad_token",
"tenant_id",
@ -76,7 +57,7 @@ pub struct OcrWireRequest {
pub custom_llm_provider: Option<String>,
pub extra_headers: Option<Map<String, Value>>,
#[serde(default)]
pub optional_params: Map<String, Value>,
pub optional_params: OpaqueParams,
#[serde(default)]
pub input_sources: BTreeMap<String, InputSource>,
pub timeout_seconds: Option<f64>,
@ -92,17 +73,9 @@ pub fn consumed_optional_param_names(
) -> Result<Vec<&'static str>, Error> {
use super::provider_config::OcrConfigKind;
let (_, config) = super::provider_config::resolve_provider_config(model, custom_llm_provider)?;
let provider_fields: &[&str] = match config {
OcrConfigKind::Cohere | OcrConfigKind::AzureCohere => &["output_format"],
OcrConfigKind::Mistral | OcrConfigKind::AzureAi | OcrConfigKind::VertexAi => {
MISTRAL_OPTION_FIELDS
}
OcrConfigKind::AzureDocumentIntelligence => DOCUMENT_INTELLIGENCE_OPTION_FIELDS,
OcrConfigKind::ReductoV3 => REDUCTO_V3_OPTION_FIELDS,
OcrConfigKind::ReductoLegacy => REDUCTO_LEGACY_OPTION_FIELDS,
OcrConfigKind::VertexDeepSeek => DEEPSEEK_OPTION_FIELDS,
};
let (provider_model, config) =
super::provider_config::resolve_provider_config(model, custom_llm_provider)?;
let provider_fields = config.get_supported_ocr_params(&provider_model);
let auth_fields: &[&str] = match config {
OcrConfigKind::AzureAi
| OcrConfigKind::AzureDocumentIntelligence

View file

@ -0,0 +1,87 @@
use std::ops::Deref;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OpaqueParams(Map<String, Value>);
impl OpaqueParams {
pub fn into_inner(self) -> Map<String, Value> {
self.0
}
pub fn retain_supported(&self, supported: &[&str]) -> Self {
Self(
self.iter()
.filter(|(name, _)| supported.contains(&name.as_str()))
.map(|(name, value)| (name.clone(), value.clone()))
.collect(),
)
}
}
impl Deref for OpaqueParams {
type Target = Map<String, Value>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Map<String, Value>> for OpaqueParams {
fn from(value: Map<String, Value>) -> Self {
Self(value)
}
}
impl From<OpaqueParams> for Map<String, Value> {
fn from(value: OpaqueParams) -> Self {
value.0
}
}
impl FromIterator<(String, Value)> for OpaqueParams {
fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl IntoIterator for OpaqueParams {
type Item = (String, Value);
type IntoIter = serde_json::map::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn supported_keys_preserve_opaque_values() {
let params: OpaqueParams = serde_json::from_value(json!({
"object": {"future": [1, null]},
"null": null,
"unsupported": true
}))
.unwrap();
let retained = params.retain_supported(&["object", "null"]);
assert_eq!(
serde_json::to_value(retained).unwrap(),
json!({"object": {"future": [1, null]}, "null": null})
);
}
#[test]
fn outer_value_must_be_an_object() {
assert!(serde_json::from_value::<OpaqueParams>(json!(["value"])).is_err());
}
}

View file

@ -6,9 +6,9 @@ fn messages(value: Value) -> Vec<ChatMessage> {
serde_json::from_value(value).expect("valid messages")
}
fn params(value: Value) -> Map<String, Value> {
fn params(value: Value) -> OpaqueParams {
match value {
Value::Object(map) => map,
Value::Object(map) => map.into(),
other => panic!("params must be an object, got {other}"),
}
}
@ -419,13 +419,20 @@ fn resolves_the_messages_url_and_x_api_key_auth() {
let config = &ANTHROPIC_CHAT_COMPLETIONS_CONFIG;
assert_eq!(
config
.complete_url(None, "claude-sonnet-4-5", &Map::new(), &|_| None)
.complete_url(None, "claude-sonnet-4-5", &OpaqueParams::default(), &|_| {
None
})
.expect("url builds"),
"https://api.anthropic.com/v1/messages"
);
assert_eq!(
config
.auth(Some("sk-x"), "claude-sonnet-4-5", &Map::new(), &|_| None)
.auth(
Some("sk-x"),
"claude-sonnet-4-5",
&OpaqueParams::default(),
&|_| None,
)
.expect("auth resolves"),
ChatCompletionsAuth::Header {
name: "x-api-key",

View file

@ -11,6 +11,7 @@ use crate::chat_completions::types::{
};
use crate::constants::ANTHROPIC_OAUTH_TOKEN_PREFIX;
use crate::error::Error;
use crate::params::OpaqueParams;
use crate::providers::anthropic::messages::transformation::{
complete_anthropic_url, resolve_anthropic_api_key,
};
@ -77,7 +78,7 @@ impl ChatCompletionsProviderConfig for AnthropicChatCompletionsConfig {
&self,
api_base: Option<&str>,
_model: &str,
_optional_params: &Map<String, Value>,
_optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<String, Error> {
Ok(complete_anthropic_url(api_base, env_lookup))
@ -87,7 +88,7 @@ impl ChatCompletionsProviderConfig for AnthropicChatCompletionsConfig {
&self,
api_key: Option<&str>,
_model: &str,
_optional_params: &Map<String, Value>,
_optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<ChatCompletionsAuth, Error> {
Ok(ChatCompletionsAuth::Header {
@ -124,7 +125,7 @@ impl ChatCompletionsProviderConfig for AnthropicChatCompletionsConfig {
fn unsupported_reason(
&self,
messages: &[ChatMessage],
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
) -> Option<Unsupported> {
unsupported_param(self.supported_openai_params(), &[], optional_params)
.or_else(|| messages.iter().find_map(unsupported_message))
@ -141,10 +142,14 @@ impl ChatCompletionsProviderConfig for AnthropicChatCompletionsConfig {
&self,
model: &str,
messages: Vec<ChatMessage>,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<ProviderChatRequestData, Error> {
Ok(ProviderChatRequestData {
body: anthropic_body(model, &build_conversation(&messages), optional_params),
body: anthropic_body(
model,
&build_conversation(&messages),
optional_params.into_inner(),
),
})
}

View file

@ -7,6 +7,7 @@ use crate::audio_transcription::types::{
AudioTranscriptionRequestData, AudioTranscriptionResponseData,
};
use crate::error::{Error, json_type_name};
use crate::params::OpaqueParams;
pub use super::aws_base::{aws_auth_config, bedrock_model_id_and_region, resolve_bedrock_region};
use super::constants::{BEDROCK_RUNTIME_ENDPOINT_TEMPLATE, BEDROCK_SERVICE};
@ -54,7 +55,7 @@ impl AudioTranscriptionProviderConfig for BedrockAudioTranscriptionConfig {
&self,
_model: &str,
audio: Value,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<AudioTranscriptionRequestData, Error> {
let (data, format) = audio_fields(audio)?;
let mut instruction = "Transcribe the audio. Respond with only the transcript.".to_string();
@ -109,7 +110,7 @@ impl AudioTranscriptionProviderConfig for BedrockAudioTranscriptionConfig {
&self,
api_base: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<String, Error> {
let (model_id, model_region) = bedrock_model_id_and_region(model);
@ -131,7 +132,7 @@ impl AudioTranscriptionProviderConfig for BedrockAudioTranscriptionConfig {
fn auth_strategy(
&self,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<AudioTranscriptionAuth, Error> {
let (_, model_region) = bedrock_model_id_and_region(model);
@ -152,12 +153,12 @@ mod tests {
#[test]
fn request_matches_python_shape() {
let params = Map::from_iter([
let params = OpaqueParams::from(Map::from_iter([
("language".to_string(), json!("en")),
("prompt".to_string(), json!("Speaker names")),
("temperature".to_string(), json!(0)),
("timestamp_granularities".to_string(), json!(["word"])),
]);
]));
let params = BEDROCK_AUDIO_TRANSCRIPTION_CONFIG.map_transcription_params(&params);
let result = BEDROCK_AUDIO_TRANSCRIPTION_CONFIG
.transform_transcription_request(
@ -199,14 +200,17 @@ mod tests {
let result = BEDROCK_AUDIO_TRANSCRIPTION_CONFIG.transform_transcription_request(
"model",
json!({"data": "AQI="}),
Map::new(),
OpaqueParams::default(),
);
assert!(result.is_err());
}
#[test]
fn region_and_url_precedence_match_python() {
let params = Map::from_iter([("aws_region_name".to_string(), json!("eu-west-1"))]);
let params = OpaqueParams::from(Map::from_iter([(
"aws_region_name".to_string(),
json!("eu-west-1"),
)]));
let url = BEDROCK_AUDIO_TRANSCRIPTION_CONFIG
.complete_url(
None,

View file

@ -6,9 +6,9 @@ fn messages(value: Value) -> Vec<ChatMessage> {
serde_json::from_value(value).expect("valid messages")
}
fn params(value: Value) -> Map<String, Value> {
fn params(value: Value) -> OpaqueParams {
match value {
Value::Object(map) => map,
Value::Object(map) => map.into(),
other => panic!("params must be an object, got {other}"),
}
}
@ -225,9 +225,12 @@ fn builds_the_converse_url_from_the_region_in_the_model_id() {
let config = &BEDROCK_CHAT_COMPLETIONS_CONFIG;
assert_eq!(
config
.complete_url(None, "us-east-1/anthropic.claude-v2", &Map::new(), &|_| {
None
})
.complete_url(
None,
"us-east-1/anthropic.claude-v2",
&OpaqueParams::default(),
&|_| { None }
)
.expect("url builds"),
"https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-v2/converse"
);
@ -239,13 +242,23 @@ fn falls_back_to_the_region_env_then_the_default_region() {
let with_env = |key: &str| (key == "AWS_REGION_NAME").then(|| "eu-west-1".to_string());
assert_eq!(
config
.complete_url(None, "anthropic.claude-v2", &Map::new(), &with_env)
.complete_url(
None,
"anthropic.claude-v2",
&OpaqueParams::default(),
&with_env
)
.expect("url builds"),
"https://bedrock-runtime.eu-west-1.amazonaws.com/model/anthropic.claude-v2/converse"
);
assert_eq!(
config
.complete_url(None, "anthropic.claude-v2", &Map::new(), &|_| None)
.complete_url(
None,
"anthropic.claude-v2",
&OpaqueParams::default(),
&|_| None
)
.expect("url builds"),
"https://bedrock-runtime.us-west-2.amazonaws.com/model/anthropic.claude-v2/converse"
);
@ -276,7 +289,7 @@ fn signs_with_sigv4_in_the_resolved_region() {
.auth(
None,
"eu-central-1/anthropic.claude-v2",
&Map::new(),
&OpaqueParams::default(),
&|_| None
)
.expect("auth resolves"),
@ -300,7 +313,7 @@ fn a_bearer_token_outranks_sigv4_the_way_python_resolves_it() {
.auth(
api_key,
"eu-central-1/anthropic.claude-v2",
&Map::new(),
&OpaqueParams::default(),
env,
)
.expect("auth resolves")
@ -542,7 +555,7 @@ fn leaves_a_complete_converse_url_untouched() {
.complete_url(
Some(already_built),
"anthropic.claude-v2",
&Map::new(),
&OpaqueParams::default(),
&|_| None
)
.expect("url builds"),

View file

@ -12,6 +12,7 @@ use crate::chat_completions::types::{
ProviderChatResponseData,
};
use crate::error::Error;
use crate::params::OpaqueParams;
use super::super::aws_base::{bedrock_model_id_and_region, resolve_bedrock_region};
use super::super::constants::{AWS_BEARER_TOKEN_BEDROCK, BEDROCK_RUNTIME_ENDPOINT_TEMPLATE};
@ -109,7 +110,7 @@ impl ChatCompletionsProviderConfig for BedrockChatCompletionsConfig {
&self,
api_base: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<String, Error> {
let (model_id, model_region) = bedrock_model_id_and_region(model);
@ -136,7 +137,7 @@ impl ChatCompletionsProviderConfig for BedrockChatCompletionsConfig {
&self,
api_key: Option<&str>,
model: &str,
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<ChatCompletionsAuth, Error> {
// Python reads `api_key` as the Bedrock bearer token and consults the
@ -174,7 +175,7 @@ impl ChatCompletionsProviderConfig for BedrockChatCompletionsConfig {
fn unsupported_reason(
&self,
messages: &[ChatMessage],
optional_params: &Map<String, Value>,
optional_params: &OpaqueParams,
) -> Option<Unsupported> {
unsupported_param(
self.supported_openai_params(),
@ -212,7 +213,7 @@ impl ChatCompletionsProviderConfig for BedrockChatCompletionsConfig {
&self,
_model: &str,
messages: Vec<ChatMessage>,
optional_params: Map<String, Value>,
optional_params: OpaqueParams,
) -> Result<ProviderChatRequestData, Error> {
Ok(ProviderChatRequestData {
body: converse_body(&build_conversation(&messages), &optional_params),

View file

@ -63,7 +63,7 @@ async fn rejects_invalid_pages_features_and_format() {
api_base: Some("http://127.0.0.1:1".into()),
custom_llm_provider: None,
extra_headers: None,
optional_params: options.as_object().unwrap().clone(),
optional_params: options.as_object().unwrap().clone().into(),
input_sources: Default::default(),
timeout_seconds: None,
});

View file

@ -27,7 +27,8 @@ fn request_boundary_selects_mistral_and_rejects_unknown_providers() {
optional_params: json!({"extract_header":true,"unknown":42})
.as_object()
.unwrap()
.clone(),
.clone()
.into(),
input_sources: Default::default(),
timeout_seconds: None,
};
@ -40,7 +41,7 @@ fn request_boundary_selects_mistral_and_rejects_unknown_providers() {
api_base: None,
custom_llm_provider: Some("unknown".into()),
extra_headers: None,
optional_params: serde_json::Map::new(),
optional_params: Default::default(),
input_sources: Default::default(),
timeout_seconds: None,
})

View file

@ -29,7 +29,7 @@ pub(crate) fn wire_request(model: &str, base: &str, options: Value) -> LiteLLMOc
api_base: Some(base.into()),
custom_llm_provider: None,
extra_headers: None,
optional_params: options.as_object().unwrap().clone(),
optional_params: options.as_object().unwrap().clone().into(),
input_sources: Default::default(),
timeout_seconds: Some(2.0),
})

View file

@ -40,7 +40,7 @@ fn prepare_transcription(
api_base: api_base.as_deref(),
custom_llm_provider: custom_llm_provider.as_deref(),
extra_headers,
optional_params,
optional_params: optional_params.into(),
timeout,
})
.await

View file

@ -37,7 +37,7 @@ fn prepare_chat_completions(
run_chat_completions(ChatCompletionsRequest {
model: &model,
messages: Value::Array(messages),
optional_params,
optional_params: optional_params.into(),
api_key: api_key.as_deref(),
api_base: api_base.as_deref(),
custom_llm_provider: custom_llm_provider.as_deref(),
@ -56,7 +56,7 @@ fn chat_completions_decline(
#[pyo3(from_py_with = litellm_python_interop::from_py)] optional_params: Option<Value>,
custom_llm_provider: Option<String>,
) -> PyResult<Option<String>> {
let optional_params = object_or_empty("optional_params", optional_params)?;
let optional_params = object_or_empty("optional_params", optional_params)?.into();
Ok(chat_completions_decline_reason(
&model,
custom_llm_provider.as_deref(),

View file

@ -143,7 +143,7 @@ pub(super) fn project_request(
api_base: arguments.api_base()?,
custom_llm_provider,
extra_headers: arguments.extra_headers()?,
optional_params,
optional_params: optional_params.into(),
input_sources,
timeout_seconds: arguments.timeout_seconds()?,
};
@ -500,7 +500,7 @@ kwargs = {'api_key': key}
api_base: None,
custom_llm_provider: None,
extra_headers: None,
optional_params: Map::new(),
optional_params: Default::default(),
input_sources: Default::default(),
timeout_seconds: None,
}) {

View file

@ -44,7 +44,7 @@ fn prepare_ocr(
api_base,
custom_llm_provider,
extra_headers,
optional_params,
optional_params: optional_params.into(),
input_sources,
timeout_seconds: timeout.map(|value| value.as_secs_f64()),
})?;