mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 02:23:59 +00:00
fix(braintrust_logging.py): filter metadata before logging
avoid unserializable json
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user