1+ import unittest .mock
2+
3+ import pytest
14from braintrust import Attachment
25from braintrust .integrations .utils import (
36 _camel_to_snake ,
47 _convert_data_url_to_attachment ,
58 _is_supported_metric_value ,
9+ _log_and_end_span ,
10+ _log_error_and_end_span ,
611 _merge_timing_and_usage_metrics ,
712 _parse_openai_usage_metrics ,
813 _prettify_response_params ,
14+ _serialize_response_format ,
15+ _timing_metrics ,
16+ _try_to_dict ,
917)
1018
1119
@@ -27,6 +35,40 @@ def test_is_supported_metric_value_excludes_booleans():
2735 assert not _is_supported_metric_value ("1" )
2836
2937
38+ def test_try_to_dict_uses_to_dict_when_available ():
39+ class ToDictOnly :
40+ __slots__ = ("_payload" ,)
41+
42+ def __init__ (self , payload ):
43+ self ._payload = payload
44+
45+ def to_dict (self ):
46+ return self ._payload
47+
48+ result = _try_to_dict (ToDictOnly ({"tokens" : 3 }))
49+
50+ assert result == {"tokens" : 3 }
51+
52+
53+ def test_try_to_dict_falls_back_to_vars_for_plain_objects ():
54+ class PlainObject :
55+ def __init__ (self ):
56+ self .foo = "bar"
57+ self .count = 2
58+
59+ result = _try_to_dict (PlainObject ())
60+
61+ assert result == {"foo" : "bar" , "count" : 2 }
62+
63+
64+ def test_try_to_dict_returns_original_when_no_conversion_is_possible ():
65+ obj = object ()
66+
67+ result = _try_to_dict (obj )
68+
69+ assert result is obj
70+
71+
3072def test_parse_openai_usage_metrics_handles_nested_token_details ():
3173 usage = {
3274 "prompt_tokens" : 10 ,
@@ -88,6 +130,81 @@ def test_convert_data_url_to_attachment_preserves_invalid_base64():
88130 assert converted == data_url
89131
90132
133+ def test_convert_data_url_to_attachment_preserves_non_data_urls ():
134+ value = "https://example.com/image.png"
135+
136+ converted = _convert_data_url_to_attachment (value )
137+
138+ assert converted == value
139+
140+
141+ def test_serialize_response_format_with_pydantic_basemodel_subclass ():
142+ pydantic = pytest .importorskip ("pydantic" )
143+
144+ class ResponseFormat (pydantic .BaseModel ):
145+ answer : str
146+
147+ serialized = _serialize_response_format (ResponseFormat )
148+
149+ assert serialized ["type" ] == "json_schema"
150+ assert serialized ["json_schema" ]["name" ] == "ResponseFormat"
151+ assert serialized ["json_schema" ]["schema" ]["properties" ]["answer" ]["title" ] == "Answer"
152+
153+
154+ def test_timing_metrics_includes_time_to_first_token_when_present ():
155+ assert _timing_metrics (10.0 , 15.0 , 12.0 ) == {
156+ "start" : 10.0 ,
157+ "end" : 15.0 ,
158+ "duration" : 5.0 ,
159+ "time_to_first_token" : 2.0 ,
160+ }
161+
162+
163+ def test_timing_metrics_omits_time_to_first_token_when_absent ():
164+ assert _timing_metrics (10.0 , 15.0 ) == {
165+ "start" : 10.0 ,
166+ "end" : 15.0 ,
167+ "duration" : 5.0 ,
168+ }
169+
170+
171+ def test_log_and_end_span_logs_populated_event_then_ends ():
172+ span = unittest .mock .Mock ()
173+
174+ _log_and_end_span (
175+ span ,
176+ output = {"answer" : "4" },
177+ metrics = {"tokens" : 2 },
178+ metadata = {"provider" : "test" },
179+ )
180+
181+ span .log .assert_called_once_with (
182+ output = {"answer" : "4" },
183+ metrics = {"tokens" : 2 },
184+ metadata = {"provider" : "test" },
185+ )
186+ span .end .assert_called_once_with ()
187+
188+
189+ def test_log_and_end_span_skips_log_for_empty_event ():
190+ span = unittest .mock .Mock ()
191+
192+ _log_and_end_span (span )
193+
194+ span .log .assert_not_called ()
195+ span .end .assert_called_once_with ()
196+
197+
198+ def test_log_error_and_end_span_logs_error_then_ends ():
199+ span = unittest .mock .Mock ()
200+ error = RuntimeError ("boom" )
201+
202+ _log_error_and_end_span (span , error )
203+
204+ span .log .assert_called_once_with (error = error )
205+ span .end .assert_called_once_with ()
206+
207+
91208def test_merge_timing_and_usage_metrics (monkeypatch ):
92209 monkeypatch .setattr ("braintrust.integrations.utils.time.time" , lambda : 15.0 )
93210
0 commit comments