diff --git a/litellm/__init__.py b/litellm/__init__.py index c80720c3677..f11f1479531 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -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. diff --git a/litellm/anthropic_interface/messages/__init__.py b/litellm/anthropic_interface/messages/__init__.py index 2698cff5980..30319104844 100644 --- a/litellm/anthropic_interface/messages/__init__.py +++ b/litellm/anthropic_interface/messages/__init__.py @@ -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 ( diff --git a/litellm/chat_completions/dispatch.py b/litellm/chat_completions/dispatch.py index a8e34943d37..d36c0343988 100644 --- a/litellm/chat_completions/dispatch.py +++ b/litellm/chat_completions/dispatch.py @@ -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 ) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index 9d1e921cce4..87a4801f987 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -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"}) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py index 4a6b65bb2b1..090cd6b0971 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py @@ -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, diff --git a/litellm/main.py b/litellm/main.py index 1c6e47bfb11..0d133b915c6 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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 diff --git a/litellm/messages/dispatch.py b/litellm/messages/dispatch.py index c463999bae9..c75f6564d1b 100644 --- a/litellm/messages/dispatch.py +++ b/litellm/messages/dispatch.py @@ -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 ) diff --git a/litellm/ocr/dispatch.py b/litellm/ocr/dispatch.py index 4d530f82331..80c93273d1e 100644 --- a/litellm/ocr/dispatch.py +++ b/litellm/ocr/dispatch.py @@ -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 ) diff --git a/litellm/responses/dispatch.py b/litellm/responses/dispatch.py index 60ea7ff291a..b2748fca4b6 100644 --- a/litellm/responses/dispatch.py +++ b/litellm/responses/dispatch.py @@ -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 ) diff --git a/litellm/responses/file_search/emulated_handler.py b/litellm/responses/file_search/emulated_handler.py index aacef9c2198..887d1a9ff93 100644 --- a/litellm/responses/file_search/emulated_handler.py +++ b/litellm/responses/file_search/emulated_handler.py @@ -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) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 93bc41f3646..ae0630efddb 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -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() diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index a5021e2f777..93598e1f15a 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -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, diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index ca12b3e7cc3..2a7fdd8464c 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -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, ) diff --git a/ruff-strict.toml b/ruff-strict.toml index ae092bdde7d..b8611886d8b 100644 --- a/ruff-strict.toml +++ b/ruff-strict.toml @@ -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 # `. +"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." diff --git a/tests/test_litellm/chat_completions/test_dispatch.py b/tests/test_litellm/chat_completions/test_dispatch.py index dbd8819650e..d4bfeaf8d70 100644 --- a/tests/test_litellm/chat_completions/test_dispatch.py +++ b/tests/test_litellm/chat_completions/test_dispatch.py @@ -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"] diff --git a/tests/test_litellm/messages/test_dispatch.py b/tests/test_litellm/messages/test_dispatch.py index a7f9f1cef98..2eaf4cd9a50 100644 --- a/tests/test_litellm/messages/test_dispatch.py +++ b/tests/test_litellm/messages/test_dispatch.py @@ -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"] diff --git a/tests/test_litellm/ocr/test_dispatch.py b/tests/test_litellm/ocr/test_dispatch.py index 51a95c73f21..14d3368f869 100644 --- a/tests/test_litellm/ocr/test_dispatch.py +++ b/tests/test_litellm/ocr/test_dispatch.py @@ -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"] diff --git a/tests/test_litellm/responses/test_dispatch.py b/tests/test_litellm/responses/test_dispatch.py index 12c76ead9e1..2990360d550 100644 --- a/tests/test_litellm/responses/test_dispatch.py +++ b/tests/test_litellm/responses/test_dispatch.py @@ -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