fix(braintrust_logging.py): filter metadata before logging

avoid unserializable json
This commit is contained in:
Krrish Dholakia
2025-08-30 16:11:39 -07:00
parent ccb7ce03a3
commit cb5464421f
3 changed files with 100 additions and 15 deletions
+7 -6
View File
@@ -12,6 +12,7 @@ from pydantic import BaseModel
import litellm
from litellm import verbose_logger
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.safe_json_dumps import filter_json_serializable
from litellm.llms.custom_httpx.http_handler import (
HTTPHandler,
get_async_httpx_client,
@@ -45,9 +46,9 @@ class BraintrustLogger(CustomLogger):
"Authorization": "Bearer " + self.api_key,
"Content-Type": "application/json",
}
self._project_id_cache: Dict[
str, str
] = {} # Cache mapping project names to IDs
self._project_id_cache: Dict[str, str] = (
{}
) # Cache mapping project names to IDs
self.global_braintrust_http_handler = get_async_httpx_client(
llm_provider=httpxSpecialProvider.LoggingCallback
)
@@ -276,7 +277,7 @@ class BraintrustLogger(CustomLogger):
# Allow metadata override for span name
span_name = metadata.get("span_name", "Chat Completion")
request_data = {
"id": litellm_call_id,
"input": prompt["messages"],
@@ -431,12 +432,12 @@ class BraintrustLogger(CustomLogger):
# Allow metadata override for span name
span_name = metadata.get("span_name", "Chat Completion")
request_data = {
"id": litellm_call_id,
"input": prompt["messages"],
"output": output,
"metadata": clean_metadata,
"metadata": filter_json_serializable(clean_metadata),
"tags": tags,
"span_attributes": {"name": span_name, "type": "llm"},
}
@@ -1,5 +1,6 @@
import json
from typing import Any, Union
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
@@ -49,3 +50,94 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
safe_data = _serialize(data, set(), 0)
return json.dumps(safe_data, default=str)
def filter_json_serializable(
data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH
) -> Any:
"""
Recursively filter data to only include JSON serializable items.
Non-serializable items are completely skipped (not included in the result).
"""
def _is_json_serializable(obj: Any) -> bool:
"""Test if an object is JSON serializable."""
try:
json.dumps(obj)
return True
except (TypeError, ValueError):
return False
def _filter(obj: Any, seen: set, depth: int) -> Any:
# Check for maximum depth.
if depth > max_depth:
return None
# Base-case: if it is a primitive, test if it's serializable
if isinstance(obj, (str, int, float, bool, type(None))):
return obj if _is_json_serializable(obj) else None
# Check for circular reference.
if id(obj) in seen:
return None
seen.add(id(obj))
try:
if isinstance(obj, dict):
result = {}
for k, v in obj.items():
# Only include keys that are strings and values that are serializable
if isinstance(k, str):
filtered_value = _filter(v, seen, depth + 1)
# Only add the key-value pair if the value is serializable
if filtered_value is not None or v is None:
if _is_json_serializable(filtered_value):
result[k] = filtered_value
seen.remove(id(obj))
return result
elif isinstance(obj, list):
result = []
for item in obj:
filtered_item = _filter(item, seen, depth + 1)
# Only include items that are serializable
if filtered_item is not None or item is None:
if _is_json_serializable(filtered_item):
result.append(filtered_item)
seen.remove(id(obj))
return result
elif isinstance(obj, tuple):
filtered_items = []
for item in obj:
filtered_item = _filter(item, seen, depth + 1)
# Only include items that are serializable
if filtered_item is not None or item is None:
if _is_json_serializable(filtered_item):
filtered_items.append(filtered_item)
seen.remove(id(obj))
return tuple(filtered_items)
elif isinstance(obj, set):
filtered_items = []
for item in obj:
filtered_item = _filter(item, seen, depth + 1)
# Only include items that are serializable
if filtered_item is not None or item is None:
if _is_json_serializable(filtered_item):
filtered_items.append(filtered_item)
seen.remove(id(obj))
return sorted(filtered_items)
else:
# Test if the object is directly serializable
seen.remove(id(obj))
return obj if _is_json_serializable(obj) else None
except Exception:
if id(obj) in seen:
seen.remove(id(obj))
return None
return _filter(data, set(), 0)
+1 -9
View File
@@ -19,12 +19,4 @@ router_settings:
litellm_settings:
callbacks: ["otel"]
cache: true
cache_params:
type: redis
ttl: 600
supported_call_types: ["acompletion", "completion"]
model_group_settings:
forward_client_headers_to_llm_api:
- fake-openai-endpoint
success_callback: ["braintrust"]