From 8c21a988b7b73303daf82e8c21698f3924a14382 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 19 Sep 2026 18:31:49 +0000 Subject: [PATCH] fix(ocr): set DeepSeek OCR sampling defaults Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../vertex_ai/ocr/deepseek_transformation.rs | 65 +++++++++++++++++-- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/litellm-rust/crates/llms/src/vertex_ai/ocr/deepseek_transformation.rs b/litellm-rust/crates/llms/src/vertex_ai/ocr/deepseek_transformation.rs index f0b035621fa..9a23deefb89 100644 --- a/litellm-rust/crates/llms/src/vertex_ai/ocr/deepseek_transformation.rs +++ b/litellm-rust/crates/llms/src/vertex_ai/ocr/deepseek_transformation.rs @@ -19,6 +19,13 @@ const MODEL_PREFIX: &str = "deepseek-ai/"; const DEFAULT_LOCATION: &str = "us-central1"; const DEEPSEEK_OCR_PARAMS: &[&str] = &["stream", "temperature", "max_tokens", "top_p", "n", "stop"]; +/// DeepSeek-OCR is a transcription model: at the endpoint's default sampling temperature it +/// hallucinates extra text, so requests are greedy unless the caller sets a temperature. +const DEFAULT_TEMPERATURE: f64 = 0.0; +/// Greedy decoding on dense screenshots falls into repetition loops that run to the token limit; +/// a mild penalty breaks them without changing clean-document output. +const DEFAULT_REPETITION_PENALTY: f64 = 1.05; + pub type DeepSeekOcrParams = OpaqueParams; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -171,11 +178,19 @@ impl BaseOcrConfig for VertexAIDeepSeekOCRConfig { image_url: document.source().to_string(), }], }], - params: optional_params - .iter() - .filter(|(name, _)| DEEPSEEK_OCR_PARAMS.contains(&name.as_str())) - .map(|(name, value)| (name.clone(), value.clone())) - .collect(), + params: [ + ("temperature", DEFAULT_TEMPERATURE), + ("repetition_penalty", DEFAULT_REPETITION_PENALTY), + ] + .into_iter() + .map(|(name, value)| (name.to_string(), Value::from(value))) + .chain( + optional_params + .iter() + .filter(|(name, _)| DEEPSEEK_OCR_PARAMS.contains(&name.as_str())) + .map(|(name, value)| (name.clone(), value.clone())), + ) + .collect(), }) } } @@ -484,6 +499,46 @@ mod tests { assert!(result.get("ignored").is_none()); } + #[test] + fn request_uses_greedy_defaults_unless_the_caller_overrides_them() { + let request = |params: DeepSeekOcrParams| { + serde_json::to_value( + VertexAIDeepSeekOCRConfig + .transform_ocr_request( + "deepseek-ai/deepseek-ocr-maas", + document(), + ¶ms, + &[], + ) + .unwrap(), + ) + .unwrap() + }; + let defaults = request(DeepSeekOcrParams::default()); + assert_eq!(defaults["temperature"], 0.0); + assert_eq!(defaults["repetition_penalty"], 1.05); + assert_eq!( + request(serde_json::from_value(json!({"temperature":0.7})).unwrap())["temperature"], + 0.7 + ); + } + + #[test] + fn caller_temperature_argument_overrides_the_greedy_default_in_the_composed_body() { + let arguments = serde_json::from_value(json!({"temperature":0.7})).unwrap(); + let body = VertexAIDeepSeekOCRConfig + .transform_ocr_request( + "deepseek-ai/deepseek-ocr-maas", + document(), + &DeepSeekOcrParams::default(), + &[], + ) + .unwrap(); + let composed = + litellm_core_utils::call_arguments::compose_body(&arguments, &body, &[]).unwrap(); + assert_eq!(composed["temperature"], 0.7); + } + #[rstest] #[case(json!({"type":"image_url","image_url":"data:image/png;base64,AA=="}))] #[case(json!({"type":"document_url","document_url":"data:application/pdf;base64,AA=="}))]