Skip to content

Commit 6d36219

Browse files
Merge pull request #14122 from BerriAI/litellm_dev_08_30_2025_p1
Braintrust - fix logging when OTEL is enabled + Gemini - add 'thoughtSignature' support via 'thinking_blocks'
2 parents d2c519f + 5d65324 commit 6d36219

File tree

10 files changed

+501
-259
lines changed

10 files changed

+501
-259
lines changed

litellm/integrations/braintrust_logging.py

Lines changed: 33 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
# What is this?
22
## Log success + failure events to Braintrust
33

4-
import copy
54
import os
65
from datetime import datetime
76
from typing import Dict, Optional
87

98
import httpx
10-
from pydantic import BaseModel
119

1210
import litellm
1311
from litellm import verbose_logger
@@ -24,7 +22,6 @@
2422

2523
def get_utc_datetime():
2624
import datetime as dt
27-
from datetime import datetime
2825

2926
if hasattr(dt, "UTC"):
3027
return datetime.now(dt.UTC) # type: ignore
@@ -45,9 +42,9 @@ def __init__(
4542
"Authorization": "Bearer " + self.api_key,
4643
"Content-Type": "application/json",
4744
}
48-
self._project_id_cache: Dict[
49-
str, str
50-
] = {} # Cache mapping project names to IDs
45+
self._project_id_cache: Dict[str, str] = (
46+
{}
47+
) # Cache mapping project names to IDs
5148
self.global_braintrust_http_handler = get_async_httpx_client(
5249
llm_provider=httpxSpecialProvider.LoggingCallback
5350
)
@@ -108,43 +105,6 @@ async def get_project_id_async(self, project_name: str) -> str:
108105
except httpx.HTTPStatusError as e:
109106
raise Exception(f"Failed to register project: {e.response.text}")
110107

111-
@staticmethod
112-
def add_metadata_from_header(litellm_params: dict, metadata: dict) -> dict:
113-
"""
114-
Adds metadata from proxy request headers to Braintrust logging if keys start with "braintrust_"
115-
and overwrites litellm_params.metadata if already included.
116-
117-
For example if you want to append your trace to an existing `trace_id` via header, send
118-
`headers: { ..., langfuse_existing_trace_id: your-existing-trace-id }` via proxy request.
119-
"""
120-
if litellm_params is None:
121-
return metadata
122-
123-
if litellm_params.get("proxy_server_request") is None:
124-
return metadata
125-
126-
if metadata is None:
127-
metadata = {}
128-
129-
proxy_headers = (
130-
litellm_params.get("proxy_server_request", {}).get("headers", {}) or {}
131-
)
132-
133-
for metadata_param_key in proxy_headers:
134-
if metadata_param_key.startswith("braintrust"):
135-
trace_param_key = metadata_param_key.replace("braintrust", "", 1)
136-
if trace_param_key in metadata:
137-
verbose_logger.warning(
138-
f"Overwriting Braintrust `{trace_param_key}` from request header"
139-
)
140-
else:
141-
verbose_logger.debug(
142-
f"Found Braintrust `{trace_param_key}` in request header"
143-
)
144-
metadata[trace_param_key] = proxy_headers.get(metadata_param_key)
145-
146-
return metadata
147-
148108
async def create_default_project_and_experiment(self):
149109
project = await self.global_braintrust_http_handler.post(
150110
f"{self.api_base}/project", headers=self.headers, json={"name": "litellm"}
@@ -169,7 +129,9 @@ def log_success_event( # noqa: PLR0915
169129
verbose_logger.debug("REACHES BRAINTRUST SUCCESS")
170130
try:
171131
litellm_call_id = kwargs.get("litellm_call_id")
132+
standard_logging_object = kwargs.get("standard_logging_object", {})
172133
prompt = {"messages": kwargs.get("messages")}
134+
173135
output = None
174136
choices = []
175137
if response_obj is not None and (
@@ -192,33 +154,13 @@ def log_success_event( # noqa: PLR0915
192154
):
193155
output = response_obj["data"]
194156

195-
litellm_params = kwargs.get("litellm_params", {})
196-
metadata = (
197-
litellm_params.get("metadata", {}) or {}
198-
) # if litellm_params['metadata'] == None
199-
metadata = self.add_metadata_from_header(litellm_params, metadata)
200-
clean_metadata = {}
201-
try:
202-
metadata = copy.deepcopy(
203-
metadata
204-
) # Avoid modifying the original metadata
205-
except Exception:
206-
new_metadata = {}
207-
for key, value in metadata.items():
208-
if (
209-
isinstance(value, list)
210-
or isinstance(value, dict)
211-
or isinstance(value, str)
212-
or isinstance(value, int)
213-
or isinstance(value, float)
214-
):
215-
new_metadata[key] = copy.deepcopy(value)
216-
metadata = new_metadata
157+
litellm_params = kwargs.get("litellm_params", {}) or {}
158+
dynamic_metadata = litellm_params.get("metadata", {}) or {}
217159

218160
# Get project_id from metadata or create default if needed
219-
project_id = metadata.get("project_id")
161+
project_id = dynamic_metadata.get("project_id")
220162
if project_id is None:
221-
project_name = metadata.get("project_name")
163+
project_name = dynamic_metadata.get("project_name")
222164
project_id = (
223165
self.get_project_id_sync(project_name) if project_name else None
224166
)
@@ -229,8 +171,9 @@ def log_success_event( # noqa: PLR0915
229171
project_id = self.default_project_id
230172

231173
tags = []
232-
if isinstance(metadata, dict):
233-
for key, value in metadata.items():
174+
175+
if isinstance(dynamic_metadata, dict):
176+
for key, value in dynamic_metadata.items():
234177
# generate langfuse tags - Default Tags sent to Langfuse from LiteLLM Proxy
235178
if (
236179
litellm.langfuse_default_tags is not None
@@ -239,25 +182,12 @@ def log_success_event( # noqa: PLR0915
239182
):
240183
tags.append(f"{key}:{value}")
241184

242-
# clean litellm metadata before logging
243-
if key in [
244-
"headers",
245-
"endpoint",
246-
"caching_groups",
247-
"previous_models",
248-
]:
249-
continue
250-
else:
251-
clean_metadata[key] = value
185+
if (
186+
isinstance(value, str) and key not in standard_logging_object
187+
): # support logging dynamic metadata to braintrust
188+
standard_logging_object[key] = value
252189

253190
cost = kwargs.get("response_cost", None)
254-
if cost is not None:
255-
clean_metadata["litellm_response_cost"] = cost
256-
257-
# metadata.model is required for braintrust to calculate the "Estimated cost" metric
258-
litellm_model = kwargs.get("model", None)
259-
if litellm_model is not None:
260-
clean_metadata["model"] = litellm_model
261191

262192
metrics: Optional[dict] = None
263193
usage_obj = getattr(response_obj, "usage", None)
@@ -275,12 +205,12 @@ def log_success_event( # noqa: PLR0915
275205
}
276206

277207
# Allow metadata override for span name
278-
span_name = metadata.get("span_name", "Chat Completion")
279-
208+
span_name = dynamic_metadata.get("span_name", "Chat Completion")
209+
280210
request_data = {
281211
"id": litellm_call_id,
282212
"input": prompt["messages"],
283-
"metadata": clean_metadata,
213+
"metadata": standard_logging_object,
284214
"tags": tags,
285215
"span_attributes": {"name": span_name, "type": "llm"},
286216
}
@@ -312,6 +242,7 @@ async def async_log_success_event( # noqa: PLR0915
312242
verbose_logger.debug("REACHES BRAINTRUST SUCCESS")
313243
try:
314244
litellm_call_id = kwargs.get("litellm_call_id")
245+
standard_logging_object = kwargs.get("standard_logging_object", {})
315246
prompt = {"messages": kwargs.get("messages")}
316247
output = None
317248
choices = []
@@ -336,32 +267,12 @@ async def async_log_success_event( # noqa: PLR0915
336267
output = response_obj["data"]
337268

338269
litellm_params = kwargs.get("litellm_params", {})
339-
metadata = (
340-
litellm_params.get("metadata", {}) or {}
341-
) # if litellm_params['metadata'] == None
342-
metadata = self.add_metadata_from_header(litellm_params, metadata)
343-
clean_metadata = {}
344-
new_metadata = {}
345-
for key, value in metadata.items():
346-
if (
347-
isinstance(value, list)
348-
or isinstance(value, str)
349-
or isinstance(value, int)
350-
or isinstance(value, float)
351-
):
352-
new_metadata[key] = value
353-
elif isinstance(value, BaseModel):
354-
new_metadata[key] = value.model_dump_json()
355-
elif isinstance(value, dict):
356-
for k, v in value.items():
357-
if isinstance(v, datetime):
358-
value[k] = v.isoformat()
359-
new_metadata[key] = value
270+
dynamic_metadata = litellm_params.get("metadata", {}) or {}
360271

361272
# Get project_id from metadata or create default if needed
362-
project_id = metadata.get("project_id")
273+
project_id = dynamic_metadata.get("project_id")
363274
if project_id is None:
364-
project_name = metadata.get("project_name")
275+
project_name = dynamic_metadata.get("project_name")
365276
project_id = (
366277
await self.get_project_id_async(project_name)
367278
if project_name
@@ -374,8 +285,9 @@ async def async_log_success_event( # noqa: PLR0915
374285
project_id = self.default_project_id
375286

376287
tags = []
377-
if isinstance(metadata, dict):
378-
for key, value in metadata.items():
288+
289+
if isinstance(dynamic_metadata, dict):
290+
for key, value in dynamic_metadata.items():
379291
# generate langfuse tags - Default Tags sent to Langfuse from LiteLLM Proxy
380292
if (
381293
litellm.langfuse_default_tags is not None
@@ -384,25 +296,12 @@ async def async_log_success_event( # noqa: PLR0915
384296
):
385297
tags.append(f"{key}:{value}")
386298

387-
# clean litellm metadata before logging
388-
if key in [
389-
"headers",
390-
"endpoint",
391-
"caching_groups",
392-
"previous_models",
393-
]:
394-
continue
395-
else:
396-
clean_metadata[key] = value
299+
if (
300+
isinstance(value, str) and key not in standard_logging_object
301+
): # support logging dynamic metadata to braintrust
302+
standard_logging_object[key] = value
397303

398304
cost = kwargs.get("response_cost", None)
399-
if cost is not None:
400-
clean_metadata["litellm_response_cost"] = cost
401-
402-
# metadata.model is required for braintrust to calculate the "Estimated cost" metric
403-
litellm_model = kwargs.get("model", None)
404-
if litellm_model is not None:
405-
clean_metadata["model"] = litellm_model
406305

407306
metrics: Optional[dict] = None
408307
usage_obj = getattr(response_obj, "usage", None)
@@ -430,13 +329,13 @@ async def async_log_success_event( # noqa: PLR0915
430329
)
431330

432331
# Allow metadata override for span name
433-
span_name = metadata.get("span_name", "Chat Completion")
434-
332+
span_name = dynamic_metadata.get("span_name", "Chat Completion")
333+
435334
request_data = {
436335
"id": litellm_call_id,
437336
"input": prompt["messages"],
438337
"output": output,
439-
"metadata": clean_metadata,
338+
"metadata": standard_logging_object,
440339
"tags": tags,
441340
"span_attributes": {"name": span_name, "type": "llm"},
442341
}

litellm/litellm_core_utils/safe_json_dumps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from typing import Any, Union
3+
34
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
45

56

0 commit comments

Comments
 (0)