mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
fix(otel): reject non-string destination callback vars
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5dc107a2f5
commit
37addcfb6f
4 changed files with 56 additions and 6 deletions
|
|
@ -2056,6 +2056,8 @@ class AddTeamCallback(LiteLLMPydanticObjectBase):
|
|||
for key, value in callback_vars.items():
|
||||
if key not in valid_keys:
|
||||
raise ValueError(f"Invalid callback variable: {key}. Must be one of {valid_keys}")
|
||||
if key in (CAPTURE_MESSAGE_CONTENT_VAR, EXCLUDED_SERVICES_VAR) and not isinstance(value, str):
|
||||
raise ValueError(f"{key} must be a string, got {type(value).__name__}")
|
||||
callback_vars[key] = str(value)
|
||||
validate_no_callback_env_reference(key, callback_vars[key], source="key/team callback metadata")
|
||||
if key == "langfuse_environment":
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ from collections.abc import Callable, Mapping, Sequence
|
|||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.types.utils import CAPTURE_MESSAGE_CONTENT_VAR, EXCLUDED_SERVICES_VAR
|
||||
|
||||
_STRING_OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[str, object])
|
||||
_NEWRELIC_CALLBACK: Final = "newrelic"
|
||||
_NEWRELIC_VAR_PREFIX: Final = "newrelic_"
|
||||
_LANGFUSE_OTEL_CALLBACK: Final = "langfuse_otel"
|
||||
|
|
@ -269,13 +272,18 @@ def logging_metadata_config_error(metadata: Mapping[str, object] | None) -> str
|
|||
entries: Final = _object_sequence(metadata.get("logging"))
|
||||
if entries is None:
|
||||
return None
|
||||
entry_error: Final = next(
|
||||
(error for entry in entries if (error := _logging_entry_error(entry)) is not None),
|
||||
None,
|
||||
)
|
||||
if entry_error is not None:
|
||||
return entry_error
|
||||
entry_vars: Final = tuple(_entry_callback_vars(entry) for entry in entries)
|
||||
named_vars: Final = tuple(zip((_entry_callback_name(entry) for entry in entries), entry_vars))
|
||||
return next(
|
||||
(
|
||||
error
|
||||
for error in (
|
||||
*(_logging_entry_error(entry) for entry in entries),
|
||||
*(conflicting_span_scope_error(entry_vars[i], entry_vars[:i]) for i in range(len(entry_vars))),
|
||||
*(_conflicting_entry_setting_error(named_vars[i], named_vars[:i]) for i in range(len(named_vars))),
|
||||
)
|
||||
|
|
@ -330,12 +338,25 @@ def _entry_callback_vars(entry: object) -> Mapping[str, str]:
|
|||
def _logging_entry_error(entry: object) -> str | None:
|
||||
if not isinstance(entry, Mapping):
|
||||
return None
|
||||
callback_name: Final = entry.get("callback_name")
|
||||
if not isinstance(callback_name, str) or not isinstance(entry.get("callback_vars"), Mapping):
|
||||
typed_entry: Final[Mapping[str, object]] = _STRING_OBJECT_MAPPING_ADAPTER.validate_python(entry)
|
||||
callback_name: Final = typed_entry.get("callback_name")
|
||||
callback_vars_raw: Final = typed_entry.get("callback_vars")
|
||||
if not isinstance(callback_name, str) or not isinstance(callback_vars_raw, Mapping):
|
||||
return None
|
||||
callback_type_raw: Final = entry.get("callback_type")
|
||||
callback_vars: Final[Mapping[str, object]] = _STRING_OBJECT_MAPPING_ADAPTER.validate_python(callback_vars_raw)
|
||||
if CAPTURE_MESSAGE_CONTENT_VAR in callback_vars:
|
||||
capture_message_content: Final = callback_vars[CAPTURE_MESSAGE_CONTENT_VAR]
|
||||
if not isinstance(capture_message_content, str):
|
||||
return f"{CAPTURE_MESSAGE_CONTENT_VAR} must be a string, got {type(capture_message_content).__name__}"
|
||||
if EXCLUDED_SERVICES_VAR in callback_vars:
|
||||
excluded_services: Final = callback_vars[EXCLUDED_SERVICES_VAR]
|
||||
if not isinstance(excluded_services, str):
|
||||
return f"{EXCLUDED_SERVICES_VAR} must be a string, got {type(excluded_services).__name__}"
|
||||
callback_type_raw: Final = typed_entry.get("callback_type")
|
||||
return callback_config_error(
|
||||
callback_name, _entry_callback_vars(entry), callback_type_raw if isinstance(callback_type_raw, str) else None
|
||||
callback_name,
|
||||
_entry_callback_vars(typed_entry),
|
||||
callback_type_raw if isinstance(callback_type_raw, str) else None,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -343,6 +343,23 @@ def test_an_unsupported_capture_message_content_is_rejected_on_key_logging_metad
|
|||
assert error is not None and "Invalid capture_message_content" in error
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("otel_v2_on")
|
||||
@pytest.mark.parametrize("callback_var", ["capture_message_content", "excluded_services"])
|
||||
def test_key_logging_metadata_rejects_non_string_destination_callback_vars(callback_var: str) -> None:
|
||||
error: Final = logging_metadata_config_error(
|
||||
{
|
||||
"logging": [
|
||||
{
|
||||
"callback_name": "newrelic",
|
||||
"callback_type": "success_and_failure",
|
||||
"callback_vars": {callback_var: None},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
assert error == f"{callback_var} must be a string, got NoneType"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("otel_v2_on")
|
||||
def test_each_entry_keeps_its_own_capture_message_content() -> None:
|
||||
error: Final = logging_metadata_config_error(
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ redacted audit rows for callback mutations.
|
|||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from typing import Final
|
||||
from typing import Final, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, Request
|
||||
from pydantic import ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm.proxy._types import (
|
||||
|
|
@ -1821,3 +1822,12 @@ def test_add_team_callback_returns_http_422_for_unknown_excluded_services() -> N
|
|||
|
||||
assert response.status_code == 422
|
||||
assert "excluded_services" in response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("callback_var", ["capture_message_content", "excluded_services"])
|
||||
@pytest.mark.parametrize("value", [None, 1])
|
||||
def test_add_team_callback_rejects_non_string_destination_callback_vars(callback_var: str, value: object) -> None:
|
||||
callback_vars: Final = cast("dict[str, str]", {callback_var: value})
|
||||
with pytest.raises(ValidationError) as exc:
|
||||
AddTeamCallback(callback_name="newrelic", callback_vars=callback_vars)
|
||||
assert f"{callback_var} must be a string, got {type(value).__name__}" in str(exc.value)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue