cloudiful-docling-convert 0.3.5

Core Rust library for Docling-backed document conversion
Documentation
use crate::error::{PdfConvertError, Result};

use super::docling::DoclingConfig;

const OPTIONAL_VLM_FIELDS: [&str; 5] = [
    "OPENAI_BASE_URL",
    "VLM_PIPELINE_MODEL",
    "PICTURE_DESCRIPTION_MODEL",
    "CODE_FORMULA_MODEL",
    "OPENAI_API_KEY",
];

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResolvedVlmConfig {
    pub openai_base_url: String,
    pub vlm_pipeline_model: String,
    pub picture_description_model: String,
    pub code_formula_model: String,
    pub api_key: String,
}

impl DoclingConfig {
    pub(crate) fn resolved_vlm_config(&self) -> Result<Option<ResolvedVlmConfig>> {
        let openai_base_url = trimmed_non_empty(&self.openai_base_url);
        let vlm_pipeline_model = trimmed_non_empty(&self.vlm_pipeline_model);
        let picture_description_model = trimmed_non_empty(&self.picture_description_model);
        let code_formula_model = trimmed_non_empty(&self.code_formula_model);
        let api_key = self.openai_api_key.as_deref().and_then(trimmed_non_empty);

        let fields = [
            openai_base_url.as_ref(),
            vlm_pipeline_model.as_ref(),
            picture_description_model.as_ref(),
            code_formula_model.as_ref(),
            api_key.as_ref(),
        ];
        let present_count = fields.iter().filter(|value| value.is_some()).count();

        if present_count == 0 {
            return Ok(None);
        }

        if present_count != fields.len() {
            return Err(PdfConvertError::validation_error(
                "VLM runtime",
                format!(
                    "optional VLM runtime config is incomplete; provide all of: {}, or leave all unset",
                    OPTIONAL_VLM_FIELDS.join(", ")
                ),
            ));
        }

        Ok(Some(ResolvedVlmConfig {
            openai_base_url: openai_base_url.expect("openai_base_url present"),
            vlm_pipeline_model: vlm_pipeline_model.expect("vlm_pipeline_model present"),
            picture_description_model: picture_description_model
                .expect("picture_description_model present"),
            code_formula_model: code_formula_model.expect("code_formula_model present"),
            api_key: api_key.expect("openai api key present"),
        }))
    }
}

fn trimmed_non_empty(value: &str) -> Option<String> {
    let trimmed = value.trim();
    (!trimmed.is_empty()).then(|| trimmed.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn config() -> DoclingConfig {
        DoclingConfig::without_vlm("http://127.0.0.1:5001/v1")
    }

    #[test]
    fn missing_vlm_bundle_is_allowed() {
        assert!(config().resolved_vlm_config().unwrap().is_none());
    }

    #[test]
    fn partial_vlm_bundle_is_rejected() {
        let mut config = config();
        config.openai_base_url = "https://api.openai.com/v1".into();
        config.vlm_pipeline_model = "gpt-4o-mini".into();

        let error = config.resolved_vlm_config().unwrap_err();
        assert!(
            error
                .to_string()
                .contains("optional VLM runtime config is incomplete")
        );
    }
}