From 52b9cc1854b5f47ab560952c8a15836efeffa67e Mon Sep 17 00:00:00 2001 From: Zhibo Zhang Date: Sun, 1 Nov 2026 15:29:16 +0800 Subject: [PATCH 1/5] fix(router): gate OCR requests on declared capability Signed-off-by: Zhibo Zhang <24061721@hdu.edu.cn> --- litellm/router_python_core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/litellm/router_python_core.py b/litellm/router_python_core.py index 30b05456987..93be758a69d 100644 --- a/litellm/router_python_core.py +++ b/litellm/router_python_core.py @@ -15164,6 +15164,8 @@ class PythonCore(DeprecatedRouterMembers): deployments=_returned_deployments, ) request_settings: Final[Mapping[str, object]] = request_kwargs if request_kwargs is not None else {} + original_function_name: Final = getattr(request_settings.get("original_function"), "__name__", None) + requires_ocr_capability: Final = original_function_name in {"ocr", "aocr"} requested_params: Final[frozenset[str]] = ( self._pre_call_check_requested_chat_params(request_settings) if request_kwargs is not None and self._host.dynamic().drop_params is False @@ -15176,6 +15178,16 @@ class PythonCore(DeprecatedRouterMembers): _model_info = deployment.get("model_info", {}) max_input_tokens = deployment_max_input_tokens[idx] + if requires_ocr_capability and ( + not isinstance(_model_info, Mapping) or _model_info.get("mode") != "ocr" + ): + invalid_model_indices.add(idx) + verbose_router_logger.debug( + "Skipping deployment %s for OCR request: declared mode=%s", + _litellm_params.get("model"), + _model_info.get("mode") if isinstance(_model_info, Mapping) else None, + ) + continue if ( max_input_tokens is not None and has_countable_input From 371f7e3cf826aa802d76f182d39348aef8594ca4 Mon Sep 17 00:00:00 2001 From: Zhibo Zhang Date: Sun, 1 Nov 2026 15:29:22 +0800 Subject: [PATCH 2/5] types(router): include OCR as a model mode Signed-off-by: Zhibo Zhang <24061721@hdu.edu.cn> --- litellm/types/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/types/router.py b/litellm/types/router.py index 41a986837da..b716ebf0223 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -1050,7 +1050,7 @@ class ModelGroupInfo(LiteLLMBaseModel): input_cost_per_pixel: float | None = None mode: ( str - | Literal["chat", "embedding", "completion", "image_generation", "audio_transcription", "rerank", "moderations"] + | Literal["chat", "embedding", "completion", "image_generation", "audio_transcription", "ocr", "rerank", "moderations"] | None ) = Field(default="chat") tpm: int | None = None From f035d67cbdb2fc1c7f70d2fd3b043705d513b4bf Mon Sep 17 00:00:00 2001 From: Zhibo Zhang Date: Sun, 1 Nov 2026 15:29:28 +0800 Subject: [PATCH 3/5] test(router): cover OCR capability filtering Signed-off-by: Zhibo Zhang <24061721@hdu.edu.cn> --- .../test_router/test_router_ocr_capability.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/unit/test_router/test_router_ocr_capability.py diff --git a/tests/unit/test_router/test_router_ocr_capability.py b/tests/unit/test_router/test_router_ocr_capability.py new file mode 100644 index 00000000000..c54e8a520bf --- /dev/null +++ b/tests/unit/test_router/test_router_ocr_capability.py @@ -0,0 +1,45 @@ +import pytest +from litellm import Router + + +def test_router_ocr_capability_gate_skips_chat_fallbacks() -> None: + router = Router( + model_list=[ + { + "model_name": "document-ocr", + "litellm_params": {"model": "openai/gpt-4o"}, + "model_info": {"id": "ocr", "mode": "ocr"}, + }, + { + "model_name": "document-ocr", + "litellm_params": {"model": "openai/gpt-4o-mini"}, + "model_info": {"id": "chat", "mode": "chat"}, + }, + ], + enable_pre_call_checks=True, + ) + + async def aocr(*args, **kwargs): + return None + + async def chat(*args, **kwargs): + return None + + deployments = router.get_model_list(model_name="document-ocr") + chat_deployments = router._pre_call_checks( + model="document-ocr", + healthy_deployments=deployments, + messages=None, + input=None, + request_kwargs={"original_function": chat}, + ) + assert [deployment["model_info"]["id"] for deployment in chat_deployments] == ["ocr", "chat"] + + ocr_deployments = router._pre_call_checks( + model="document-ocr", + healthy_deployments=deployments, + messages=None, + input=None, + request_kwargs={"original_function": aocr}, + ) + assert [deployment["model_info"]["id"] for deployment in ocr_deployments] == ["ocr"] From 62f4d0c095b34532327f68e1c24e38a129a33659 Mon Sep 17 00:00:00 2001 From: Zhibo Zhang Date: Sun, 1 Nov 2026 15:29:56 +0800 Subject: [PATCH 4/5] style(tests): remove unused router test import Signed-off-by: Zhibo Zhang <24061721@hdu.edu.cn> --- tests/unit/test_router/test_router_ocr_capability.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_router/test_router_ocr_capability.py b/tests/unit/test_router/test_router_ocr_capability.py index c54e8a520bf..f85fe58c466 100644 --- a/tests/unit/test_router/test_router_ocr_capability.py +++ b/tests/unit/test_router/test_router_ocr_capability.py @@ -1,4 +1,3 @@ -import pytest from litellm import Router From 10cbe497fd0047aabb09aa6bc37a10baa169aa6c Mon Sep 17 00:00:00 2001 From: Zhibo Zhang Date: Mon, 2 Nov 2026 19:59:51 +0800 Subject: [PATCH 5/5] test(router): cover sync and async OCR capability routes --- .../test_router/test_router_ocr_capability.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_router/test_router_ocr_capability.py b/tests/unit/test_router/test_router_ocr_capability.py index f85fe58c466..eec4cfa545e 100644 --- a/tests/unit/test_router/test_router_ocr_capability.py +++ b/tests/unit/test_router/test_router_ocr_capability.py @@ -18,6 +18,9 @@ def test_router_ocr_capability_gate_skips_chat_fallbacks() -> None: enable_pre_call_checks=True, ) + async def ocr(*args, **kwargs): + return None + async def aocr(*args, **kwargs): return None @@ -34,11 +37,12 @@ def test_router_ocr_capability_gate_skips_chat_fallbacks() -> None: ) assert [deployment["model_info"]["id"] for deployment in chat_deployments] == ["ocr", "chat"] - ocr_deployments = router._pre_call_checks( - model="document-ocr", - healthy_deployments=deployments, - messages=None, - input=None, - request_kwargs={"original_function": aocr}, - ) - assert [deployment["model_info"]["id"] for deployment in ocr_deployments] == ["ocr"] + for ocr_function in (ocr, aocr): + ocr_deployments = router._pre_call_checks( + model="document-ocr", + healthy_deployments=deployments, + messages=None, + input=None, + request_kwargs={"original_function": ocr_function}, + ) + assert [deployment["model_info"]["id"] for deployment in ocr_deployments] == ["ocr"]