route stuff through dispatch no direct main

This commit is contained in:
Yujong Lee 2026-09-17 11:06:46 -07:00
parent 56ba988b62
commit b170d61b8d
18 changed files with 306 additions and 27 deletions

View file

@ -1405,10 +1405,22 @@ from .images.main import *
from .videos.main import *
from .batch_completion.main import *
from .rerank_api.main import *
from .llms.anthropic.experimental_pass_through.messages.handler import *
from .messages.dispatch import *
from .responses.main import *
from .responses.dispatch import *
from .responses.main import (
acancel_responses,
acompact_responses,
adelete_responses,
aget_responses,
alist_input_items,
aresponses_api_with_mcp,
cancel_responses,
compact_responses,
delete_responses,
get_responses,
list_input_items,
mock_responses_api_response,
)
# Interactions API is available as litellm.interactions module
# Usage: litellm.interactions.create(), litellm.interactions.get(), etc.

View file

@ -13,10 +13,10 @@ This is an __init__.py file to allow the following interface
from collections.abc import AsyncIterator, Coroutine, Iterator
from typing import Any
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
from litellm.messages import (
anthropic_messages as _async_anthropic_messages,
)
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
from litellm.messages import (
anthropic_messages_handler as _sync_anthropic_messages,
)
from litellm.types.llms.anthropic_messages.anthropic_response import (

View file

@ -31,13 +31,15 @@ PythonAcompletion: TypeAlias = Callable[..., Awaitable[ChatResult]]
def _python_completion() -> PythonCompletion:
return cast( # cast-ok: forward the original call shape through the Python @client decorator
PythonCompletion, main.completion
PythonCompletion,
main.completion, # noqa: TID251 # dispatch boundary owns this Python fallback
)
def _python_acompletion() -> PythonAcompletion:
return cast( # cast-ok: forward the original call shape through the Python @client decorator
PythonAcompletion, main.acompletion
PythonAcompletion,
main.acompletion, # noqa: TID251 # dispatch boundary owns this Python fallback
)

View file

@ -40,6 +40,8 @@ from ..utils import is_reasoning_auto_summary_enabled
from .interceptors import get_messages_interceptors
from .utils import AnthropicMessagesRequestUtils, mock_response
__all__ = ("anthropic_messages", "anthropic_messages_handler")
# Providers that are routed directly to the OpenAI Responses API instead of
# going through chat/completions.
_RESPONSES_API_PROVIDERS: Final = frozenset({"openai"})

View file

@ -414,9 +414,7 @@ async def _call_messages_handler(
Using the public function (decorated with @client) ensures logging, retries,
and provider resolution all work correctly, identical to a direct user call.
"""
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
anthropic_messages,
)
from litellm.messages import anthropic_messages
return await anthropic_messages(
model=model,

View file

@ -5968,7 +5968,7 @@ def responses_with_retries(*args, **kwargs):
except Exception as e:
raise Exception(f"tenacity import failed please run `pip install tenacity`. Error{e}")
from litellm.responses.main import responses
from litellm.responses.dispatch import responses
num_retries: Final = kwargs.pop("num_retries", 3)
# reset retries in .responses()
@ -5998,7 +5998,7 @@ async def aresponses_with_retries(*args, **kwargs):
except Exception as e:
raise Exception(f"tenacity import failed please run `pip install tenacity`. Error{e}")
from litellm.responses.main import aresponses
from litellm.responses.dispatch import aresponses
num_retries: Final = kwargs.pop("num_retries", 3)
kwargs["max_retries"] = 0

View file

@ -30,13 +30,15 @@ PythonAmessages: TypeAlias = Callable[..., Awaitable[MessagesResult]]
def _python_messages() -> PythonMessages:
return cast( # cast-ok: forward the original call shape through the legacy handler
PythonMessages, main.anthropic_messages_handler
PythonMessages,
main.anthropic_messages_handler, # noqa: TID251 # dispatch boundary owns this Python fallback
)
def _python_amessages() -> PythonAmessages:
return cast( # cast-ok: forward the original call shape through the Python @client decorator
PythonAmessages, main.anthropic_messages
PythonAmessages,
main.anthropic_messages, # noqa: TID251 # dispatch boundary owns this Python fallback
)

View file

@ -43,10 +43,12 @@ def _public_request(name: str, args: tuple[object, ...], kwargs: Mapping[str, ob
_PYTHON_OCR: Final = cast( # cast-ok: forward the original call shape through the Python @client decorator
Callable[..., OCRResponse | Coroutine[object, object, OCRResponse]], main.ocr
Callable[..., OCRResponse | Coroutine[object, object, OCRResponse]],
main.ocr, # noqa: TID251 # dispatch boundary owns this Python fallback
)
_PYTHON_AOCR: Final = cast( # cast-ok: forward the original call shape through the Python @client decorator
Callable[..., Awaitable[OCRResponse]], main.aocr
Callable[..., Awaitable[OCRResponse]],
main.aocr, # noqa: TID251 # dispatch boundary owns this Python fallback
)

View file

@ -24,13 +24,15 @@ PythonAresponses: TypeAlias = Callable[..., Awaitable[ResponsesResult]]
def _python_responses() -> PythonResponses:
return cast( # cast-ok: forward the original call shape through the Python @client decorator
PythonResponses, main.responses
PythonResponses,
main.responses, # noqa: TID251 # dispatch boundary owns this Python fallback
)
def _python_aresponses() -> PythonAresponses:
return cast( # cast-ok: forward the original call shape through the Python @client decorator
PythonAresponses, main.aresponses
PythonAresponses,
main.aresponses, # noqa: TID251 # dispatch boundary owns this Python fallback
)

View file

@ -390,7 +390,7 @@ def _synthesize_responses_api_response(
async def _call_aresponses(input, model, tools, **kwargs): # pragma: no cover thin wrapper for patching in tests
from litellm.responses.main import aresponses
from litellm.responses.main import aresponses # noqa: TID251 # inner call must not re-enter file-search emulation
return await aresponses(input=input, model=model, tools=tools, **kwargs)

View file

@ -67,6 +67,23 @@ else:
from .streaming_iterator import BaseResponsesAPIStreamingIterator
__all__ = (
"acancel_responses",
"acompact_responses",
"adelete_responses",
"aget_responses",
"alist_input_items",
"aresponses",
"aresponses_api_with_mcp",
"cancel_responses",
"compact_responses",
"delete_responses",
"get_responses",
"list_input_items",
"mock_responses_api_response",
"responses",
)
####### ENVIRONMENT VARIABLES ###################
# Initialize any necessary instances or variables here
base_llm_http_handler = BaseLLMHTTPHandler()

View file

@ -16,7 +16,7 @@ from litellm.proxy._experimental.mcp_server.utils import (
split_server_prefix_from_name,
strip_known_server_prefix,
)
from litellm.responses.main import aresponses
from litellm.responses.main import aresponses # noqa: TID251 # inner call must skip the MCP gateway that invoked it
from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator
from litellm.types.llms.openai import (
ResponseInputParam,

View file

@ -609,7 +609,7 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
"""Create the initial response iterator by making the first LLM call"""
try:
# Import the core aresponses function that doesn't have MCP logic
from litellm.responses.main import aresponses
from litellm.responses.main import aresponses # noqa: TID251 # core call without MCP logic
# Make the initial response API call - but avoid the MCP wrapper
params: Final[dict[str, object]] = self.original_request_params.copy()
@ -773,7 +773,7 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
self.base_iterator = None
return
from litellm.responses.main import aresponses
from litellm.responses.main import aresponses # noqa: TID251 # follow-up call without MCP logic
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
LiteLLM_Proxy_MCP_Handler,
)

View file

@ -57,3 +57,15 @@ max-args = 5
"typing_extensions.TypeGuard".msg = "Same as typing.TypeGuard."
"typing.TypeIs".msg = "Unverified narrowing (the body is trusted). Parse into a concrete type instead."
"typing_extensions.TypeIs".msg = "Same as typing.TypeIs."
# Dispatched public entry points: import them from their dispatch module so every
# supported call path selects Rust or Python in one place. Only the dispatch
# modules and internal recursive calls may reach the Python implementation
# directly, each with a `# noqa: TID251 # <reason>`.
"litellm.responses.main.responses".msg = "Import litellm.responses.dispatch.responses so the call routes through dispatch."
"litellm.responses.main.aresponses".msg = "Import litellm.responses.dispatch.aresponses so the call routes through dispatch."
"litellm.llms.anthropic.experimental_pass_through.messages.handler.anthropic_messages".msg = "Import litellm.messages.anthropic_messages so the call routes through dispatch."
"litellm.llms.anthropic.experimental_pass_through.messages.handler.anthropic_messages_handler".msg = "Import litellm.messages.anthropic_messages_handler so the call routes through dispatch."
"litellm.ocr.main.ocr".msg = "Import litellm.ocr.dispatch.ocr so the call routes through dispatch."
"litellm.ocr.main.aocr".msg = "Import litellm.ocr.dispatch.aocr so the call routes through dispatch."
"litellm.main.completion".msg = "Import litellm.completion so the call routes through dispatch."
"litellm.main.acompletion".msg = "Import litellm.acompletion so the call routes through dispatch."

View file

@ -1,5 +1,5 @@
import inspect
from collections.abc import Callable, Mapping
from collections.abc import Awaitable, Callable, Mapping
from typing import Final, cast # noqa: TID251 # narrows legacy callable signatures for inspect
import pytest
@ -10,9 +10,12 @@ from litellm.chat_completions.dispatch import (
_ADISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
_DISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
)
from litellm.rust_bridge import catalog
from litellm.rust_bridge.bindings import NativeBinding
from litellm.rust_bridge.catalog import Route, Rule
from litellm.rust_bridge.chat_completions.entrypoints import (
NATIVE_ACOMPLETION,
NATIVE_COMPLETION,
LiteLLMChatCompletionsRequest,
NativeAcompletion,
NativeCompletion,
@ -219,3 +222,50 @@ def test_binding_errors_delegate_to_python(args: tuple[object, ...], kwargs: Map
is response
)
assert captured == [(args, kwargs)]
def test_public_completion_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMChatCompletionsRequest]] = []
expected: Final = ModelResponse()
def native(
request: LiteLLMChatCompletionsRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> ModelResponse:
captured.append(request)
return expected
NATIVE_COMPLETION.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_completion: Final = cast(Callable[..., ModelResponse], litellm.completion)
try:
result: Final = public_completion(model="gpt-4o", messages=MESSAGES)
finally:
NATIVE_COMPLETION.reset()
assert result is expected
assert [request.model for request in captured] == ["gpt-4o"]
@pytest.mark.asyncio
async def test_public_acompletion_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMChatCompletionsRequest]] = []
expected: Final = ModelResponse()
async def native(
request: LiteLLMChatCompletionsRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> ModelResponse:
captured.append(request)
return expected
NATIVE_ACOMPLETION.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_acompletion: Final = cast(Callable[..., Awaitable[ModelResponse]], litellm.acompletion)
try:
result: Final = await public_acompletion(model="gpt-4o", messages=MESSAGES)
finally:
NATIVE_ACOMPLETION.reset()
assert result is expected
assert [request.model for request in captured] == ["gpt-4o"]

View file

@ -1,5 +1,5 @@
import inspect
from collections.abc import Callable, Mapping
from collections.abc import Awaitable, Callable, Mapping
from typing import Final, cast # noqa: TID251 # narrows legacy callable signatures for inspect
import pytest
@ -10,10 +10,13 @@ from litellm.messages.dispatch import (
_ADISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
_DISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
)
from litellm.rust_bridge import catalog
from litellm.rust_bridge.bindings import NativeBinding
from litellm.rust_bridge.catalog import Route, Rule, Rules
from litellm.rust_bridge.configuration import Rollout
from litellm.rust_bridge.messages.entrypoints import (
NATIVE_AMESSAGES,
NATIVE_MESSAGES,
LiteLLMMessagesRequest,
NativeAmessages,
NativeMessages,
@ -235,3 +238,50 @@ def test_binding_errors_delegate_to_python(args: tuple[object, ...], kwargs: Map
)
assert result is expected
assert captured == [(args, kwargs)]
def test_anthropic_create_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMMessagesRequest]] = []
expected: Final = response()
def native(
request: LiteLLMMessagesRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> AnthropicMessagesResponse:
captured.append(request)
return expected
NATIVE_MESSAGES.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_create: Final = cast(Callable[..., AnthropicMessagesResponse], litellm.anthropic.create)
try:
result: Final = public_create(max_tokens=16, messages=MESSAGES, model="claude-sonnet-4-5")
finally:
NATIVE_MESSAGES.reset()
assert result is expected
assert [request.model for request in captured] == ["claude-sonnet-4-5"]
@pytest.mark.asyncio
async def test_anthropic_acreate_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMMessagesRequest]] = []
expected: Final = response()
async def native(
request: LiteLLMMessagesRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> AnthropicMessagesResponse:
captured.append(request)
return expected
NATIVE_AMESSAGES.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_acreate: Final = cast(Callable[..., Awaitable[AnthropicMessagesResponse]], litellm.anthropic.acreate)
try:
result: Final = await public_acreate(max_tokens=16, messages=MESSAGES, model="claude-sonnet-4-5")
finally:
NATIVE_AMESSAGES.reset()
assert result is expected
assert [request.model for request in captured] == ["claude-sonnet-4-5"]

View file

@ -1,18 +1,26 @@
from collections.abc import Mapping
from typing import Final
from collections.abc import Awaitable, Callable, Mapping
from typing import Final, cast # noqa: TID251 # narrows legacy callable signatures for inspect
import httpx
import pytest
import litellm
from litellm.llms.base_llm.ocr.transformation import OCRResponse
from litellm.ocr.dispatch import (
_ADISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
_DISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
)
from litellm.rust_bridge import catalog
from litellm.rust_bridge.bindings import NativeBinding
from litellm.rust_bridge.catalog import Route, Rule, Rules
from litellm.rust_bridge.configuration import Rollout
from litellm.rust_bridge.ocr.entrypoints import LiteLLMOcrRequest, NativeAocr, NativeOcr
from litellm.rust_bridge.ocr.entrypoints import (
NATIVE_AOCR,
NATIVE_OCR,
LiteLLMOcrRequest,
NativeAocr,
NativeOcr,
)
PYTHON_RULES: Final[Rules] = (Rule(Route.OCR, Rollout.PYTHON_ONLY),)
RUST_RULES: Final[Rules] = (Rule(Route.OCR, Rollout.RUST_REQUIRED),)
@ -324,3 +332,58 @@ async def test_aocr_parser_errors_before_python_or_native(
native=lambda hook, request, call_args, call_kwargs: hook(request, call_args, call_kwargs),
rules=RUST_RULES,
)
def test_public_ocr_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
document: Final[Mapping[str, object]] = {
"type": "document_url",
"document_url": "https://example.invalid/document.pdf",
}
captured: Final[list[LiteLLMOcrRequest]] = []
expected: Final = response()
def native(
request: LiteLLMOcrRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> OCRResponse:
captured.append(request)
return expected
NATIVE_OCR.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_ocr: Final = cast(Callable[..., OCRResponse], litellm.ocr)
try:
result: Final = public_ocr(model="mistral/mistral-ocr-latest", document=document)
finally:
NATIVE_OCR.reset()
assert result is expected
assert [request.model for request in captured] == ["mistral/mistral-ocr-latest"]
@pytest.mark.asyncio
async def test_public_aocr_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
document: Final[Mapping[str, object]] = {
"type": "document_url",
"document_url": "https://example.invalid/document.pdf",
}
captured: Final[list[LiteLLMOcrRequest]] = []
expected: Final = response()
async def native(
request: LiteLLMOcrRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> OCRResponse:
captured.append(request)
return expected
NATIVE_AOCR.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_aocr: Final = cast(Callable[..., Awaitable[OCRResponse]], litellm.aocr)
try:
result: Final = await public_aocr(model="mistral/mistral-ocr-latest", document=document)
finally:
NATIVE_AOCR.reset()
assert result is expected
assert [request.model for request in captured] == ["mistral/mistral-ocr-latest"]

View file

@ -1,19 +1,23 @@
import inspect
from collections.abc import Callable, Mapping
from collections.abc import Awaitable, Callable, Mapping
from typing import Final, cast # noqa: TID251 # narrows legacy callable signatures for inspect
import pytest
import litellm
from litellm.responses import dispatch as responses_dispatch
from litellm.responses import main as python_responses
from litellm.responses.dispatch import (
_ADISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
_DISPATCH, # pyright: ignore[reportPrivateUsage] # tests configured dispatch
)
from litellm.rust_bridge import catalog
from litellm.rust_bridge.bindings import NativeBinding
from litellm.rust_bridge.catalog import Route, Rule
from litellm.rust_bridge.configuration import Rollout
from litellm.rust_bridge.responses.entrypoints import (
NATIVE_ARESPONSES,
NATIVE_RESPONSES,
LiteLLMResponsesRequest,
NativeAresponses,
NativeResponses,
@ -253,3 +257,66 @@ def test_binding_errors_delegate_unchanged_to_python(
is response
)
assert captured == [(args, kwargs)]
def test_public_responses_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMResponsesRequest]] = []
expected: Final = _response()
def native(
request: LiteLLMResponsesRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> ResponsesAPIResponse:
captured.append(request)
return expected
NATIVE_RESPONSES.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_responses: Final = cast(Callable[..., ResponsesAPIResponse], litellm.responses)
try:
result: Final = public_responses(input=INPUT, model="gpt-4o")
finally:
NATIVE_RESPONSES.reset()
assert result is expected
assert [request.model for request in captured] == ["gpt-4o"]
@pytest.mark.asyncio
async def test_public_aresponses_routes_through_dispatch(monkeypatch: pytest.MonkeyPatch) -> None:
captured: Final[list[LiteLLMResponsesRequest]] = []
expected: Final = _response()
async def native(
request: LiteLLMResponsesRequest,
args: tuple[object, ...],
kwargs: Mapping[str, object],
) -> ResponsesAPIResponse:
captured.append(request)
return expected
NATIVE_ARESPONSES.override(native)
monkeypatch.setattr(catalog, "RULES", RUST_RULES)
public_aresponses: Final = cast(Callable[..., Awaitable[ResponsesAPIResponse]], litellm.aresponses)
try:
result: Final = await public_aresponses(input=INPUT, model="gpt-4o")
finally:
NATIVE_ARESPONSES.reset()
assert result is expected
assert [request.model for request in captured] == ["gpt-4o"]
def test_responses_with_retries_uses_the_dispatch_entrypoint(monkeypatch: pytest.MonkeyPatch) -> None:
calls: Final[list[Mapping[str, object]]] = []
expected: Final = _response()
def dispatch_responses(*args: object, **kwargs: object) -> ResponsesAPIResponse: # kwargs-ok: records call shape
calls.append(kwargs)
return expected
monkeypatch.setattr(responses_dispatch, "responses", dispatch_responses)
retry: Final = cast(Callable[..., ResponsesAPIResponse], litellm.responses_with_retries)
result: Final = retry(input=INPUT, model="gpt-4o", num_retries=1)
assert result is expected
assert calls[0]["num_retries"] == 0
assert calls[0]["max_retries"] == 0