fix(ocr): annotate OCRResponse.tables/keyValuePairs as Any so the model builds

OCRResponse declares a field literally named `object`. That name shadows the
builtin inside the class namespace, and pydantic resolves the deferred
annotations against that namespace — so `list[dict[str, object]]` on `tables`
and `keyValuePairs` resolves to the string "ocr" and is treated as an
unresolved forward reference. Constructing the model then raises:

  PydanticUserError: `OCRResponse` is not fully defined; you should define
  `ocr`, then call `OCRResponse.model_rebuild()`.

Annotating both fields `Any` breaks the collision without renaming `object`,
which is part of the response contract (it mirrors Mistral's OCR payload).
The comment records why, so a later cleanup does not put `object` back.

Reproduced on pydantic 2.13.4 / python 3.14. Adds a regression test that
simply constructs the model, with and without the two fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mihidum Hettiyahandi 2026-08-21 09:31:32 +10:00
parent fde307539e
commit 55b02a28f4
2 changed files with 38 additions and 2 deletions

View file

@ -92,8 +92,12 @@ class OCRResponse(LiteLLMPydanticObjectBase):
document_annotation: Any | None = None
usage_info: OCRUsageInfo | None = None
content: str | None = None
tables: list[dict[str, object]] | None = None
keyValuePairs: list[dict[str, object]] | None = None
# `Any`, not `object`: the `object` field below shadows the builtin in this
# class namespace, and pydantic resolves the deferred annotations against
# that namespace — so `dict[str, object]` resolves to the string "ocr" and
# the model can never be built.
tables: list[dict[str, Any]] | None = None
keyValuePairs: list[dict[str, Any]] | None = None
object: str = "ocr"
model_config = {"extra": "allow"}

View file

@ -0,0 +1,32 @@
"""`OCRResponse` must be constructible.
The model declares a field literally named `object`, which shadows the builtin
inside the class namespace. Pydantic resolves the deferred annotations against
that namespace, so any *other* field annotated with the builtin `object`
resolves to the string "ocr" and pydantic treats it as an unresolved forward
reference — the model then cannot be built at all.
"""
from litellm.llms.base_llm.ocr.transformation import OCRPage, OCRResponse
def test_ocr_response_can_be_constructed():
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# hello")],
model="mistral-ocr-latest",
)
assert response.object == "ocr"
assert response.pages[0].markdown == "# hello"
def test_ocr_response_accepts_tables_and_key_value_pairs():
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# hello")],
model="mistral-ocr-latest",
tables=[{"rows": [["a", "b"]]}],
keyValuePairs=[{"key": "invoice_no", "value": "42"}],
)
assert response.tables == [{"rows": [["a", "b"]]}]
assert response.keyValuePairs == [{"key": "invoice_no", "value": "42"}]