Skip to main content

cloudiful_docling_convert/api/
vlm_config.rs

1use crate::error::{PdfConvertError, Result};
2
3use super::docling::DoclingConfig;
4
5const OPTIONAL_VLM_FIELDS: [&str; 5] = [
6    "docling_openai_base_url",
7    "docling_vlm_pipeline_model",
8    "docling_picture_description_model",
9    "docling_code_formula_model",
10    "docling_api_key",
11];
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub(crate) struct ResolvedVlmConfig {
15    pub openai_base_url: String,
16    pub vlm_pipeline_model: String,
17    pub picture_description_model: String,
18    pub code_formula_model: String,
19    pub api_key: String,
20}
21
22impl DoclingConfig {
23    pub fn without_vlm(base_url: impl Into<String>) -> Self {
24        Self {
25            base_url: base_url.into(),
26            openai_base_url: String::new(),
27            vlm_pipeline_model: String::new(),
28            picture_description_model: String::new(),
29            code_formula_model: String::new(),
30            api_key: None,
31        }
32    }
33
34    pub(crate) fn resolved_vlm_config(&self) -> Result<Option<ResolvedVlmConfig>> {
35        let openai_base_url = trimmed_non_empty(&self.openai_base_url);
36        let vlm_pipeline_model = trimmed_non_empty(&self.vlm_pipeline_model);
37        let picture_description_model = trimmed_non_empty(&self.picture_description_model);
38        let code_formula_model = trimmed_non_empty(&self.code_formula_model);
39        let api_key = self.api_key.as_deref().and_then(trimmed_non_empty);
40
41        let fields = [
42            openai_base_url.as_ref(),
43            vlm_pipeline_model.as_ref(),
44            picture_description_model.as_ref(),
45            code_formula_model.as_ref(),
46            api_key.as_ref(),
47        ];
48        let present_count = fields.iter().filter(|value| value.is_some()).count();
49
50        if present_count == 0 {
51            return Ok(None);
52        }
53
54        if present_count != fields.len() {
55            return Err(PdfConvertError::validation_error(
56                "docling runtime",
57                format!(
58                    "optional VLM runtime config is incomplete; provide all of: {}, or leave all unset",
59                    OPTIONAL_VLM_FIELDS.join(", ")
60                ),
61            ));
62        }
63
64        Ok(Some(ResolvedVlmConfig {
65            openai_base_url: openai_base_url.expect("openai_base_url present"),
66            vlm_pipeline_model: vlm_pipeline_model.expect("vlm_pipeline_model present"),
67            picture_description_model: picture_description_model
68                .expect("picture_description_model present"),
69            code_formula_model: code_formula_model.expect("code_formula_model present"),
70            api_key: api_key.expect("api_key present"),
71        }))
72    }
73}
74
75fn trimmed_non_empty(value: &str) -> Option<String> {
76    let trimmed = value.trim();
77    if trimmed.is_empty() {
78        None
79    } else {
80        Some(trimmed.to_string())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    fn config() -> DoclingConfig {
89        DoclingConfig {
90            base_url: "http://127.0.0.1:5001/v1".into(),
91            openai_base_url: String::new(),
92            vlm_pipeline_model: String::new(),
93            picture_description_model: String::new(),
94            code_formula_model: String::new(),
95            api_key: None,
96        }
97    }
98
99    #[test]
100    fn missing_vlm_bundle_is_allowed() {
101        assert!(config().resolved_vlm_config().unwrap().is_none());
102    }
103
104    #[test]
105    fn partial_vlm_bundle_is_rejected() {
106        let mut config = config();
107        config.openai_base_url = "https://api.openai.com/v1".into();
108        config.vlm_pipeline_model = "gpt-4o-mini".into();
109
110        let error = config.resolved_vlm_config().unwrap_err();
111        assert!(
112            error
113                .to_string()
114                .contains("optional VLM runtime config is incomplete")
115        );
116    }
117}