From 55b02a28f4715ca80142a755648417cd810ebe18 Mon Sep 17 00:00:00 2001 From: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com> Date: Fri, 21 Aug 2026 09:31:32 +1000 Subject: [PATCH] fix(ocr): annotate OCRResponse.tables/keyValuePairs as Any so the model builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- litellm/llms/base_llm/ocr/transformation.py | 8 +++-- .../ocr/test_ocr_response_model_build.py | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm/ocr/test_ocr_response_model_build.py diff --git a/litellm/llms/base_llm/ocr/transformation.py b/litellm/llms/base_llm/ocr/transformation.py index d1c77186ea8..fff76c9f7b5 100644 --- a/litellm/llms/base_llm/ocr/transformation.py +++ b/litellm/llms/base_llm/ocr/transformation.py @@ -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"} diff --git a/tests/test_litellm/ocr/test_ocr_response_model_build.py b/tests/test_litellm/ocr/test_ocr_response_model_build.py new file mode 100644 index 00000000000..94773595465 --- /dev/null +++ b/tests/test_litellm/ocr/test_ocr_response_model_build.py @@ -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"}]