1
1
# What is this?
2
2
## Log success + failure events to Braintrust
3
3
4
- import copy
5
4
import os
6
5
from datetime import datetime
7
6
from typing import Dict , Optional
8
7
9
8
import httpx
10
- from pydantic import BaseModel
11
9
12
10
import litellm
13
11
from litellm import verbose_logger
24
22
25
23
def get_utc_datetime ():
26
24
import datetime as dt
27
- from datetime import datetime
28
25
29
26
if hasattr (dt , "UTC" ):
30
27
return datetime .now (dt .UTC ) # type: ignore
@@ -45,9 +42,9 @@ def __init__(
45
42
"Authorization" : "Bearer " + self .api_key ,
46
43
"Content-Type" : "application/json" ,
47
44
}
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
51
48
self .global_braintrust_http_handler = get_async_httpx_client (
52
49
llm_provider = httpxSpecialProvider .LoggingCallback
53
50
)
@@ -108,43 +105,6 @@ async def get_project_id_async(self, project_name: str) -> str:
108
105
except httpx .HTTPStatusError as e :
109
106
raise Exception (f"Failed to register project: { e .response .text } " )
110
107
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
-
148
108
async def create_default_project_and_experiment (self ):
149
109
project = await self .global_braintrust_http_handler .post (
150
110
f"{ self .api_base } /project" , headers = self .headers , json = {"name" : "litellm" }
@@ -169,7 +129,9 @@ def log_success_event( # noqa: PLR0915
169
129
verbose_logger .debug ("REACHES BRAINTRUST SUCCESS" )
170
130
try :
171
131
litellm_call_id = kwargs .get ("litellm_call_id" )
132
+ standard_logging_object = kwargs .get ("standard_logging_object" , {})
172
133
prompt = {"messages" : kwargs .get ("messages" )}
134
+
173
135
output = None
174
136
choices = []
175
137
if response_obj is not None and (
@@ -192,33 +154,13 @@ def log_success_event( # noqa: PLR0915
192
154
):
193
155
output = response_obj ["data" ]
194
156
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 {}
217
159
218
160
# 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" )
220
162
if project_id is None :
221
- project_name = metadata .get ("project_name" )
163
+ project_name = dynamic_metadata .get ("project_name" )
222
164
project_id = (
223
165
self .get_project_id_sync (project_name ) if project_name else None
224
166
)
@@ -229,8 +171,9 @@ def log_success_event( # noqa: PLR0915
229
171
project_id = self .default_project_id
230
172
231
173
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 ():
234
177
# generate langfuse tags - Default Tags sent to Langfuse from LiteLLM Proxy
235
178
if (
236
179
litellm .langfuse_default_tags is not None
@@ -239,25 +182,12 @@ def log_success_event( # noqa: PLR0915
239
182
):
240
183
tags .append (f"{ key } :{ value } " )
241
184
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
252
189
253
190
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
261
191
262
192
metrics : Optional [dict ] = None
263
193
usage_obj = getattr (response_obj , "usage" , None )
@@ -275,12 +205,12 @@ def log_success_event( # noqa: PLR0915
275
205
}
276
206
277
207
# 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
+
280
210
request_data = {
281
211
"id" : litellm_call_id ,
282
212
"input" : prompt ["messages" ],
283
- "metadata" : clean_metadata ,
213
+ "metadata" : standard_logging_object ,
284
214
"tags" : tags ,
285
215
"span_attributes" : {"name" : span_name , "type" : "llm" },
286
216
}
@@ -312,6 +242,7 @@ async def async_log_success_event( # noqa: PLR0915
312
242
verbose_logger .debug ("REACHES BRAINTRUST SUCCESS" )
313
243
try :
314
244
litellm_call_id = kwargs .get ("litellm_call_id" )
245
+ standard_logging_object = kwargs .get ("standard_logging_object" , {})
315
246
prompt = {"messages" : kwargs .get ("messages" )}
316
247
output = None
317
248
choices = []
@@ -336,32 +267,12 @@ async def async_log_success_event( # noqa: PLR0915
336
267
output = response_obj ["data" ]
337
268
338
269
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 {}
360
271
361
272
# 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" )
363
274
if project_id is None :
364
- project_name = metadata .get ("project_name" )
275
+ project_name = dynamic_metadata .get ("project_name" )
365
276
project_id = (
366
277
await self .get_project_id_async (project_name )
367
278
if project_name
@@ -374,8 +285,9 @@ async def async_log_success_event( # noqa: PLR0915
374
285
project_id = self .default_project_id
375
286
376
287
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 ():
379
291
# generate langfuse tags - Default Tags sent to Langfuse from LiteLLM Proxy
380
292
if (
381
293
litellm .langfuse_default_tags is not None
@@ -384,25 +296,12 @@ async def async_log_success_event( # noqa: PLR0915
384
296
):
385
297
tags .append (f"{ key } :{ value } " )
386
298
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
397
303
398
304
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
406
305
407
306
metrics : Optional [dict ] = None
408
307
usage_obj = getattr (response_obj , "usage" , None )
@@ -430,13 +329,13 @@ async def async_log_success_event( # noqa: PLR0915
430
329
)
431
330
432
331
# 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
+
435
334
request_data = {
436
335
"id" : litellm_call_id ,
437
336
"input" : prompt ["messages" ],
438
337
"output" : output ,
439
- "metadata" : clean_metadata ,
338
+ "metadata" : standard_logging_object ,
440
339
"tags" : tags ,
441
340
"span_attributes" : {"name" : span_name , "type" : "llm" },
442
341
}
0 commit comments