-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(file_processors): route docling VLM processing through stack inference API #6225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de4c4d4
166e720
f14b4b9
b4492ee
1900e03
234a67b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| # Copyright (c) The OGX Contributors. | ||
| # All rights reserved. | ||
| # | ||
| # This source code is licensed under the terms described in the LICENSE file in | ||
| # the root directory of this source tree. | ||
|
|
||
| import asyncio | ||
| import base64 | ||
| import time | ||
| from io import BytesIO | ||
|
|
||
| from docling.models.inference_engines.vlm.base import ( | ||
| BaseVlmEngine, | ||
| VlmEngineInput, | ||
| VlmEngineOutput, | ||
| ) | ||
|
|
||
| from ogx.log import get_logger | ||
| from ogx_api.inference.models import ( | ||
| OpenAIChatCompletionContentPartImageParam, | ||
| OpenAIChatCompletionContentPartTextParam, | ||
| OpenAIChatCompletionRequestWithExtraBody, | ||
| OpenAIImageURL, | ||
| OpenAIUserMessageParam, | ||
| ) | ||
|
|
||
| log = get_logger(name=__name__, category="providers::file_processors") | ||
|
|
||
|
|
||
| class OgxInferenceVlmEngine(BaseVlmEngine): | ||
| """VLM engine that routes inference through the stack's model-serving API. | ||
|
|
||
| Instead of making HTTP requests to an external endpoint, this engine calls | ||
| the stack's Inference API directly in-process via dependency injection. | ||
| """ | ||
|
|
||
| def __init__(self, inference_api, model: str, event_loop: asyncio.AbstractEventLoop) -> None: | ||
| self.inference_api = inference_api | ||
| self.model = model | ||
| self.event_loop = event_loop | ||
| self._initialized = True | ||
|
|
||
| def initialize(self) -> None: | ||
| pass | ||
|
|
||
| def predict_batch(self, input_batch: list[VlmEngineInput]) -> list[VlmEngineOutput]: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we add a focused unit test for this new VLM path? The existing file-processor suite passes, but it doesn't exercise
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, added tests |
||
| if not input_batch: | ||
| return [] | ||
|
|
||
| log.info( | ||
| "Processing VLM batch through stack inference API", | ||
| model=self.model, | ||
| batch_size=len(input_batch), | ||
| ) | ||
|
|
||
| start_time = time.time() | ||
| outputs = [self._predict_single(inp) for inp in input_batch] | ||
| total_time = time.time() - start_time | ||
|
|
||
| log.info( | ||
| "VLM batch complete", | ||
| batch_size=len(input_batch), | ||
| total_time_s=round(total_time, 2), | ||
| per_image_s=round(total_time / len(input_batch), 2), | ||
| ) | ||
|
|
||
| return outputs | ||
|
|
||
| def _predict_single(self, input_data: VlmEngineInput) -> VlmEngineOutput: | ||
| request_start = time.time() | ||
|
|
||
| try: | ||
| image = input_data.image.copy().convert("RGBA") | ||
| img_io = BytesIO() | ||
| image.save(img_io, "PNG") | ||
| image_base64 = base64.b64encode(img_io.getvalue()).decode("utf-8") | ||
| except Exception: | ||
| log.exception("Failed to encode image for VLM inference") | ||
| return VlmEngineOutput(text="", stop_reason="error") | ||
|
|
||
| message = OpenAIUserMessageParam( | ||
| role="user", | ||
| content=[ | ||
| OpenAIChatCompletionContentPartImageParam( | ||
| image_url=OpenAIImageURL(url=f"data:image/png;base64,{image_base64}"), | ||
| ), | ||
| OpenAIChatCompletionContentPartTextParam(text=input_data.prompt), | ||
| ], | ||
| ) | ||
|
|
||
| try: | ||
| request = OpenAIChatCompletionRequestWithExtraBody( | ||
| model=self.model, | ||
| messages=[message], | ||
| temperature=input_data.temperature, | ||
| max_tokens=input_data.max_new_tokens or None, | ||
| stop=input_data.stop_strings or None, | ||
| stream=False, | ||
| ) | ||
| future = asyncio.run_coroutine_threadsafe( | ||
| self.inference_api.openai_chat_completion(request), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will this handle provider data credentials?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes the engine calls the stack's inference API in-process via the Python object (inference_api.openai_chat_completion()), not over HTTP. Auth credentials (API keys, tokens, etc.) are configured on the inference provider itself and are already resolved by the time the call reaches the provider implementation. This is the same dependency injection pattern used by the responses and file_search providers.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have two modes for credentials, 0. keys that are baked into config (only suggested for single / small team deployments) and 1. user supplied via x-ogx-provider-data (preferred). i expect this works for (0). does it work for (1)? |
||
| self.event_loop, | ||
| ) | ||
| response = future.result(timeout=120) | ||
|
|
||
| generated_text = response.choices[0].message.content or "" | ||
| finish_reason = response.choices[0].finish_reason or "stop" | ||
|
|
||
| stop_reason = "end_of_sequence" | ||
| if finish_reason == "length": | ||
| stop_reason = "length" | ||
| elif finish_reason == "content_filter": | ||
| log.warning("VLM response was filtered due to content safety policy") | ||
| stop_reason = "content_filtered" | ||
|
|
||
| generation_time = time.time() - request_start | ||
|
|
||
| return VlmEngineOutput( | ||
| text=generated_text.strip(), | ||
| stop_reason=stop_reason, | ||
| metadata={ | ||
| "generation_time": generation_time, | ||
| "model": self.model, | ||
| }, | ||
| ) | ||
| except Exception: | ||
| log.exception("Failed to process VLM inference request") | ||
| return VlmEngineOutput(text="", stop_reason="error") | ||
|
|
||
| def cleanup(self) -> None: | ||
| pass | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this fallback description is stale after the latest change.
DoclingFileProcessor._build_converter()now raises whenvlm_modelis configured without an inference provider, so users following this doc would expect a graceful fallback but actually get a startup error. Can we update this generated/provider doc text to match the fail-fast behavior, or restore the fallback if that is still intended?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, updated the comment and to fallback to the standard pipeline when not available