From 0502c21ad8a4e8f3a556a3af56d6333c31aa6d25 Mon Sep 17 00:00:00 2001 From: Tai An Date: Sat, 8 Aug 2026 06:21:04 -0700 Subject: [PATCH] feat(doc-intel): expose model_id and analysis features on DocumentIntelligenceConverter (#2273) DocumentIntelligenceConverter hardcoded model_id="prebuilt-layout" and an unconditional FORMULAS + OCR_HIGH_RESOLUTION + STYLE_FONT add-on set for every OCR-eligible input, with no way to select a cheaper analysis through the public API. Per Azure pay-as-you-go pricing that is roughly 10-19x the cost of prebuilt-read with no add-ons. Add two keyword-only parameters, plumbed through MarkItDown as docintel_model_id / docintel_features: - model_id: str = "prebuilt-layout" - features: List[str] | None = None (None keeps the current add-on set, [] disables all add-ons) Defaults are unchanged, so existing callers get identical requests. Add-ons are still never sent for office file types, which the service does not support them for, and the caller's list is copied rather than aliased into the request. --- .../markitdown/src/markitdown/_markitdown.py | 8 ++ .../converters/_doc_intel_converter.py | 15 +++- .../markitdown/tests/test_docintel_options.py | 74 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/markitdown/tests/test_docintel_options.py diff --git a/packages/markitdown/src/markitdown/_markitdown.py b/packages/markitdown/src/markitdown/_markitdown.py index 2b4f2a695..39a3f139c 100644 --- a/packages/markitdown/src/markitdown/_markitdown.py +++ b/packages/markitdown/src/markitdown/_markitdown.py @@ -222,6 +222,14 @@ def enable_builtins(self, **kwargs) -> None: if docintel_version is not None: docintel_args["api_version"] = docintel_version + docintel_model_id = kwargs.get("docintel_model_id") + if docintel_model_id is not None: + docintel_args["model_id"] = docintel_model_id + + docintel_features = kwargs.get("docintel_features") + if docintel_features is not None: + docintel_args["features"] = docintel_features + self.register_converter( DocumentIntelligenceConverter(**docintel_args), ) diff --git a/packages/markitdown/src/markitdown/converters/_doc_intel_converter.py b/packages/markitdown/src/markitdown/converters/_doc_intel_converter.py index adc94372a..4e8896055 100644 --- a/packages/markitdown/src/markitdown/converters/_doc_intel_converter.py +++ b/packages/markitdown/src/markitdown/converters/_doc_intel_converter.py @@ -146,6 +146,8 @@ def __init__( DocumentIntelligenceFileType.BMP, DocumentIntelligenceFileType.TIFF, ], + model_id: str = "prebuilt-layout", + features: List[str] | None = None, ): """ Initialize the DocumentIntelligenceConverter. @@ -155,10 +157,18 @@ def __init__( api_version (str): The API version to use. Defaults to "2024-07-31-preview". credential (AzureKeyCredential | TokenCredential | None): The credential to use for authentication. file_types (List[DocumentIntelligenceFileType]): The file types to accept. Defaults to all supported file types. + model_id (str): The Document Intelligence model to analyze with. Defaults to "prebuilt-layout". + Pass e.g. "prebuilt-read" for cheaper plain-text extraction. + features (List[str] | None): The analysis add-on features to request for OCR-eligible + file types. Defaults to None, which keeps the built-in set (formulas, high + resolution OCR, and font style). Pass an empty list to disable all add-ons. + Add-ons are never sent for office file types, which do not support them. """ super().__init__() self._file_types = file_types + self._features = features + self.model_id = model_id # Raise an error if the dependencies are not available. # This is different than other converters since this one isn't even instantiated @@ -228,6 +238,9 @@ def _analysis_features(self, stream_info: StreamInfo) -> List[str]: if mimetype.startswith(prefix): return [] + if self._features is not None: + return list(self._features) + return [ DocumentAnalysisFeature.FORMULAS, # enable formula extraction DocumentAnalysisFeature.OCR_HIGH_RESOLUTION, # enable high resolution OCR @@ -242,7 +255,7 @@ def convert( ) -> DocumentConverterResult: # Extract the text using Azure Document Intelligence poller = self.doc_intel_client.begin_analyze_document( - model_id="prebuilt-layout", + model_id=self.model_id, body=AnalyzeDocumentRequest(bytes_source=file_stream.read()), features=self._analysis_features(stream_info), output_content_format=CONTENT_FORMAT, # TODO: replace with "ContentFormat.MARKDOWN" when the bug is fixed diff --git a/packages/markitdown/tests/test_docintel_options.py b/packages/markitdown/tests/test_docintel_options.py new file mode 100644 index 000000000..116ff43cb --- /dev/null +++ b/packages/markitdown/tests/test_docintel_options.py @@ -0,0 +1,74 @@ +import io + +from markitdown._stream_info import StreamInfo +from markitdown.converters._doc_intel_converter import ( + DocumentIntelligenceConverter, + DocumentIntelligenceFileType, +) + +ALL_TYPES = list(DocumentIntelligenceFileType) + + +def _make_converter(features=None, model_id="prebuilt-layout"): + """Build a converter without touching Azure (mirrors test_docintel_html.py).""" + conv = DocumentIntelligenceConverter.__new__(DocumentIntelligenceConverter) + conv._file_types = ALL_TYPES + conv._features = features + conv.model_id = model_id + return conv + + +PDF = StreamInfo(mimetype="application/pdf", extension=".pdf") +DOCX = StreamInfo(mimetype=None, extension=".docx") + + +def _values(features): + """Azure's feature enum is a str-enum; compare on the wire values.""" + return [getattr(f, "value", f) for f in features] + + +def test_default_features_unchanged(): + conv = _make_converter() + assert _values(conv._analysis_features(PDF)) == [ + "formulas", + "ocrHighResolution", + "styleFont", + ] + + +def test_default_model_id_unchanged(): + assert _make_converter().model_id == "prebuilt-layout" + + +def test_empty_features_disables_addons(): + conv = _make_converter(features=[]) + assert conv._analysis_features(PDF) == [] + + +def test_explicit_features_are_used(): + conv = _make_converter(features=["ocrHighResolution"]) + assert _values(conv._analysis_features(PDF)) == ["ocrHighResolution"] + + +def test_explicit_features_not_sent_for_office_types(): + """Office file types do not support add-ons, so they stay empty.""" + conv = _make_converter(features=["ocrHighResolution"]) + assert conv._analysis_features(DOCX) == [] + + +def test_explicit_features_list_is_copied(): + """The caller's list must not be aliased into the request.""" + requested = ["ocrHighResolution"] + conv = _make_converter(features=requested) + returned = conv._analysis_features(PDF) + returned.append("styleFont") + assert requested == ["ocrHighResolution"] + + +def test_custom_model_id_is_stored(): + assert _make_converter(model_id="prebuilt-read").model_id == "prebuilt-read" + + +def test_accepts_still_works(): + """Sanity: the new attributes do not disturb accepts().""" + assert _make_converter().accepts(io.BytesIO(b""), PDF)