Skip to content

Commit a915d89

Browse files
Harshit28jclaude
andcommitted
fix: preserve pass_through call_type during proxy error logging
_handle_logging_proxy_only_error was overwriting call_type from "pass_through_endpoint" to "acompletion" when request_data contained "messages" (e.g. Claude Code CLI requests). This defeated the dedup guard in failure_handler, causing duplicate logs to persist. Skip the call_type overwrite when the logging object already has call_type == pass_through_endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9cd6b58 commit a915d89

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

litellm/proxy/utils.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,20 +1843,26 @@ async def _handle_logging_proxy_only_error(
18431843
)
18441844

18451845
input: Union[list, str, dict] = ""
1846+
_is_pass_through = (
1847+
litellm_logging_obj.call_type == CallTypes.pass_through.value
1848+
)
18461849
if "messages" in request_data and isinstance(
18471850
request_data["messages"], list
18481851
):
18491852
input = request_data["messages"]
18501853
litellm_logging_obj.model_call_details["messages"] = input
1851-
litellm_logging_obj.call_type = CallTypes.acompletion.value
1854+
if not _is_pass_through:
1855+
litellm_logging_obj.call_type = CallTypes.acompletion.value
18521856
elif "prompt" in request_data and isinstance(request_data["prompt"], str):
18531857
input = request_data["prompt"]
18541858
litellm_logging_obj.model_call_details["prompt"] = input
1855-
litellm_logging_obj.call_type = CallTypes.atext_completion.value
1859+
if not _is_pass_through:
1860+
litellm_logging_obj.call_type = CallTypes.atext_completion.value
18561861
elif "input" in request_data and isinstance(request_data["input"], list):
18571862
input = request_data["input"]
18581863
litellm_logging_obj.model_call_details["input"] = input
1859-
litellm_logging_obj.call_type = CallTypes.aembedding.value
1864+
if not _is_pass_through:
1865+
litellm_logging_obj.call_type = CallTypes.aembedding.value
18601866
litellm_logging_obj.pre_call(
18611867
input=input,
18621868
api_key="",

tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,79 @@ def test_non_pass_through_failure_calls_custom_logger_callback():
345345
litellm.failure_callback = original_failure_callback
346346

347347

348+
@pytest.mark.asyncio
349+
async def test_handle_logging_proxy_only_error_preserves_pass_through_call_type():
350+
"""
351+
Test that _handle_logging_proxy_only_error does NOT overwrite call_type
352+
from "pass_through_endpoint" to "acompletion" when request_data contains
353+
"messages". This ensures the dedup guard in failure_handler works even for
354+
Claude Code CLI requests forwarded via pass-through with a chat body.
355+
"""
356+
from unittest.mock import AsyncMock, MagicMock, patch
357+
358+
from litellm.integrations.custom_logger import CustomLogger
359+
from litellm.litellm_core_utils.litellm_logging import Logging
360+
from litellm.proxy._types import UserAPIKeyAuth
361+
362+
# Create a logging_obj with pass_through call_type (as pass-through endpoints do)
363+
logging_obj = Logging(
364+
model="claude-sonnet-4-20250514",
365+
messages=[{"role": "user", "content": "hello"}],
366+
stream=False,
367+
call_type="pass_through_endpoint",
368+
start_time=None,
369+
litellm_call_id="test-call-id",
370+
function_id="test",
371+
)
372+
373+
# request_data with "messages" — simulates a Claude Code CLI chat request
374+
request_data = {
375+
"model": "claude-sonnet-4-20250514",
376+
"messages": [{"role": "user", "content": "hello"}],
377+
"litellm_logging_obj": logging_obj,
378+
"call_type": "pass_through_endpoint",
379+
}
380+
381+
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
382+
383+
# Mock the proxy_logging_obj to get access to _handle_logging_proxy_only_error
384+
from litellm.proxy.utils import ProxyLogging
385+
386+
proxy_logging = ProxyLogging(user_api_key_cache=MagicMock())
387+
388+
# Track what happens to call_type through the flow
389+
original_async_failure = logging_obj.async_failure_handler
390+
call_types_seen = []
391+
392+
async def tracking_async_failure(**kwargs):
393+
call_types_seen.append(("async", logging_obj.call_type))
394+
# Don't actually run handlers — just track
395+
return
396+
397+
logging_obj.async_failure_handler = tracking_async_failure
398+
399+
original_sync_failure = logging_obj.failure_handler
400+
401+
def tracking_sync_failure(*args, **kwargs):
402+
call_types_seen.append(("sync", logging_obj.call_type))
403+
return
404+
405+
logging_obj.failure_handler = tracking_sync_failure
406+
407+
await proxy_logging._handle_logging_proxy_only_error(
408+
request_data=request_data,
409+
user_api_key_dict=mock_user_api_key_dict,
410+
route="/v1/chat/completions",
411+
original_exception=Exception("test error"),
412+
)
413+
414+
# call_type should still be "pass_through_endpoint", NOT "acompletion"
415+
assert logging_obj.call_type == "pass_through_endpoint", (
416+
f"call_type was mutated to '{logging_obj.call_type}', "
417+
"expected 'pass_through_endpoint'"
418+
)
419+
420+
348421
def test_is_langfuse_route():
349422
"""
350423
Test that the is_langfuse_route method correctly identifies Langfuse routes

0 commit comments

Comments
 (0)