mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
Co-authored-by: yuneng <yuneng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
175 lines
6.1 KiB
Python
175 lines
6.1 KiB
Python
"""
|
|
Unit tests for ProxyLogging._init_litellm_callbacks.
|
|
|
|
Validates that string callbacks in litellm.callbacks are replaced in-place
|
|
with their initialized instances, preventing duplicate entries (string + instance)
|
|
that caused double-counting of metrics like litellm_proxy_total_requests_metric.
|
|
"""
|
|
|
|
from typing import List, Union
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
|
|
|
|
class FakeCustomLogger(CustomLogger):
|
|
"""A minimal CustomLogger subclass for testing."""
|
|
|
|
pass
|
|
|
|
|
|
class TestInitLitellmCallbacks:
|
|
"""Tests for ProxyLogging._init_litellm_callbacks."""
|
|
|
|
def _make_proxy_logging(self):
|
|
"""Create a ProxyLogging instance with mocked dependencies."""
|
|
from litellm.proxy.utils import ProxyLogging
|
|
|
|
mock_cache = MagicMock()
|
|
proxy_logging = ProxyLogging(user_api_key_cache=mock_cache)
|
|
return proxy_logging
|
|
|
|
@patch(
|
|
"litellm.proxy.utils.ProxyLogging._add_proxy_hooks",
|
|
new_callable=lambda: lambda self, *a, **kw: None,
|
|
)
|
|
def test_should_replace_string_callback_with_instance(self, _mock_hooks):
|
|
"""
|
|
When litellm.callbacks contains a string callback (e.g. "lago"),
|
|
_init_litellm_callbacks should replace the string with the initialized
|
|
CustomLogger instance, not leave both the string and instance in the list.
|
|
"""
|
|
fake_logger = FakeCustomLogger()
|
|
|
|
# Start with a string callback in litellm.callbacks
|
|
litellm.callbacks = ["lago"] # type: ignore
|
|
|
|
proxy_logging = self._make_proxy_logging()
|
|
|
|
with patch(
|
|
"litellm.litellm_core_utils.litellm_logging._init_custom_logger_compatible_class",
|
|
return_value=fake_logger,
|
|
):
|
|
proxy_logging._init_litellm_callbacks(llm_router=None)
|
|
|
|
# The string "lago" should be replaced by the instance, not appended
|
|
string_entries = [c for c in litellm.callbacks if isinstance(c, str)]
|
|
instance_entries = [
|
|
c for c in litellm.callbacks if isinstance(c, FakeCustomLogger)
|
|
]
|
|
|
|
assert (
|
|
len(string_entries) == 0
|
|
), f"String callbacks should have been replaced, but found: {string_entries}"
|
|
assert (
|
|
len(instance_entries) == 1
|
|
), f"Expected exactly one FakeCustomLogger instance, found {len(instance_entries)}"
|
|
assert instance_entries[0] is fake_logger
|
|
|
|
# Clean up
|
|
litellm.callbacks = [] # type: ignore
|
|
|
|
@patch(
|
|
"litellm.proxy.utils.ProxyLogging._add_proxy_hooks",
|
|
new_callable=lambda: lambda self, *a, **kw: None,
|
|
)
|
|
def test_should_not_duplicate_existing_instance_callbacks(self, _mock_hooks):
|
|
"""
|
|
When litellm.callbacks already contains a CustomLogger instance (not a string),
|
|
_init_litellm_callbacks should not create a duplicate.
|
|
"""
|
|
existing_logger = FakeCustomLogger()
|
|
|
|
litellm.callbacks = [existing_logger] # type: ignore
|
|
|
|
proxy_logging = self._make_proxy_logging()
|
|
|
|
proxy_logging._init_litellm_callbacks(llm_router=None)
|
|
|
|
# Count how many FakeCustomLogger instances are in litellm.callbacks
|
|
instance_count = sum(
|
|
1 for c in litellm.callbacks if isinstance(c, FakeCustomLogger)
|
|
)
|
|
assert instance_count == 1, (
|
|
f"Expected exactly 1 FakeCustomLogger instance, found {instance_count}. "
|
|
f"litellm.callbacks = {litellm.callbacks}"
|
|
)
|
|
|
|
# Clean up
|
|
litellm.callbacks = [] # type: ignore
|
|
|
|
@patch(
|
|
"litellm.proxy.utils.ProxyLogging._add_proxy_hooks",
|
|
new_callable=lambda: lambda self, *a, **kw: None,
|
|
)
|
|
def test_should_handle_unrecognized_string_callback(self, _mock_hooks):
|
|
"""
|
|
When _init_custom_logger_compatible_class returns None for a string callback,
|
|
the string should remain in litellm.callbacks (not crash).
|
|
"""
|
|
litellm.callbacks = ["unknown_callback"] # type: ignore
|
|
|
|
proxy_logging = self._make_proxy_logging()
|
|
|
|
with patch(
|
|
"litellm.litellm_core_utils.litellm_logging._init_custom_logger_compatible_class",
|
|
return_value=None,
|
|
):
|
|
proxy_logging._init_litellm_callbacks(llm_router=None)
|
|
|
|
# The unknown string callback should still be there (not replaced, not crashed)
|
|
assert "unknown_callback" in litellm.callbacks
|
|
|
|
# Clean up
|
|
litellm.callbacks = [] # type: ignore
|
|
|
|
@patch(
|
|
"litellm.proxy.utils.ProxyLogging._add_proxy_hooks",
|
|
new_callable=lambda: lambda self, *a, **kw: None,
|
|
)
|
|
def test_should_replace_multiple_string_callbacks(self, _mock_hooks):
|
|
"""
|
|
When litellm.callbacks contains multiple string callbacks,
|
|
each should be replaced with its corresponding initialized instance.
|
|
"""
|
|
fake_logger_a = FakeCustomLogger()
|
|
fake_logger_b = FakeCustomLogger()
|
|
|
|
litellm.callbacks = ["callback_a", "callback_b"] # type: ignore
|
|
|
|
proxy_logging = self._make_proxy_logging()
|
|
|
|
call_count = 0
|
|
|
|
def mock_init_class(callback_name, **kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
return fake_logger_a
|
|
return fake_logger_b
|
|
|
|
with patch(
|
|
"litellm.litellm_core_utils.litellm_logging._init_custom_logger_compatible_class",
|
|
side_effect=mock_init_class,
|
|
):
|
|
proxy_logging._init_litellm_callbacks(llm_router=None)
|
|
|
|
string_entries = [c for c in litellm.callbacks if isinstance(c, str)]
|
|
instance_entries = [
|
|
c for c in litellm.callbacks if isinstance(c, FakeCustomLogger)
|
|
]
|
|
|
|
assert (
|
|
len(string_entries) == 0
|
|
), f"All string callbacks should have been replaced: {string_entries}"
|
|
assert (
|
|
len(instance_entries) == 2
|
|
), f"Expected 2 FakeCustomLogger instances, found {len(instance_entries)}"
|
|
assert instance_entries[0] is fake_logger_a
|
|
assert instance_entries[1] is fake_logger_b
|
|
|
|
# Clean up
|
|
litellm.callbacks = [] # type: ignore
|