This commit is contained in:
Peter Nancarrow 2026-11-06 15:52:25 -05:00 • committed by GitHub
commit 98b4c1446d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 126 additions and 5 deletions

View file

@ -191,11 +191,17 @@ class LoggingCallbackManager:
callback_settings:
custom_callback_name:
callback_type: generic_api
turn_off_message_logging: true
endpoint: https://webhook-test.com/30343bc33591bc5e6dc44217ceae3e0a
headers:
Authorization: Bearer $LITELLM_MASTER_KEY
ramp:
turn_off_message_logging: true
"""
callback_config: Final = litellm.callback_settings.get(callback)
turn_off_message_logging: Final = (
isinstance(callback_config, dict) and callback_config.get("turn_off_message_logging") is True
)
# Check if callback is in callback_settings with callback_type: generic_api
if isinstance(callback_config, dict) and callback_config.get("callback_type") == "generic_api":
@ -228,6 +234,7 @@ class LoggingCallbackManager:
and cached_logger.max_retries == max_retries
and cached_logger.retry_delay == retry_delay
and cached_logger.timeout == timeout
and cached_logger.turn_off_message_logging == turn_off_message_logging
):
return cached_logger
@ -239,6 +246,7 @@ class LoggingCallbackManager:
max_retries=max_retries,
retry_delay=retry_delay,
timeout=timeout,
turn_off_message_logging=turn_off_message_logging,
)
_generic_api_logger_cache[callback] = new_logger
return new_logger
@ -251,12 +259,15 @@ class LoggingCallbackManager:
if is_callback_compatible(callback):
# Check if we already have a cached logger for this callback
cached_logger = _generic_api_logger_cache.get(callback)
if isinstance(cached_logger, GenericAPILogger):
if (
isinstance(cached_logger, GenericAPILogger)
and cached_logger.turn_off_message_logging == turn_off_message_logging
):
return cached_logger
# Create new GenericAPILogger with callback_name parameter
# This will load config from generic_api_compatible_callbacks.json
new_logger = GenericAPILogger(callback_name=callback)
new_logger = GenericAPILogger(callback_name=callback, turn_off_message_logging=turn_off_message_logging)
_generic_api_logger_cache[callback] = new_logger
return new_logger

View file

@ -1,13 +1,21 @@
import os
from copy import deepcopy
from datetime import datetime
from typing import Final
from unittest.mock import AsyncMock, patch
import httpx
import pytest
import respx
from pydantic import TypeAdapter
import litellm
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.generic_api.generic_api_callback import GenericAPILogger
from litellm.integrations.langfuse.langfuse_prompt_management import LangfusePromptManagement
from litellm.integrations.opentelemetry import OpenTelemetry
from litellm.litellm_core_utils.logging_callback_manager import LoggingCallbackManager
from litellm.litellm_core_utils import logging_callback_manager as callback_manager_module
@pytest.fixture
@ -307,9 +315,7 @@ async def test_generic_api_callback_settings_retry_config():
}
try:
result = LoggingCallbackManager.add_custom_callback_generic_api_str(
callback_name
)
result = LoggingCallbackManager.add_custom_callback_generic_api_str(callback_name)
assert isinstance(result, GenericAPILogger)
assert result.endpoint == "https://example.com/api/logs"
@ -344,3 +350,107 @@ def test_duplicate_multiple_loggers_test():
assert langfuse_count == 1, "Should have exactly one LangfusePromptManagement instance"
assert otel_count == 1, "Should have exactly one OpenTelemetry instance"
@pytest.fixture
def generic_api_redaction_state(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(callback_manager_module, "_generic_api_logger_cache", {})
monkeypatch.setattr(litellm, "callback_settings", {})
monkeypatch.setattr(litellm, "turn_off_message_logging", False)
monkeypatch.setattr(litellm, "generic_api_use_v1", False)
monkeypatch.setattr(litellm, "standard_logging_payload_excluded_fields", None)
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
monkeypatch.setenv("RAMP_API_KEY", "test-ramp-key")
async def _assert_generic_api_export(logger: GenericAPILogger, redact: bool) -> None:
payload: Final = {
"messages": [{"role": "user", "content": "private-prompt-canary"}],
"response": "private-response-canary",
"model": "accounting-model",
"prompt_tokens": 20,
"completion_tokens": 5,
"total_tokens": 25,
"response_cost": 0.03,
}
original: Final = deepcopy(payload)
full_logger: Final = CustomLogger()
full_details: Final = {"standard_logging_object": payload}
ramp_details: Final = logger.redact_standard_logging_payload_from_model_call_details(full_details)
now: Final = datetime(2026, 1, 1)
with respx.mock as http:
destination: Final = http.post(logger.endpoint).mock(return_value=httpx.Response(200))
await logger.async_log_success_event(ramp_details, None, now, now)
await logger.flush_queue()
assert destination.call_count == 1
request_body: Final = destination.calls.last.request.content
exported: Final = TypeAdapter(list[dict[str, object]]).validate_json(request_body)
assert len(exported) == 1
assert exported[0]["model"] == payload["model"]
assert exported[0]["prompt_tokens"] == payload["prompt_tokens"]
assert exported[0]["completion_tokens"] == payload["completion_tokens"]
assert exported[0]["total_tokens"] == payload["total_tokens"]
assert exported[0]["response_cost"] == payload["response_cost"]
assert (b"private-prompt-canary" not in request_body) is redact
assert (b"private-response-canary" not in request_body) is redact
assert full_logger.redact_standard_logging_payload_from_model_call_details(full_details) == full_details
assert payload == original
@pytest.mark.asyncio
@pytest.mark.parametrize("callback_name", ["ramp", "accounting_sink"])
@pytest.mark.parametrize("redaction", [None, False, True])
async def test_generic_api_redaction_keeps_original_logging_payload(
callback_name: str,
redaction: bool | None,
generic_api_redaction_state: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
settings: Final = (
{
"callback_type": "generic_api",
"endpoint": "https://example.com/logs",
"headers": {"Content-Type": "application/json"},
"log_format": "json_array",
}
if callback_name == "accounting_sink"
else {}
)
redaction_setting: Final = {} if redaction is None else {"turn_off_message_logging": redaction}
monkeypatch.setattr(litellm, "callback_settings", {callback_name: {**settings, **redaction_setting}})
logger: Final = LoggingCallbackManager.add_custom_callback_generic_api_str(callback_name)
assert isinstance(logger, GenericAPILogger)
await _assert_generic_api_export(logger, redact=redaction is True)
@pytest.mark.asyncio
@pytest.mark.parametrize("callback_name", ["ramp", "accounting_sink"])
@pytest.mark.parametrize("before,after", [(False, True), (True, False), (True, True)])
async def test_generic_api_cached_logger_tracks_redaction_changes(
callback_name: str,
before: bool,
after: bool,
generic_api_redaction_state: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
settings: Final = (
{
"callback_type": "generic_api",
"endpoint": "https://example.com/logs",
"headers": {"Content-Type": "application/json"},
"log_format": "json_array",
}
if callback_name == "accounting_sink"
else {}
)
monkeypatch.setattr(litellm, "callback_settings", {callback_name: {**settings, "turn_off_message_logging": before}})
initial: Final = LoggingCallbackManager.add_custom_callback_generic_api_str(callback_name)
assert isinstance(initial, GenericAPILogger)
await _assert_generic_api_export(initial, redact=before)
monkeypatch.setattr(litellm, "callback_settings", {callback_name: {**settings, "turn_off_message_logging": after}})
updated: Final = LoggingCallbackManager.add_custom_callback_generic_api_str(callback_name)
assert isinstance(updated, GenericAPILogger)
await _assert_generic_api_export(updated, redact=after)
assert (updated is initial) is (before == after)