From 9cae6fa43751256bd4958165e84fa032125b100f Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:56:47 -0700 Subject: [PATCH] fix(logging): classify async anthropic_messages and generate_content as async (#33589) --- litellm/google_genai/main.py | 12 +++ litellm/litellm_core_utils/litellm_logging.py | 3 + .../messages/handler.py | 4 + litellm/types/utils.py | 2 + .../test_litellm_logging.py | 95 ++++++++++++++++++- .../llms/azure/test_azure_common_utils.py | 1 + ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 +- 7 files changed, 117 insertions(+), 2 deletions(-) diff --git a/litellm/google_genai/main.py b/litellm/google_genai/main.py index 8e77c562094..3b1e712342f 100644 --- a/litellm/google_genai/main.py +++ b/litellm/google_genai/main.py @@ -17,6 +17,7 @@ from litellm.llms.base_llm.google_genai.transformation import ( ) from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import CallTypes from litellm.utils import ProviderConfigManager, client if TYPE_CHECKING: @@ -39,6 +40,11 @@ base_llm_http_handler = BaseLLMHTTPHandler() ################################################# +def _mark_async_entrypoint(logging_obj: LiteLLMLoggingObj | None, marker: str, is_async: bool) -> None: + if logging_obj is not None: + logging_obj.model_call_details.setdefault("litellm_params", {})[marker] = is_async + + class GenerateContentSetupResult(BaseModel): """Internal Type - Result of setting up a generate content call""" @@ -315,6 +321,8 @@ def generate_content( try: _is_async = kwargs.pop("agenerate_content", False) + _mark_async_entrypoint(kwargs.get("litellm_logging_obj"), CallTypes.agenerate_content.value, _is_async) + # Handle generationConfig parameter from kwargs for backward compatibility if "generationConfig" in kwargs and config is None: config = kwargs.pop("generationConfig") @@ -403,6 +411,8 @@ async def agenerate_content_stream( try: kwargs["agenerate_content_stream"] = True + _mark_async_entrypoint(kwargs.get("litellm_logging_obj"), CallTypes.agenerate_content_stream.value, True) + # Handle generationConfig parameter from kwargs for backward compatibility if "generationConfig" in kwargs and config is None: config = kwargs.pop("generationConfig") @@ -497,6 +507,8 @@ def generate_content_stream( # Remove any async-related flags since this is the sync function _is_async = kwargs.pop("agenerate_content_stream", False) + _mark_async_entrypoint(kwargs.get("litellm_logging_obj"), CallTypes.agenerate_content_stream.value, _is_async) + # Handle generationConfig parameter from kwargs for backward compatibility if "generationConfig" in kwargs and config is None: config = kwargs.pop("generationConfig") diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 9a0b4937fdb..36d17596873 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1531,6 +1531,9 @@ class Logging(LiteLLMLoggingBaseClass): and litellm_params.get(CallTypes.aimage_generation.value, False) is not True and litellm_params.get(CallTypes.atranscription.value, False) is not True and litellm_params.get(CallTypes.allm_passthrough_route.value, False) is not True + and litellm_params.get(CallTypes.aanthropic_messages.value, False) is not True + and litellm_params.get(CallTypes.agenerate_content.value, False) is not True + and litellm_params.get(CallTypes.agenerate_content_stream.value, False) is not True ) def _is_assembled_stream_success(self, result=None) -> bool: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index dd983f0c344..ebee9323766 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -36,6 +36,7 @@ from litellm.types.llms.anthropic_messages.anthropic_response import ( AnthropicMessagesResponse, ) from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import CallTypes from litellm.utils import ProviderConfigManager, client from ..utils import is_reasoning_auto_summary_enabled @@ -463,6 +464,9 @@ def anthropic_messages_handler( "model": original_model, "custom_llm_provider": custom_llm_provider, } + litellm_logging_obj.model_call_details.setdefault("litellm_params", {})[CallTypes.aanthropic_messages.value] = ( + is_async + ) # Check if stream was converted for WebSearch interception # This is set in the async wrapper above when stream=True is converted to stream=False diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 7e372ca3c68..04f1ff68c5d 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -328,6 +328,7 @@ class CallTypes(str, Enum): cancel_batch = "cancel_batch" pass_through = "pass_through_endpoint" anthropic_messages = "anthropic_messages" + aanthropic_messages = "aanthropic_messages" get_assistants = "get_assistants" aget_assistants = "aget_assistants" create_assistants = "create_assistants" @@ -496,6 +497,7 @@ CallTypesLiteral = Literal[ "pass_through_endpoint", "allm_passthrough_route", "anthropic_messages", + "aanthropic_messages", "aretrieve_batch", "retrieve_batch", "generate_content", diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 6875894c1bf..5bffda126fe 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -653,6 +653,80 @@ async def test_logging_result_for_bridge_calls(logging_obj): assert mock_should_run_logging.call_count == 1 +@pytest.mark.asyncio +async def test_anthropic_messages_marks_litellm_params_async(): + """LIT-4447: the async ``anthropic_messages`` entrypoint must plant + ``aanthropic_messages`` in ``litellm_params`` so ``_is_sync_litellm_request`` + classifies the request async and the sync CustomLogger hook does not fire in + addition to the async one, mirroring how ``acompletion`` / ``aresponses`` set + their own async markers.""" + import asyncio + + import litellm + from litellm.integrations.custom_logger import CustomLogger + + captured = {} + logged = asyncio.Event() + + class CaptureLogger(CustomLogger): + async def async_log_success_event(self, kwargs, response_obj, start_time, end_time): + captured["litellm_params"] = kwargs.get("litellm_params", {}) + logged.set() + + logger = CaptureLogger() + logger.log_success_event = MagicMock() + original_callbacks = getattr(litellm, "callbacks", []) + try: + litellm.callbacks = [logger] + await litellm.anthropic_messages( + max_tokens=100, + messages=[{"role": "user", "content": "Hey"}], + model="anthropic/claude-sonnet-4-5", + mock_response="Hello, world!", + ) + await asyncio.wait_for(logged.wait(), timeout=10) + + assert captured["litellm_params"].get("aanthropic_messages") is True + assert LitellmLogging._is_sync_litellm_request(captured["litellm_params"]) is False + logger.log_success_event.assert_not_called() + finally: + litellm.callbacks = original_callbacks + + +@pytest.mark.asyncio +async def test_agenerate_content_marks_litellm_params_async(): + """LIT-4475: the async ``agenerate_content`` entrypoint must plant + ``agenerate_content`` in ``litellm_params`` so ``_is_sync_litellm_request`` + classifies the nested delegated call async, preventing the sync CustomLogger + hook from firing alongside the async one.""" + import time + + import litellm + + logging_obj = LitellmLogging( + model="gemini/gemini-2.0-flash", + messages=[{"role": "user", "content": "hi"}], + stream=False, + call_type="agenerate_content", + start_time=time.time(), + litellm_call_id="agenerate-content-marker-check", + function_id="fn", + ) + try: + await litellm.agenerate_content( + model="gemini/gemini-2.0-flash", + contents=[{"role": "user", "parts": [{"text": "hi"}]}], + mock_response="hello", + litellm_logging_obj=logging_obj, + ) + except Exception: + pass + + litellm_params = logging_obj.model_call_details.get("litellm_params", {}) + assert litellm_params.get("agenerate_content") is True + assert LitellmLogging._is_sync_litellm_request(litellm_params) is False + + @pytest.mark.asyncio async def test_logging_non_streaming_request(): import asyncio @@ -712,7 +786,15 @@ async def test_logging_non_streaming_request(): @pytest.mark.parametrize( - "async_flag", ["acompletion", "aresponses", "allm_passthrough_route"] + "async_flag", + [ + "acompletion", + "aresponses", + "allm_passthrough_route", + "aanthropic_messages", + "agenerate_content", + "agenerate_content_stream", + ], ) def test_success_handler_skips_sync_callbacks_for_async_requests( logging_obj, async_flag @@ -805,6 +887,17 @@ def test_is_sync_litellm_request(): LitellmLogging._is_sync_litellm_request({"allm_passthrough_route": True}) is False ) + assert ( + LitellmLogging._is_sync_litellm_request({"aanthropic_messages": True}) is False + ) + assert LitellmLogging._is_sync_litellm_request({"agenerate_content": True}) is False + assert ( + LitellmLogging._is_sync_litellm_request({"agenerate_content_stream": True}) + is False + ) + assert ( + LitellmLogging._is_sync_litellm_request({"aanthropic_messages": False}) is True + ) def test_get_litellm_params_propagates_allm_passthrough_route(): diff --git a/tests/test_litellm/llms/azure/test_azure_common_utils.py b/tests/test_litellm/llms/azure/test_azure_common_utils.py index 413241adf37..a3280b90fe3 100644 --- a/tests/test_litellm/llms/azure/test_azure_common_utils.py +++ b/tests/test_litellm/llms/azure/test_azure_common_utils.py @@ -426,6 +426,7 @@ def test_select_azure_base_url_called(setup_mocks): "arerank", "arealtime", "anthropic_messages", + "aanthropic_messages", "add_message", "arun_thread_stream", "aresponses", diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 11fc38c7c34..9ad0b8d1101 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -21690,7 +21690,7 @@ export interface components { * CallTypes * @enum {string} */ - CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill"; + CallTypes: "embedding" | "aembedding" | "completion" | "acompletion" | "atext_completion" | "text_completion" | "image_generation" | "aimage_generation" | "image_edit" | "aimage_edit" | "moderation" | "amoderation" | "atranscription" | "transcription" | "aspeech" | "speech" | "rerank" | "arerank" | "search" | "asearch" | "_arealtime" | "_aresponses_websocket" | "create_batch" | "acreate_batch" | "aretrieve_batch" | "retrieve_batch" | "acancel_batch" | "cancel_batch" | "pass_through_endpoint" | "anthropic_messages" | "aanthropic_messages" | "get_assistants" | "aget_assistants" | "create_assistants" | "acreate_assistants" | "delete_assistant" | "adelete_assistant" | "acreate_thread" | "create_thread" | "aget_thread" | "get_thread" | "a_add_message" | "add_message" | "aget_messages" | "get_messages" | "arun_thread" | "run_thread" | "arun_thread_stream" | "run_thread_stream" | "afile_retrieve" | "file_retrieve" | "afile_delete" | "file_delete" | "afile_list" | "file_list" | "acreate_file" | "create_file" | "afile_content" | "file_content" | "create_fine_tuning_job" | "acreate_fine_tuning_job" | "create_video" | "acreate_video" | "avideo_retrieve" | "video_retrieve" | "avideo_content" | "video_content" | "video_remix" | "avideo_remix" | "video_list" | "avideo_list" | "video_retrieve_job" | "avideo_retrieve_job" | "video_delete" | "avideo_delete" | "video_create_character" | "avideo_create_character" | "video_get_character" | "avideo_get_character" | "video_edit" | "avideo_edit" | "video_extension" | "avideo_extension" | "vector_store_file_create" | "avector_store_file_create" | "vector_store_file_list" | "avector_store_file_list" | "vector_store_file_retrieve" | "avector_store_file_retrieve" | "vector_store_file_content" | "avector_store_file_content" | "vector_store_file_update" | "avector_store_file_update" | "vector_store_file_delete" | "avector_store_file_delete" | "vector_store_create" | "avector_store_create" | "vector_store_search" | "avector_store_search" | "create_container" | "acreate_container" | "list_containers" | "alist_containers" | "retrieve_container" | "aretrieve_container" | "delete_container" | "adelete_container" | "list_container_files" | "alist_container_files" | "upload_container_file" | "aupload_container_file" | "create_sandbox" | "acreate_sandbox" | "delete_sandbox" | "adelete_sandbox" | "run_code" | "arun_code" | "code_interpreter_tool" | "acode_interpreter_tool" | "acancel_fine_tuning_job" | "cancel_fine_tuning_job" | "alist_fine_tuning_jobs" | "list_fine_tuning_jobs" | "aretrieve_fine_tuning_job" | "retrieve_fine_tuning_job" | "responses" | "aresponses" | "alist_input_items" | "llm_passthrough_route" | "allm_passthrough_route" | "generate_content" | "agenerate_content" | "generate_content_stream" | "agenerate_content_stream" | "ocr" | "aocr" | "call_mcp_tool" | "list_mcp_tools" | "asend_message" | "send_message" | "acreate_skill"; /** CallbackDelete */ CallbackDelete: { /** Callback Name */