Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9385731
model runtime refactoring
dolfim-ibm Jan 26, 2026
d355a79
Merge remote-tracking branch 'origin/main' into feat-model-runtimes
dolfim-ibm Jan 26, 2026
d5b7e2d
add test
dolfim-ibm Jan 26, 2026
a8cae1e
fix code formula preset
dolfim-ibm Jan 27, 2026
ab29cee
batch prediction
dolfim-ibm Jan 27, 2026
35da1f8
use presets and new vlm options in CLI
dolfim-ibm Jan 28, 2026
f9b803e
use new model settings by default
dolfim-ibm Jan 28, 2026
daedeee
running
dolfim-ibm Jan 28, 2026
dfb610e
update examples
dolfim-ibm Jan 29, 2026
f48d8b4
fixes for running examples
dolfim-ibm Jan 30, 2026
0e1007a
keep old stage
dolfim-ibm Jan 30, 2026
dc406cd
update model
dolfim-ibm Jan 30, 2026
46188c1
use granite 3.3 and set options
dolfim-ibm Jan 30, 2026
1cfbcfd
revisit init logic and propagate the proper options to the runtimes
dolfim-ibm Jan 30, 2026
7957842
update all stages with original setup
dolfim-ibm Jan 30, 2026
1d6264c
per stage registry
dolfim-ibm Jan 30, 2026
6278eb5
use chat template
dolfim-ibm Jan 30, 2026
aa0bb26
remove duplicated predict() and factor out some utils
dolfim-ibm Feb 1, 2026
76f986b
working picture description examples
dolfim-ibm Feb 1, 2026
334ae81
add granite docling as code formula model
dolfim-ibm Feb 1, 2026
daa90bf
rename code formula presets
dolfim-ibm Feb 1, 2026
1a3d2b0
fix running minimal_vlm example
dolfim-ibm Feb 1, 2026
afa2d36
add all models to presets and run compare_vlm
dolfim-ibm Feb 1, 2026
ab748a2
remove unused repo_id
dolfim-ibm Feb 1, 2026
eb6c53a
Merge remote-tracking branch 'origin/main' into feat-model-runtimes
dolfim-ibm Feb 1, 2026
7b96837
update vlm api model example
dolfim-ibm Feb 1, 2026
036b659
fix legacy examples
dolfim-ibm Feb 1, 2026
1c0b53a
add another legacy example
dolfim-ibm Feb 1, 2026
8dc0fcd
fix test
dolfim-ibm Feb 1, 2026
e65bd75
avoid automatic fallback to mlx and fix end_of_utterance in codeformula
dolfim-ibm Feb 1, 2026
c07c3b1
move vlm_convert_model
dolfim-ibm Feb 1, 2026
053e611
use new vlm runtime class
dolfim-ibm Feb 2, 2026
474d00e
flasg for CI
dolfim-ibm Feb 2, 2026
c2edf64
rename runtimes to explicit vlm_runtimes
dolfim-ibm Feb 2, 2026
2259a55
renaming from runtime to inference engine and model families
dolfim-ibm Feb 3, 2026
bbf4821
fixes
dolfim-ibm Feb 3, 2026
356bfa0
fix test
dolfim-ibm Feb 3, 2026
92a7e8d
add docs with stages
dolfim-ibm Feb 3, 2026
256d9a2
update docs catalog page
dolfim-ibm Feb 3, 2026
e186e6c
Merge remote-tracking branch 'origin/main' into feat-model-runtimes
dolfim-ibm Feb 4, 2026
e1e52b0
rename runtime to inference engine
dolfim-ibm Feb 4, 2026
514d99f
Enable pipeline override and reuse with compatible options (WIP)
cau-git Feb 4, 2026
4ea1204
Merge from main
cau-git Feb 5, 2026
34c0dbc
Merge branch 'main' of github.com:DS4SD/docling into cau/allow-pipeli…
cau-git Feb 10, 2026
dd9eb32
fix: enforce strict compatible pipeline overrides without reinit
cau-git Feb 10, 2026
21440d8
Fix narrow type assertions
cau-git Feb 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docling/datamodel/pipeline_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,21 @@ class PipelineOptions(BaseOptions):
),
] = None

def _get_compatibility_payload(self) -> dict[str, Any]:
"""Get payload for compatibility hashing.

Base implementation returns full model dump. Subclasses with do_* fields
should override to exclude them.

Returns:
Dictionary suitable for compatibility hashing
"""
return self.model_dump(serialize_as_any=True)

def _get_runtime_toggle_payload(self) -> dict[str, bool]:
"""Get payload with runtime-togglable do_* fields."""
return {}


class ConvertPipelineOptions(PipelineOptions):
"""Base configuration for document conversion pipelines."""
Expand Down Expand Up @@ -980,6 +995,22 @@ class ConvertPipelineOptions(PipelineOptions):
False # True: extract data in tabular format from bar-, pie and line-charts
)

def _get_compatibility_payload(self) -> dict[str, Any]:
"""Override to exclude do_* fields from compatibility check."""
payload = super()._get_compatibility_payload()
# Explicitly exclude do_* fields owned by this class
payload.pop("do_picture_classification", None)
payload.pop("do_picture_description", None)
payload.pop("do_chart_extraction", None)
return payload

def _get_runtime_toggle_payload(self) -> dict[str, bool]:
return {
"do_picture_classification": self.do_picture_classification,
"do_picture_description": self.do_picture_description,
"do_chart_extraction": self.do_chart_extraction,
}


class PaginatedPipelineOptions(ConvertPipelineOptions):
"""Configuration for pipelines processing paginated documents."""
Expand Down Expand Up @@ -1333,6 +1364,28 @@ class PdfPipelineOptions(PaginatedPipelineOptions):
),
] = 100

def _get_compatibility_payload(self) -> dict[str, Any]:
"""Override to exclude do_* fields from compatibility check."""
payload = super()._get_compatibility_payload()
# Explicitly exclude do_* fields owned by this class
payload.pop("do_table_structure", None)
payload.pop("do_ocr", None)
payload.pop("do_code_enrichment", None)
payload.pop("do_formula_enrichment", None)
return payload

def _get_runtime_toggle_payload(self) -> dict[str, bool]:
payload = super()._get_runtime_toggle_payload()
payload.update(
{
"do_table_structure": self.do_table_structure,
"do_ocr": self.do_ocr,
"do_code_enrichment": self.do_code_enrichment,
"do_formula_enrichment": self.do_formula_enrichment,
}
)
return payload


class ProcessingPipeline(str, Enum):
"""Available document processing pipeline types for different use cases.
Expand Down
142 changes: 132 additions & 10 deletions docling/document_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,69 @@ def _get_initialized_pipelines(
) -> dict[tuple[Type[BasePipeline], str], BasePipeline]:
return self.initialized_pipelines

def _get_pipeline_options_hash(self, pipeline_options: PipelineOptions) -> str:
"""Generate a hash of pipeline options to use as part of the cache key."""
options_str = str(pipeline_options.model_dump())
def _get_pipeline_options_hash(
self, pipeline_options: PipelineOptions, for_compatibility: bool = False
) -> str:
"""Generate a hash of pipeline options.

Args:
pipeline_options: Options to hash
for_compatibility: If True, use compatibility payload (excludes do_* fields)

Returns:
MD5 hash string
"""
if for_compatibility:
options_str = str(pipeline_options._get_compatibility_payload())
else:
options_str = str(pipeline_options.model_dump(serialize_as_any=True))

return hashlib.md5(
options_str.encode("utf-8"), usedforsecurity=False
).hexdigest()

def _check_options_compatibility(
self, initialized_options: PipelineOptions, override_options: PipelineOptions
) -> bool:
"""Check if override options are compatible with initialized pipeline.

Compatible means:
- Same options class type
- Compatibility payloads match (non-do_* fields are identical)
- Override does not enable do_* flags that were disabled at init

Args:
initialized_options: Options used to initialize pipeline
override_options: Options to use for this execution

Returns:
True if compatible, False otherwise
"""
# Must be same class
if type(initialized_options) is not type(override_options):
return False

# Compatibility hashes must match (all fields except do_*)
init_compat_hash = self._get_pipeline_options_hash(
initialized_options, for_compatibility=True
)
override_compat_hash = self._get_pipeline_options_hash(
override_options, for_compatibility=True
)

if init_compat_hash != override_compat_hash:
return False

initialized_toggles = initialized_options._get_runtime_toggle_payload()
override_toggles = override_options._get_runtime_toggle_payload()

for toggle_name, override_value in override_toggles.items():
init_value = initialized_toggles[toggle_name]
if override_value and not init_value:
return False

return True

def initialize_pipeline(self, format: InputFormat):
"""Initialize the conversion pipeline for the selected format.

Expand Down Expand Up @@ -289,6 +345,7 @@ def convert(
max_num_pages: int = sys.maxsize,
max_file_size: int = sys.maxsize,
page_range: PageRange = DEFAULT_PAGE_RANGE,
format_options: Optional[dict[InputFormat, PipelineOptions]] = None,
) -> ConversionResult:
"""Convert one document fetched from a file path, URL, or DocumentStream.

Expand All @@ -306,6 +363,9 @@ def convert(
Documents exceeding this number will not be converted.
max_file_size: Maximum file size to convert.
page_range: Range of pages to convert.
format_options: Optional mapping of formats to pipeline options to override
initialized options. Must be compatible: same options class, identical
non-do_* fields, and do_* flags may only change from True to False.

Returns:
The conversion result, which contains a `DoclingDocument` in the `document`
Expand All @@ -321,6 +381,7 @@ def convert(
max_file_size=max_file_size,
headers=headers,
page_range=page_range,
format_options=format_options,
)
return next(all_res)

Expand All @@ -333,6 +394,7 @@ def convert_all(
max_num_pages: int = sys.maxsize,
max_file_size: int = sys.maxsize,
page_range: PageRange = DEFAULT_PAGE_RANGE,
format_options: Optional[dict[InputFormat, PipelineOptions]] = None,
) -> Iterator[ConversionResult]:
"""Convert multiple documents from file paths, URLs, or DocumentStreams.

Expand All @@ -346,6 +408,9 @@ def convert_all(
max_file_size: Maximum number of pages accepted per document. Documents
exceeding this number will be skipped.
page_range: Range of pages to convert in each document.
format_options: Optional mapping of formats to pipeline options to override
initialized options. Must be compatible: same options class, identical
non-do_* fields, and do_* flags may only change from True to False.

Yields:
The conversion results, each containing a `DoclingDocument` in the
Expand All @@ -362,7 +427,11 @@ def convert_all(
conv_input = _DocumentConversionInput(
path_or_stream_iterator=source, limits=limits, headers=headers
)
conv_res_iter = self._convert(conv_input, raises_on_error=raises_on_error)
conv_res_iter = self._convert(
conv_input,
raises_on_error=raises_on_error,
override_format_options=format_options,
)

had_result = False
for conv_res in conv_res_iter:
Expand Down Expand Up @@ -438,7 +507,10 @@ def convert_string(
raise ValueError(f"format {format} is not supported in `convert_string`")

def _convert(
self, conv_input: _DocumentConversionInput, raises_on_error: bool
self,
conv_input: _DocumentConversionInput,
raises_on_error: bool,
override_format_options: Optional[dict[InputFormat, PipelineOptions]] = None,
) -> Iterator[ConversionResult]:
start_time = time.monotonic()

Expand All @@ -448,7 +520,9 @@ def _convert(
):
_log.info("Going to convert document batch...")
process_func = partial(
self._process_document, raises_on_error=raises_on_error
self._process_document,
raises_on_error=raises_on_error,
override_format_options=override_format_options,
)

if (
Expand Down Expand Up @@ -505,13 +579,20 @@ def _get_pipeline(self, doc_format: InputFormat) -> Optional[BasePipeline]:
return self.initialized_pipelines[cache_key]

def _process_document(
self, in_doc: InputDocument, raises_on_error: bool
self,
in_doc: InputDocument,
raises_on_error: bool,
override_format_options: Optional[dict[InputFormat, PipelineOptions]] = None,
) -> ConversionResult:
valid = (
self.allowed_formats is not None and in_doc.format in self.allowed_formats
)
if valid:
conv_res = self._execute_pipeline(in_doc, raises_on_error=raises_on_error)
conv_res = self._execute_pipeline(
in_doc,
raises_on_error=raises_on_error,
override_format_options=override_format_options,
)
else:
error_message = f"File format not allowed: {in_doc.file}"
if raises_on_error:
Expand All @@ -529,12 +610,53 @@ def _process_document(
return conv_res

def _execute_pipeline(
self, in_doc: InputDocument, raises_on_error: bool
self,
in_doc: InputDocument,
raises_on_error: bool,
override_format_options: Optional[dict[InputFormat, PipelineOptions]] = None,
) -> ConversionResult:
if in_doc.valid:
pipeline = self._get_pipeline(in_doc.format)

# Look up override options for this document's format
override_options = None
if override_format_options is not None:
override_options = override_format_options.get(in_doc.format)

# If override options provided, check compatibility and handle accordingly
if override_options is not None and pipeline is not None:
is_compatible = self._check_options_compatibility(
pipeline.pipeline_options, override_options
)

if not is_compatible:
error_message = (
"Pipeline override options are incompatible with the "
"initialized pipeline. Overrides may only change do_* "
"flags from True to False while keeping all non-do_* "
"fields unchanged."
)
if raises_on_error:
raise ConversionError(error_message)

return ConversionResult(
input=in_doc,
status=ConversionStatus.FAILURE,
errors=[
ErrorItem(
component_type=DoclingComponentType.USER_INPUT,
module_name=self.__class__.__name__,
error_message=error_message,
)
],
)

if pipeline is not None:
conv_res = pipeline.execute(in_doc, raises_on_error=raises_on_error)
conv_res = pipeline.execute(
in_doc,
raises_on_error=raises_on_error,
override_options=override_options,
)
else:
if raises_on_error:
raise ConversionError(
Expand Down
Loading