cloudiful_docling_convert/api/
vlm_config.rs1use 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}