From b04381957e1c9dd865d47983fe1ffb62a51e8d1e Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Mon, 8 Jun 2026 15:45:05 -0700 Subject: [PATCH] perf(anthropic_messages): remove thread hop in aanthropic_messages dispatch The async /messages entry point dispatched anthropic_messages_handler via loop.run_in_executor(None, ...) wrapped in a copied contextvars context. That handler is @client-decorated; with is_async=True its async wrapper returns a coroutine immediately rather than blocking, so the executor only ever ran a function that returned a coroutine which was then awaited on the event loop. The thread hop did no blocking work; it was pure overhead and contended for the default ThreadPoolExecutor that is shared with sync logging callbacks and blocking SDK calls, hurting throughput under concurrency. Dispatch the handler directly on the running loop and await the result. The asyncio.iscoroutine guard is retained because the mock_response shortcut returns a materialized dict regardless of is_async, so a flat await would crash that path. --- .../messages/handler.py | 19 ++++-------- ...erimental_pass_through_messages_handler.py | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index a3ac465c463..8f82f37249a 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -6,8 +6,6 @@ """ import asyncio -import contextvars -from functools import partial from typing import ( Any, AsyncIterator, @@ -300,11 +298,9 @@ async def anthropic_messages( **kwargs, ) - loop = asyncio.get_event_loop() kwargs["is_async"] = True - func = partial( - anthropic_messages_handler, + response = anthropic_messages_handler( max_tokens=max_tokens, messages=messages, model=model, @@ -330,14 +326,11 @@ async def anthropic_messages( _litellm_messages_presanitized=True, **kwargs, ) - ctx = contextvars.copy_context() - func_with_context = partial(ctx.run, func) - init_response = await loop.run_in_executor(None, func_with_context) - - if asyncio.iscoroutine(init_response): - response = await init_response - else: - response = init_response + # ``anthropic_messages_handler`` returns a coroutine on the async backend + # paths but a materialized response on the ``mock_response`` shortcut, so + # only await when there is something to await. + if asyncio.iscoroutine(response): + return await response return response diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index b1e1d789d74..d0efc4ba261 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -1,3 +1,4 @@ +import asyncio import json import os import sys @@ -673,3 +674,33 @@ async def test_async_wrapper_sets_presanitized_and_sanitizes_once(): assert spy.call_count == 1 assert captured["presanitized"] is True assert [b["type"] for b in captured["messages"][0]["content"]] == ["tool_use"] + + +@pytest.mark.asyncio +async def test_aanthropic_messages_does_not_use_thread_executor(): + """The async Anthropic /messages entry must dispatch the handler directly on + the running event loop, never through ``loop.run_in_executor``. + + Mutation control: revert handler.py to + ``await loop.run_in_executor(None, func_with_context)`` and this test must + fail with AssertionError('thread hop fired'). + """ + from litellm.llms.anthropic.experimental_pass_through.messages import handler + + loop = asyncio.get_running_loop() + + def fail_on_thread_hop(*args, **kwargs): + raise AssertionError("thread hop fired") + + with patch.object(loop, "run_in_executor", side_effect=fail_on_thread_hop): + response = await handler.anthropic_messages( + max_tokens=8, + messages=[{"role": "user", "content": "hi"}], + model="anthropic/claude-3-5-haiku-20241022", + custom_llm_provider="anthropic", + api_key="sk-test", + stream=False, + mock_response="pong", + ) + + assert response["content"][0]["text"] == "pong"