Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion docs/docs/extraction/prerequisites-support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ When you call [NVIDIA-hosted NIMs](deployment-options.md#when-to-use-nvidia-host

Local Hugging Face inference defaults to `nvidia/NVIDIA-Nemotron-Parse-v1.2`. Set `nemotron_parse_model="nvidia/NVIDIA-Nemotron-Parse-2.0"` to use Parse 2.0 locally.

To use hosted Build, set `nemotron_parse_invoke_url` to the Build chat-completions URL and set `method="nemotron_parse"`. You can normally omit `nemotron_parse_model` so the library selects the model automatically. If you set `nemotron_parse_model` explicitly, it must match the endpoint contract. Mixed Build and self-hosted endpoint lists require an explicit model.
To use hosted Build, set `nemotron_parse_invoke_url` to the Build chat-completions URL and set `method="nemotron_parse"`. You can normally omit `nemotron_parse_model` so the library selects the model automatically. If you set `nemotron_parse_model` explicitly, it must match the endpoint contract.

Each endpoint list must contain only hosted Build endpoints or only compatible self-hosted endpoints. The library rejects a list that mixes hosted Build and self-hosted endpoints because one extraction workflow uses one model ID and request contract. Setting `nemotron_parse_model` explicitly does not make a mixed list valid. To use both deployment types, configure separate ingestors or extraction workflows for each endpoint contract.

When the chart manages the Parse NIM, it wires the model that matches the selected v1.2 or v2.0 image. To select Parse v2.0, enable Parse and set `nimOperator.nemotron_parse.image.repository=nvcr.io/nim/nvidia/nemotron-parse-v2.0` and `nimOperator.nemotron_parse.image.tag=2.0.8-variant`. For a direct external v2.0 endpoint, set `nemotron_parse_model="nvidia/nemotron-parse-v2.0"` explicitly with its invoke URL.

Expand Down
4 changes: 3 additions & 1 deletion docs/docs/extraction/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@ HTTP 400: Content cannot be a plain string. The model does not support text inpu

This can occur when you send a versioned self-hosted model (for example `nvidia/nemotron-parse-v1.2` or `nvidia/nemotron-parse-v2.0`) to the NVIDIA-hosted Build endpoint, which expects the image-only `nvidia/nemotron-parse` contract. It can also occur when the selected self-hosted Parse image and configured model use different versions. The library may replace the raw HTTP error with a targeted model/contract mismatch hint.

To use hosted Build, omit `nemotron_parse_model` so the library selects `nvidia/nemotron-parse` automatically, or set `nemotron_parse_model="nvidia/nemotron-parse"` explicitly. Send `nvidia/nemotron-parse-v1.2` or `nvidia/nemotron-parse-v2.0` only to its matching self-hosted chat endpoint. For direct external Parse v2.0 endpoints, set `nemotron_parse_model="nvidia/nemotron-parse-v2.0"` explicitly. For more information, refer to [Nemotron Parse: hosted Build and self-hosted NIM contracts](prerequisites-support-matrix.md#nemotron-parse-hosted-vs-self-hosted).
To use hosted Build, omit `nemotron_parse_model` so the library selects `nvidia/nemotron-parse` automatically, or set `nemotron_parse_model="nvidia/nemotron-parse"` explicitly. Send `nvidia/nemotron-parse-v1.2` or `nvidia/nemotron-parse-v2.0` only to its matching self-hosted chat endpoint. For direct external Parse v2.0 endpoints, set `nemotron_parse_model="nvidia/nemotron-parse-v2.0"` explicitly.

Do not combine hosted Build and self-hosted endpoints in one `nemotron_parse_invoke_url` list. The library rejects this configuration because one workflow cannot send different model IDs and request contracts to individual endpoints. Setting `nemotron_parse_model` does not override this restriction. Use a homogeneous endpoint list, or configure separate ingestors or extraction workflows for hosted Build and self-hosted capacity. For more information, refer to [Nemotron Parse: hosted Build and self-hosted NIM contracts](prerequisites-support-matrix.md#nemotron-parse-hosted-vs-self-hosted).

## Hosted Page Elements NIM image size limits { #hosted-page-elements-nim-image-size-limits }

Expand Down
3 changes: 3 additions & 0 deletions nemo_retriever/src/nemo_retriever/common/params/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
)

from nemo_retriever.common.modality.caption.model_profiles import DEFAULT_LOCAL_CAPTION_MODEL_ID
from nemo_retriever.common.params.utils import validate_nemotron_parse_endpoint_list
from nemo_retriever.common.remote_auth import resolve_remote_api_key

IngestorRunMode = Literal["inprocess", "batch", "service"]
Expand Down Expand Up @@ -581,6 +582,8 @@ def _auto_enable_features(self) -> "ExtractParams":
"`nemotron_parse_invoke_url` and `nemotron_parse_model` require "
"`method='nemotron_parse'`; Parse-specific configuration is otherwise ignored."
)
if self.method == "nemotron_parse":
validate_nemotron_parse_endpoint_list(self.nemotron_parse_invoke_url or self.invoke_url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Whitespace alias bypasses validation

When nemotron_parse_invoke_url contains only whitespace and invoke_url contains mixed endpoints, raw truthiness validates the empty primary value instead of the effective fallback list, so ExtractParams construction succeeds instead of raising the required validation error.

Suggested change
validate_nemotron_parse_endpoint_list(self.nemotron_parse_invoke_url or self.invoke_url)
validate_nemotron_parse_endpoint_list(
self.nemotron_parse_invoke_url
if str(self.nemotron_parse_invoke_url or "").strip()
else self.invoke_url
)

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/common/params/models.py
Line: 586

Comment:
**Whitespace alias bypasses validation**

When `nemotron_parse_invoke_url` contains only whitespace and `invoke_url` contains mixed endpoints, raw truthiness validates the empty primary value instead of the effective fallback list, so `ExtractParams` construction succeeds instead of raising the required validation error.

```suggestion
            validate_nemotron_parse_endpoint_list(
                self.nemotron_parse_invoke_url
                if str(self.nemotron_parse_invoke_url or "").strip()
                else self.invoke_url
            )
```

**Knowledge Base Used:**
- [Operators](https://app.greptile.com/nvidia-public-github/-/custom-context/knowledge-base/nvidia/nemo-retriever/-/docs/operators.md)
- [VDB and Common Infrastructure](https://app.greptile.com/nvidia-public-github/-/custom-context/knowledge-base/nvidia/nemo-retriever/-/docs/vdb-and-common.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

if not self.use_page_elements:
consumers = [("use_table_structure", self.use_table_structure and self.extract_tables)]
enabled = [name for name, on in consumers if on]
Expand Down
18 changes: 18 additions & 0 deletions nemo_retriever/src/nemo_retriever/common/params/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,29 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict
from urllib.parse import urlsplit

if TYPE_CHECKING:
from nemo_retriever.common.params.models import BatchTuningParams


def validate_nemotron_parse_endpoint_list(invoke_url: str | None) -> tuple[str, ...]:
"""Normalize Parse endpoints and reject mixed NVIDIA Build/self-hosted lists."""
Comment on lines +16 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Validator contract lacks documentation

The new public helper does not document its parameter, normalized tuple return value, or mixed-endpoint ValueError, leaving callers without the required public interface contract.

Suggested change
def validate_nemotron_parse_endpoint_list(invoke_url: str | None) -> tuple[str, ...]:
"""Normalize Parse endpoints and reject mixed NVIDIA Build/self-hosted lists."""
def validate_nemotron_parse_endpoint_list(invoke_url: str | None) -> tuple[str, ...]:
"""Normalize and validate a Nemotron Parse endpoint list.
Args:
invoke_url: A comma-separated endpoint list, or ``None``.
Returns:
The normalized, nonempty endpoints.
Raises:
ValueError: If the list mixes NVIDIA Build and self-hosted endpoints.
"""

Rule Used: Public modules, classes, and functions must have d... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemo_retriever/src/nemo_retriever/common/params/utils.py
Line: 16-17

Comment:
**Validator contract lacks documentation**

The new public helper does not document its parameter, normalized tuple return value, or mixed-endpoint `ValueError`, leaving callers without the required public interface contract.

```suggestion
def validate_nemotron_parse_endpoint_list(invoke_url: str | None) -> tuple[str, ...]:
    """Normalize and validate a Nemotron Parse endpoint list.

    Args:
        invoke_url: A comma-separated endpoint list, or ``None``.

    Returns:
        The normalized, nonempty endpoints.

    Raises:
        ValueError: If the list mixes NVIDIA Build and self-hosted endpoints.
    """
```

**Rule Used:** Public modules, classes, and functions must have d... ([source](.greptile))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

invoke_urls = tuple(part.strip() for part in str(invoke_url or "").split(",") if part.strip())
build_endpoints = tuple(
(urlsplit(endpoint).hostname or "").lower() == "integrate.api.nvidia.com" for endpoint in invoke_urls
)
if any(build_endpoints) and not all(build_endpoints):
raise ValueError(
"Nemotron Parse endpoint lists cannot mix NVIDIA Build and self-hosted endpoints. "
"One `nemotron_parse_model` and request contract apply to the entire endpoint list, but NVIDIA Build "
"requires `nvidia/nemotron-parse` with the hosted tool-call contract and self-hosted Parse requires a "
"versioned model with a tagged contract. Configure a homogeneous endpoint list or use separate "
"ingestors for NVIDIA Build and self-hosted Parse."
)
return invoke_urls


def coerce_params[T](params: T | None, model_cls: type[T], kwargs: dict[str, Any]) -> T:
"""Merge *params* and *kwargs* into an instance of *model_cls*.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from nemo_retriever.models.nim.chat_completions import invoke_chat_completions_images
from nemo_retriever.models.nim.nim import NIMClient, invoke_image_inference_batches
from nemo_retriever.common.params import RemoteRetryParams
from nemo_retriever.common.params.utils import validate_nemotron_parse_endpoint_list

try:
from PIL import Image
Expand Down Expand Up @@ -178,18 +179,12 @@ def _resolve_nemotron_parse_contract(
) -> _ResolvedNemotronParseContract:
"""Resolve the internal request/response contract for a chat endpoint."""

invoke_urls = [part.strip() for part in str(invoke_url or "").split(",") if part.strip()]
invoke_urls = validate_nemotron_parse_endpoint_list(invoke_url)
if not invoke_urls:
raise ValueError("Nemotron Parse invoke_url is required.")

build_endpoints = [_is_nvidia_build_endpoint(url) for url in invoke_urls]
explicit_model = str(model_name or "").strip()
if not explicit_model and any(build_endpoints) and not all(build_endpoints):
raise ValueError(
"Nemotron Parse endpoint lists cannot mix NVIDIA Build and self-hosted endpoints "
"unless `nemotron_parse_model` is set explicitly."
)

resolved_model = explicit_model or (
NEMOTRON_PARSE_HOSTED_MODEL if all(build_endpoints) else NEMOTRON_PARSE_REMOTE_DEFAULT_MODEL
)
Expand Down Expand Up @@ -320,6 +315,8 @@ def nemotron_parse_pages(

invoke_url = str(invoke_url or "").strip() or str(kwargs.get("nemotron_parse_invoke_url") or "").strip()
use_remote = bool(invoke_url)
if use_remote:
validate_nemotron_parse_endpoint_list(invoke_url)
if not use_remote and model is None:
raise ValueError("A local `model` is required when `invoke_url` is not provided.")

Expand Down Expand Up @@ -531,6 +528,7 @@ def __init__(
super().__init__(**kwargs)
self._invoke_url = str(nemotron_parse_invoke_url or "").strip() or str(invoke_url or "").strip()
self._nemotron_parse_model = nemotron_parse_model
validate_nemotron_parse_endpoint_list(self._invoke_url)
self._api_key = api_key
self._request_timeout_s = float(request_timeout_s)
self._task_prompt = str(task_prompt)
Expand Down Expand Up @@ -638,6 +636,7 @@ def __init__(
str(nemotron_parse_invoke_url or "").strip() or str(invoke_url or "").strip() or self.DEFAULT_INVOKE_URL
)
self._nemotron_parse_model = nemotron_parse_model
validate_nemotron_parse_endpoint_list(self._invoke_url)
self._api_key = api_key
self._request_timeout_s = float(request_timeout_s)
self._task_prompt = str(task_prompt)
Expand Down
37 changes: 33 additions & 4 deletions nemo_retriever/tests/test_actor_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,15 +585,44 @@ def test_contract_resolution(self, endpoint, model, expected_model, expected_pro
assert contract.model == expected_model
assert contract.profile.value == expected_profile

def test_contract_resolution_rejects_mixed_endpoints_without_model(self):
@pytest.mark.parametrize(
"model",
[None, "nvidia/nemotron-parse", "nvidia/nemotron-parse-v1.2"],
)
def test_contract_resolution_rejects_mixed_endpoints(self, model):
from nemo_retriever.operators.extract.parse.nemotron_parse import _resolve_nemotron_parse_contract

endpoints = "https://integrate.api.nvidia.com/v1/chat/completions," "http://parse:8000/v1/chat/completions"
with pytest.raises(ValueError, match="cannot mix NVIDIA Build and self-hosted"):
_resolve_nemotron_parse_contract(endpoints, None)
_resolve_nemotron_parse_contract(endpoints, model)

def test_remote_actors_reject_mixed_endpoints_before_client_creation(self):
from nemo_retriever.operators.extract.parse.nemotron_parse import (
NemotronParseCPUActor,
NemotronParseGPUActor,
)

contract = _resolve_nemotron_parse_contract(endpoints, "nvidia/nemotron-parse-v1.2")
assert contract.profile.value == "v1_2_tagged"
endpoints = "https://integrate.api.nvidia.com/v1/chat/completions," "http://parse:8000/v1/chat/completions"
with patch("nemo_retriever.operators.extract.parse.nemotron_parse.NIMClient") as client:
for actor_class in (NemotronParseCPUActor, NemotronParseGPUActor):
with pytest.raises(ValueError, match="Configure a homogeneous endpoint list"):
actor_class(
nemotron_parse_invoke_url=endpoints,
nemotron_parse_model="nvidia/nemotron-parse-v1.2",
)

client.assert_not_called()

def test_pages_reject_mixed_endpoints_without_input_rows(self):
from nemo_retriever.operators.extract.parse.nemotron_parse import nemotron_parse_pages

endpoints = "https://integrate.api.nvidia.com/v1/chat/completions," "http://parse:8000/v1/chat/completions"
with pytest.raises(ValueError, match="One `nemotron_parse_model` and request contract"):
nemotron_parse_pages(
pd.DataFrame(),
invoke_url=endpoints,
nemotron_parse_model="nvidia/nemotron-parse-v1.2",
)

def test_forced_v1_2_build_text_rejection_reports_contract_mismatch(self):
from nemo_retriever.operators.extract.parse.nemotron_parse import nemotron_parse_pages
Expand Down
14 changes: 14 additions & 0 deletions nemo_retriever/tests/test_params_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ def test_normal_and_selected_parse_configurations_are_valid(self) -> None:
)
assert params.method == "nemotron_parse"

@pytest.mark.parametrize(
"model",
[None, "nvidia/nemotron-parse", "nvidia/nemotron-parse-v1.2"],
)
def test_mixed_build_and_self_hosted_parse_endpoints_are_rejected(self, model: str | None) -> None:
endpoints = "https://integrate.api.nvidia.com/v1/chat/completions," "http://127.0.0.1:8018/v1/chat/completions"

with pytest.raises(ValidationError, match="cannot mix NVIDIA Build and self-hosted"):
ExtractParams(
method="nemotron_parse",
nemotron_parse_invoke_url=endpoints,
nemotron_parse_model=model,
)

def test_graphic_elements_controls_are_removed(self) -> None:
assert "use_graphic_elements" not in ExtractParams.model_fields
assert "graphic_elements_invoke_url" not in ExtractParams.model_fields
Expand Down
Loading