braintrust 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {braintrust-0.4.1 → braintrust-0.4.2}/PKG-INFO +1 -1
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/db_fields.py +1 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/framework.py +2 -2
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/logger.py +0 -3
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/otel/__init__.py +24 -15
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_framework.py +25 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_logger.py +34 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_otel.py +118 -26
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_util.py +51 -1
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/util.py +24 -3
- braintrust-0.4.2/src/braintrust/version.py +4 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/litellm.py +43 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_litellm.py +73 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/PKG-INFO +1 -1
- braintrust-0.4.1/src/braintrust/version.py +0 -4
- {braintrust-0.4.1 → braintrust-0.4.2}/README.md +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/setup.cfg +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/setup.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/_generated_types.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/audit.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/aws.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/bt_json.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/__main__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/eval.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/api.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/bump_versions.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/logs.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/redshift.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/install/run_migrations.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/cli/push.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/conftest.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/context.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/contrib/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/contrib/temporal/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/contrib/temporal/test_temporal.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/auth.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/cors.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/dataset.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/eval_hooks.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/schemas.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/server.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/test_cached_login.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/test_lru_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/devserver/test_server_integration.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/framework2.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/functions/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/functions/constants.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/functions/invoke.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/functions/stream.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/generated_types.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/git_fields.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/gitutil.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/graph_util.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/http_headers.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/id_gen.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/merge_row_batch.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/oai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/object.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/otel/context.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/otel/test_distributed_tracing.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/otel/test_otel_bt_integration.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/parameters.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/disk_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/lru_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/prompt_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/test_disk_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/test_lru_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/prompt_cache/test_prompt_cache.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/py.typed +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/queue.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/resource_manager.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/score.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/serializable_data_class.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/span_identifier_v1.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/span_identifier_v2.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/span_identifier_v3.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/span_identifier_v4.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/span_types.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_bt_json.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_framework2.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_helpers.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_id_gen.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_queue.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_score.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_serializable_data_class.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_span_components.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/test_version.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/_anthropic_utils.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/agent.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/function_call.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/model.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/team.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/agno/utils.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/anthropic.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/claude_agent_sdk/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/claude_agent_sdk/_wrapper.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/claude_agent_sdk/test_wrapper.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/dspy.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/google_genai/__init__.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/langchain.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/openai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/pydantic_ai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_agno.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_anthropic.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_dspy.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_google_genai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_oai_attachments.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_openai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_openrouter.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_pydantic_ai_integration.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_pydantic_ai_wrap_openai.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_utils.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/xact_ids.py +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/SOURCES.txt +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/dependency_links.txt +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/entry_points.txt +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/requires.txt +0 -0
- {braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust.egg-info/top_level.txt +0 -0
|
@@ -1559,9 +1559,9 @@ def build_local_summary(
|
|
|
1559
1559
|
scores_by_name = defaultdict(lambda: (0, 0))
|
|
1560
1560
|
for result in results:
|
|
1561
1561
|
for name, score in result.scores.items():
|
|
1562
|
-
|
|
1563
|
-
if curr is None:
|
|
1562
|
+
if score is None:
|
|
1564
1563
|
continue
|
|
1564
|
+
curr = scores_by_name[name]
|
|
1565
1565
|
scores_by_name[name] = (curr[0] + score, curr[1] + 1)
|
|
1566
1566
|
longest_score_name = max(len(name) for name in scores_by_name) if scores_by_name else 0
|
|
1567
1567
|
avg_scores = {
|
|
@@ -3856,9 +3856,6 @@ class SpanImpl(Span):
|
|
|
3856
3856
|
if serializable_partial_record.get("metrics", {}).get("end") is not None:
|
|
3857
3857
|
self._logged_end_time = serializable_partial_record["metrics"]["end"]
|
|
3858
3858
|
|
|
3859
|
-
if len(serializable_partial_record.get("tags", [])) > 0 and self.span_parents:
|
|
3860
|
-
raise Exception("Tags can only be logged to the root span")
|
|
3861
|
-
|
|
3862
3859
|
def compute_record() -> dict[str, Any]:
|
|
3863
3860
|
exporter = _get_exporter()
|
|
3864
3861
|
return dict(
|
|
@@ -90,18 +90,13 @@ class AISpanProcessor:
|
|
|
90
90
|
def _should_keep_filtered_span(self, span):
|
|
91
91
|
"""
|
|
92
92
|
Keep spans if:
|
|
93
|
-
1.
|
|
94
|
-
2.
|
|
95
|
-
3.
|
|
96
|
-
4. Any attribute name starts with those prefixes
|
|
93
|
+
1. Custom filter returns True/False (if provided)
|
|
94
|
+
2. Span name starts with 'gen_ai.', 'braintrust.', 'llm.', 'ai.', or 'traceloop.'
|
|
95
|
+
3. Any attribute name starts with those prefixes
|
|
97
96
|
"""
|
|
98
97
|
if not span:
|
|
99
98
|
return False
|
|
100
99
|
|
|
101
|
-
# Braintrust requires root spans, so always keep them
|
|
102
|
-
if span.parent is None:
|
|
103
|
-
return True
|
|
104
|
-
|
|
105
100
|
# Apply custom filter if provided
|
|
106
101
|
if self._custom_filter:
|
|
107
102
|
custom_result = self._custom_filter(span)
|
|
@@ -384,6 +379,9 @@ def _get_braintrust_parent(object_type, object_id: str | None = None, compute_ar
|
|
|
384
379
|
|
|
385
380
|
return None
|
|
386
381
|
|
|
382
|
+
def is_root_span(span) -> bool:
|
|
383
|
+
"""Returns True if the span is a root span (no parent span)."""
|
|
384
|
+
return getattr(span, "parent", None) is None
|
|
387
385
|
|
|
388
386
|
def context_from_span_export(export_str: str):
|
|
389
387
|
"""
|
|
@@ -522,15 +520,17 @@ def add_span_parent_to_baggage(span, ctx=None):
|
|
|
522
520
|
return add_parent_to_baggage(parent_value, ctx=ctx)
|
|
523
521
|
|
|
524
522
|
|
|
525
|
-
def parent_from_headers(headers: dict[str, str]) -> str | None:
|
|
523
|
+
def parent_from_headers(headers: dict[str, str], propagator=None) -> str | None:
|
|
526
524
|
"""
|
|
527
|
-
Extract a Braintrust-compatible parent string from
|
|
525
|
+
Extract a Braintrust-compatible parent string from trace context headers.
|
|
528
526
|
|
|
529
|
-
This converts OTEL trace context headers
|
|
530
|
-
|
|
527
|
+
This converts OTEL trace context headers into a format that can be passed
|
|
528
|
+
as the 'parent' parameter to Braintrust's start_span() method.
|
|
531
529
|
|
|
532
530
|
Args:
|
|
533
|
-
headers: Dictionary with
|
|
531
|
+
headers: Dictionary with trace context headers (e.g., 'traceparent'/'baggage' for W3C)
|
|
532
|
+
propagator: Optional custom TextMapPropagator. If not provided, uses the
|
|
533
|
+
globally registered propagator (W3C TraceContext by default).
|
|
534
534
|
|
|
535
535
|
Returns:
|
|
536
536
|
Braintrust V4 export string that can be used as parent parameter,
|
|
@@ -545,6 +545,12 @@ def parent_from_headers(headers: dict[str, str]) -> str | None:
|
|
|
545
545
|
>>> parent = parent_from_headers(headers)
|
|
546
546
|
>>> with project.start_span(name="service_c", parent=parent) as span:
|
|
547
547
|
>>> span.log(input="BT span as child of OTEL parent")
|
|
548
|
+
|
|
549
|
+
>>> # Using a custom propagator (e.g., B3 format)
|
|
550
|
+
>>> from opentelemetry.propagators.b3 import B3MultiFormat
|
|
551
|
+
>>> propagator = B3MultiFormat()
|
|
552
|
+
>>> headers = {'X-B3-TraceId': '...', 'X-B3-SpanId': '...', 'baggage': '...'}
|
|
553
|
+
>>> parent = parent_from_headers(headers, propagator=propagator)
|
|
548
554
|
"""
|
|
549
555
|
if not OTEL_AVAILABLE:
|
|
550
556
|
raise ImportError(INSTALL_ERR_MSG)
|
|
@@ -553,8 +559,11 @@ def parent_from_headers(headers: dict[str, str]) -> str | None:
|
|
|
553
559
|
from opentelemetry import baggage, trace
|
|
554
560
|
from opentelemetry.propagate import extract
|
|
555
561
|
|
|
556
|
-
# Extract context from headers using
|
|
557
|
-
|
|
562
|
+
# Extract context from headers using provided propagator or global propagator
|
|
563
|
+
if propagator is not None:
|
|
564
|
+
ctx = propagator.extract(headers)
|
|
565
|
+
else:
|
|
566
|
+
ctx = extract(headers)
|
|
558
567
|
|
|
559
568
|
# Get span from context
|
|
560
569
|
span = trace.get_current_span(ctx)
|
|
@@ -343,6 +343,31 @@ async def test_eval_no_send_logs_true(with_memory_logger, simple_scorer):
|
|
|
343
343
|
assert len(logs) == 0
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
@pytest.mark.asyncio
|
|
347
|
+
async def test_eval_no_send_logs_with_none_score(with_memory_logger):
|
|
348
|
+
"""Test that scorers returning None don't crash local mode."""
|
|
349
|
+
|
|
350
|
+
def sometimes_none_scorer(input, output, expected):
|
|
351
|
+
# Return None for first input, score for second
|
|
352
|
+
if input == "hello":
|
|
353
|
+
return {"name": "conditional", "score": None}
|
|
354
|
+
return {"name": "conditional", "score": 1.0}
|
|
355
|
+
|
|
356
|
+
result = await Eval(
|
|
357
|
+
"test-none-score",
|
|
358
|
+
data=[
|
|
359
|
+
{"input": "hello", "expected": "hello world"},
|
|
360
|
+
{"input": "test", "expected": "test world"},
|
|
361
|
+
],
|
|
362
|
+
task=lambda input_val: input_val + " world",
|
|
363
|
+
scores=[sometimes_none_scorer],
|
|
364
|
+
no_send_logs=True,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Should not crash and should calculate average from non-None scores only
|
|
368
|
+
assert result.summary.scores["conditional"].score == 1.0 # Only the second score counts
|
|
369
|
+
|
|
370
|
+
|
|
346
371
|
@pytest.mark.asyncio
|
|
347
372
|
async def test_hooks_tags_append(with_memory_logger, with_simulate_login, simple_scorer):
|
|
348
373
|
"""Test that hooks.tags can be appended to and logged."""
|
|
@@ -849,6 +849,40 @@ def test_span_link_with_unresolved_experiment(with_simulate_login, with_memory_l
|
|
|
849
849
|
assert link == "https://www.braintrust.dev/error-generating-link?msg=resolve-experiment-id"
|
|
850
850
|
|
|
851
851
|
|
|
852
|
+
def test_experiment_span_link_uses_env_vars_when_logged_out(with_memory_logger):
|
|
853
|
+
"""Verify EXPERIMENT spans use BRAINTRUST_ORG_NAME env var when not logged in."""
|
|
854
|
+
simulate_logout()
|
|
855
|
+
assert_logged_out()
|
|
856
|
+
|
|
857
|
+
keys = ["BRAINTRUST_APP_URL", "BRAINTRUST_ORG_NAME"]
|
|
858
|
+
originals = {k: os.environ.get(k) for k in keys}
|
|
859
|
+
try:
|
|
860
|
+
os.environ["BRAINTRUST_APP_URL"] = "https://test-app.example.com"
|
|
861
|
+
os.environ["BRAINTRUST_ORG_NAME"] = "env-org-name"
|
|
862
|
+
|
|
863
|
+
experiment = braintrust.init(
|
|
864
|
+
project="test-project",
|
|
865
|
+
experiment="test-experiment",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
# Create span with resolved experiment ID
|
|
869
|
+
span = experiment.start_span(name="test-span")
|
|
870
|
+
span.parent_object_id = LazyValue(lambda: "test-exp-id", use_mutex=False)
|
|
871
|
+
span.end()
|
|
872
|
+
|
|
873
|
+
link = span.link()
|
|
874
|
+
|
|
875
|
+
# Should use env var org name and app url
|
|
876
|
+
assert "env-org-name" in link
|
|
877
|
+
assert "test-app.example.com" in link
|
|
878
|
+
assert "test-exp-id" in link
|
|
879
|
+
finally:
|
|
880
|
+
for k, v in originals.items():
|
|
881
|
+
os.environ.pop(k, None)
|
|
882
|
+
if v:
|
|
883
|
+
os.environ[k] = v
|
|
884
|
+
|
|
885
|
+
|
|
852
886
|
def test_permalink_with_valid_span_logged_in(with_simulate_login, with_memory_logger):
|
|
853
887
|
logger = init_logger(
|
|
854
888
|
project="test-project",
|
|
@@ -294,13 +294,12 @@ class TestSpanFiltering:
|
|
|
294
294
|
self.provider.shutdown()
|
|
295
295
|
self.memory_exporter.clear()
|
|
296
296
|
|
|
297
|
-
def
|
|
297
|
+
def test_filters_out_root_spans(self):
|
|
298
298
|
with self.tracer.start_as_current_span("root_operation"):
|
|
299
299
|
pass
|
|
300
300
|
|
|
301
301
|
spans = self.memory_exporter.get_finished_spans()
|
|
302
|
-
assert len(spans) ==
|
|
303
|
-
assert spans[0].name == "root_operation"
|
|
302
|
+
assert len(spans) == 0
|
|
304
303
|
|
|
305
304
|
def test_keeps_gen_ai_spans(self):
|
|
306
305
|
with self.tracer.start_as_current_span("root"):
|
|
@@ -312,7 +311,7 @@ class TestSpanFiltering:
|
|
|
312
311
|
spans = self.memory_exporter.get_finished_spans()
|
|
313
312
|
span_names = [span.name for span in spans]
|
|
314
313
|
|
|
315
|
-
assert "root" in span_names
|
|
314
|
+
assert "root" not in span_names
|
|
316
315
|
assert "gen_ai.completion" in span_names
|
|
317
316
|
assert "regular_operation" not in span_names
|
|
318
317
|
|
|
@@ -329,35 +328,37 @@ class TestSpanFiltering:
|
|
|
329
328
|
assert "braintrust.eval" in span_names
|
|
330
329
|
assert "database_query" not in span_names
|
|
331
330
|
|
|
332
|
-
def
|
|
331
|
+
def test_keeps_traceloop_spans(self):
|
|
333
332
|
with self.tracer.start_as_current_span("root"):
|
|
334
|
-
with self.tracer.start_as_current_span("
|
|
333
|
+
with self.tracer.start_as_current_span("traceloop.agent"):
|
|
334
|
+
pass
|
|
335
|
+
with self.tracer.start_as_current_span("traceloop.workflow.step"):
|
|
335
336
|
pass
|
|
336
337
|
|
|
337
338
|
spans = self.memory_exporter.get_finished_spans()
|
|
338
339
|
span_names = [span.name for span in spans]
|
|
339
|
-
assert "
|
|
340
|
+
assert "root" not in span_names
|
|
341
|
+
assert "traceloop.agent" in span_names
|
|
342
|
+
assert "traceloop.workflow.step" in span_names
|
|
340
343
|
|
|
341
|
-
def
|
|
344
|
+
def test_keeps_llm_spans(self):
|
|
342
345
|
with self.tracer.start_as_current_span("root"):
|
|
343
|
-
with self.tracer.start_as_current_span("
|
|
346
|
+
with self.tracer.start_as_current_span("llm.generate"):
|
|
344
347
|
pass
|
|
345
348
|
|
|
346
349
|
spans = self.memory_exporter.get_finished_spans()
|
|
347
350
|
span_names = [span.name for span in spans]
|
|
348
|
-
assert "
|
|
351
|
+
assert "llm.generate" in span_names
|
|
349
352
|
|
|
350
|
-
def
|
|
353
|
+
def test_keeps_ai_spans(self):
|
|
351
354
|
with self.tracer.start_as_current_span("root"):
|
|
352
|
-
with self.tracer.start_as_current_span("
|
|
353
|
-
pass
|
|
354
|
-
with self.tracer.start_as_current_span("traceloop.workflow.step"):
|
|
355
|
+
with self.tracer.start_as_current_span("ai.model_call"):
|
|
355
356
|
pass
|
|
356
357
|
|
|
357
358
|
spans = self.memory_exporter.get_finished_spans()
|
|
358
359
|
span_names = [span.name for span in spans]
|
|
359
|
-
assert "
|
|
360
|
-
assert "
|
|
360
|
+
assert "root" not in span_names
|
|
361
|
+
assert "ai.model_call" in span_names
|
|
361
362
|
|
|
362
363
|
def test_keeps_spans_with_llm_attributes(self):
|
|
363
364
|
with self.tracer.start_as_current_span("root"):
|
|
@@ -374,7 +375,7 @@ class TestSpanFiltering:
|
|
|
374
375
|
spans = self.memory_exporter.get_finished_spans()
|
|
375
376
|
span_names = [span.name for span in spans]
|
|
376
377
|
|
|
377
|
-
assert "root" in span_names
|
|
378
|
+
assert "root" not in span_names
|
|
378
379
|
assert "some_operation" in span_names # has gen_ai.model attribute
|
|
379
380
|
assert "another_operation" in span_names # has llm.tokens attribute
|
|
380
381
|
assert "traceloop_operation" in span_names # has traceloop.agent_id attribute
|
|
@@ -390,10 +391,7 @@ class TestSpanFiltering:
|
|
|
390
391
|
pass
|
|
391
392
|
|
|
392
393
|
spans = self.memory_exporter.get_finished_spans()
|
|
393
|
-
|
|
394
|
-
# Only root should be kept
|
|
395
|
-
assert len(spans) == 1
|
|
396
|
-
assert spans[0].name == "root"
|
|
394
|
+
assert len(spans) == 0
|
|
397
395
|
|
|
398
396
|
def test_custom_filter_keeps_spans(self):
|
|
399
397
|
def custom_filter(span):
|
|
@@ -422,9 +420,9 @@ class TestSpanFiltering:
|
|
|
422
420
|
spans = memory_exporter.get_finished_spans()
|
|
423
421
|
span_names = [span.name for span in spans]
|
|
424
422
|
|
|
425
|
-
assert "root" in span_names
|
|
426
423
|
assert "custom_keep" in span_names # kept by custom filter
|
|
427
424
|
assert "regular_operation" not in span_names # dropped by default logic
|
|
425
|
+
assert "root" not in span_names
|
|
428
426
|
|
|
429
427
|
def test_custom_filter_drops_spans(self):
|
|
430
428
|
def custom_filter(span):
|
|
@@ -453,9 +451,9 @@ class TestSpanFiltering:
|
|
|
453
451
|
spans = memory_exporter.get_finished_spans()
|
|
454
452
|
span_names = [span.name for span in spans]
|
|
455
453
|
|
|
456
|
-
assert "root" in span_names
|
|
457
454
|
assert "gen_ai.drop_this" not in span_names # dropped by custom filter
|
|
458
455
|
assert "gen_ai.keep_this" in span_names # kept by default LLM logic
|
|
456
|
+
assert "root" not in span_names
|
|
459
457
|
|
|
460
458
|
def test_custom_filter_none_uses_default_logic(self):
|
|
461
459
|
def custom_filter(span):
|
|
@@ -482,7 +480,7 @@ class TestSpanFiltering:
|
|
|
482
480
|
spans = memory_exporter.get_finished_spans()
|
|
483
481
|
span_names = [span.name for span in spans]
|
|
484
482
|
|
|
485
|
-
assert "root" in span_names
|
|
483
|
+
assert "root" not in span_names
|
|
486
484
|
assert "gen_ai.completion" in span_names # kept by default LLM logic
|
|
487
485
|
assert "regular_operation" not in span_names # dropped by default logic
|
|
488
486
|
|
|
@@ -546,11 +544,32 @@ class TestSpanFiltering:
|
|
|
546
544
|
filtered_spans = filtered_spans_exporter.get_finished_spans()
|
|
547
545
|
filtered_span_names = [span.name for span in filtered_spans]
|
|
548
546
|
|
|
549
|
-
assert len(filtered_spans) ==
|
|
550
|
-
assert "user_request" in filtered_span_names # root span
|
|
547
|
+
assert len(filtered_spans) == 2
|
|
548
|
+
assert "user_request" not in filtered_span_names # root span
|
|
551
549
|
assert "gen_ai.completion" in filtered_span_names # LLM name
|
|
552
550
|
assert "response_formatting" in filtered_span_names # LLM attribute
|
|
553
551
|
|
|
552
|
+
def test_custom_filter_is_root_span(self):
|
|
553
|
+
from braintrust.otel import AISpanProcessor, is_root_span
|
|
554
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
555
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
556
|
+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
|
557
|
+
|
|
558
|
+
memory_exporter = InMemorySpanExporter()
|
|
559
|
+
processor = AISpanProcessor(SimpleSpanProcessor(memory_exporter), custom_filter=is_root_span)
|
|
560
|
+
provider = TracerProvider()
|
|
561
|
+
provider.add_span_processor(processor)
|
|
562
|
+
tracer = provider.get_tracer("test-braintrust-root-filter")
|
|
563
|
+
|
|
564
|
+
with tracer.start_as_current_span("root_span"):
|
|
565
|
+
with tracer.start_as_current_span("child_span"):
|
|
566
|
+
pass
|
|
567
|
+
|
|
568
|
+
provider.shutdown()
|
|
569
|
+
spans = memory_exporter.get_finished_spans()
|
|
570
|
+
names = [span.name for span in spans]
|
|
571
|
+
assert "root_span" in names
|
|
572
|
+
assert "child_span" not in names
|
|
554
573
|
|
|
555
574
|
def test_parent_from_headers_invalid_inputs():
|
|
556
575
|
"""Test parent_from_headers with various invalid inputs."""
|
|
@@ -716,3 +735,76 @@ def test_add_span_parent_to_baggage():
|
|
|
716
735
|
# Test with None span (should return None and warn)
|
|
717
736
|
token = add_span_parent_to_baggage(None)
|
|
718
737
|
assert token is None
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_parent_from_headers_with_custom_propagator():
|
|
741
|
+
"""Test parent_from_headers with a custom propagator."""
|
|
742
|
+
if not _check_otel_installed():
|
|
743
|
+
pytest.skip("OpenTelemetry SDK not fully installed, skipping test")
|
|
744
|
+
|
|
745
|
+
from braintrust.otel import parent_from_headers
|
|
746
|
+
from opentelemetry import baggage as otel_baggage
|
|
747
|
+
from opentelemetry import context as otel_context
|
|
748
|
+
from opentelemetry import trace
|
|
749
|
+
from opentelemetry.propagators.textmap import CarrierT, Getter, TextMapPropagator, default_getter
|
|
750
|
+
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
|
|
751
|
+
|
|
752
|
+
class CustomHeaderPropagator(TextMapPropagator):
|
|
753
|
+
"""Custom propagator that reads trace context from X-Custom-* headers."""
|
|
754
|
+
|
|
755
|
+
def extract(
|
|
756
|
+
self,
|
|
757
|
+
carrier: CarrierT,
|
|
758
|
+
context: otel_context.Context | None = None,
|
|
759
|
+
getter: Getter = default_getter,
|
|
760
|
+
) -> otel_context.Context:
|
|
761
|
+
if context is None:
|
|
762
|
+
context = otel_context.get_current()
|
|
763
|
+
|
|
764
|
+
trace_id = getter.get(carrier, "X-Custom-Trace-Id")
|
|
765
|
+
span_id = getter.get(carrier, "X-Custom-Span-Id")
|
|
766
|
+
|
|
767
|
+
if trace_id and span_id:
|
|
768
|
+
trace_id_list = trace_id if isinstance(trace_id, list) else [trace_id]
|
|
769
|
+
span_id_list = span_id if isinstance(span_id, list) else [span_id]
|
|
770
|
+
|
|
771
|
+
span_context = SpanContext(
|
|
772
|
+
trace_id=int(trace_id_list[0], 16),
|
|
773
|
+
span_id=int(span_id_list[0], 16),
|
|
774
|
+
is_remote=True,
|
|
775
|
+
trace_flags=TraceFlags.SAMPLED,
|
|
776
|
+
)
|
|
777
|
+
span = NonRecordingSpan(span_context)
|
|
778
|
+
context = trace.set_span_in_context(span, context)
|
|
779
|
+
|
|
780
|
+
# Also extract baggage from standard baggage header
|
|
781
|
+
baggage_header = getter.get(carrier, "baggage")
|
|
782
|
+
if baggage_header:
|
|
783
|
+
baggage_list = baggage_header if isinstance(baggage_header, list) else [baggage_header]
|
|
784
|
+
for item in baggage_list[0].split(","):
|
|
785
|
+
if "=" in item:
|
|
786
|
+
key, value = item.split("=", 1)
|
|
787
|
+
context = otel_baggage.set_baggage(key.strip(), value.strip(), context)
|
|
788
|
+
|
|
789
|
+
return context
|
|
790
|
+
|
|
791
|
+
def inject(self, carrier, context=None, setter=None):
|
|
792
|
+
pass # Not needed for this test
|
|
793
|
+
|
|
794
|
+
@property
|
|
795
|
+
def fields(self):
|
|
796
|
+
return {"X-Custom-Trace-Id", "X-Custom-Span-Id", "baggage"}
|
|
797
|
+
|
|
798
|
+
propagator = CustomHeaderPropagator()
|
|
799
|
+
|
|
800
|
+
# Custom header format
|
|
801
|
+
headers = {
|
|
802
|
+
"X-Custom-Trace-Id": "4bf92f3577b34da6a3ce929d0e0e4736",
|
|
803
|
+
"X-Custom-Span-Id": "00f067aa0ba902b7",
|
|
804
|
+
"baggage": "braintrust.parent=project_name:test-project",
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
result = parent_from_headers(headers, propagator=propagator)
|
|
808
|
+
assert result is not None
|
|
809
|
+
assert isinstance(result, str)
|
|
810
|
+
assert len(result) > 0
|
|
@@ -3,7 +3,7 @@ from typing import List
|
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
|
-
from .util import LazyValue, mask_api_key
|
|
6
|
+
from .util import LazyValue, mask_api_key, merge_dicts_with_paths
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class TestLazyValue(unittest.TestCase):
|
|
@@ -160,3 +160,53 @@ def test_mask_api_key():
|
|
|
160
160
|
assert mask_api_key("12345") == "12*45"
|
|
161
161
|
for i in ["", "1", "12", "123", "1234"]:
|
|
162
162
|
assert mask_api_key(i) == "*" * len(i)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestTagsSetUnionMerge:
|
|
166
|
+
def test_tags_arrays_are_merged_as_sets_by_default(self):
|
|
167
|
+
a = {"tags": ["a", "b"]}
|
|
168
|
+
b = {"tags": ["b", "c"]}
|
|
169
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
170
|
+
assert set(a["tags"]) == {"a", "b", "c"}
|
|
171
|
+
|
|
172
|
+
def test_tags_merge_deduplicates_values(self):
|
|
173
|
+
a = {"tags": ["a", "b", "c"]}
|
|
174
|
+
b = {"tags": ["a", "b", "c", "d"]}
|
|
175
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
176
|
+
assert set(a["tags"]) == {"a", "b", "c", "d"}
|
|
177
|
+
|
|
178
|
+
def test_tags_merge_works_when_merge_into_has_no_tags(self):
|
|
179
|
+
a = {"other": "data"}
|
|
180
|
+
b = {"tags": ["a", "b"]}
|
|
181
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
182
|
+
assert set(a["tags"]) == {"a", "b"}
|
|
183
|
+
|
|
184
|
+
def test_tags_merge_works_when_merge_from_has_no_tags(self):
|
|
185
|
+
a = {"tags": ["a", "b"]}
|
|
186
|
+
b = {"other": "data"}
|
|
187
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
188
|
+
assert set(a["tags"]) == {"a", "b"}
|
|
189
|
+
|
|
190
|
+
def test_tags_are_replaced_when_included_in_merge_paths(self):
|
|
191
|
+
a = {"tags": ["a", "b"]}
|
|
192
|
+
b = {"tags": ["c", "d"]}
|
|
193
|
+
merge_dicts_with_paths(a, b, (), {("tags",)})
|
|
194
|
+
assert a["tags"] == ["c", "d"]
|
|
195
|
+
|
|
196
|
+
def test_empty_tags_array_clears_tags_when_in_merge_paths(self):
|
|
197
|
+
a = {"tags": ["a", "b"]}
|
|
198
|
+
b = {"tags": []}
|
|
199
|
+
merge_dicts_with_paths(a, b, (), {("tags",)})
|
|
200
|
+
assert a["tags"] == []
|
|
201
|
+
|
|
202
|
+
def test_none_tags_replaces_tags(self):
|
|
203
|
+
a = {"tags": ["a", "b"]}
|
|
204
|
+
b = {"tags": None}
|
|
205
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
206
|
+
assert a["tags"] is None
|
|
207
|
+
|
|
208
|
+
def test_set_union_only_applies_to_top_level_tags_field(self):
|
|
209
|
+
a = {"metadata": {"tags": ["a", "b"]}}
|
|
210
|
+
b = {"metadata": {"tags": ["c", "d"]}}
|
|
211
|
+
merge_dicts_with_paths(a, b, (), set())
|
|
212
|
+
assert a["metadata"]["tags"] == ["c", "d"]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import json
|
|
2
3
|
import sys
|
|
3
4
|
import threading
|
|
4
5
|
import urllib.parse
|
|
@@ -29,11 +30,16 @@ def coalesce(*args):
|
|
|
29
30
|
return None
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
# Fields that automatically use set-union merge semantics (unless in merge_paths).
|
|
34
|
+
_SET_UNION_FIELDS = frozenset(["tags"])
|
|
35
|
+
|
|
36
|
+
|
|
32
37
|
def merge_dicts_with_paths(
|
|
33
|
-
merge_into: dict[str, Any], merge_from: Mapping[str, Any], path: tuple[str, ...], merge_paths: set[tuple[str]]
|
|
38
|
+
merge_into: dict[str, Any], merge_from: Mapping[str, Any], path: tuple[str, ...], merge_paths: set[tuple[str, ...]]
|
|
34
39
|
) -> dict[str, Any]:
|
|
35
40
|
"""Merges merge_from into merge_into, destructively updating merge_into. Does not merge any further than
|
|
36
|
-
merge_paths.""
|
|
41
|
+
merge_paths. For fields in _SET_UNION_FIELDS (like "tags"), arrays are merged as sets (union)
|
|
42
|
+
unless the field is explicitly listed in merge_paths (opt-out to replacement)."""
|
|
37
43
|
|
|
38
44
|
if not isinstance(merge_into, dict):
|
|
39
45
|
raise ValueError("merge_into must be a dictionary")
|
|
@@ -43,7 +49,22 @@ def merge_dicts_with_paths(
|
|
|
43
49
|
for k, merge_from_v in merge_from.items():
|
|
44
50
|
full_path = path + (k,)
|
|
45
51
|
merge_into_v = merge_into.get(k)
|
|
46
|
-
|
|
52
|
+
|
|
53
|
+
# Check if this field should use set-union merge (e.g., "tags" at top level)
|
|
54
|
+
is_set_union_field = len(path) == 0 and k in _SET_UNION_FIELDS and full_path not in merge_paths
|
|
55
|
+
|
|
56
|
+
if is_set_union_field and isinstance(merge_into_v, list) and isinstance(merge_from_v, list):
|
|
57
|
+
# Set-union merge: combine arrays, deduplicate using JSON for objects
|
|
58
|
+
seen: set[str] = set()
|
|
59
|
+
combined = []
|
|
60
|
+
for item in merge_into_v + list(merge_from_v):
|
|
61
|
+
# Use JSON serialization for consistent object comparison
|
|
62
|
+
item_key = json.dumps(item, sort_keys=True) if isinstance(item, (dict, list)) else str(item)
|
|
63
|
+
if item_key not in seen:
|
|
64
|
+
seen.add(item_key)
|
|
65
|
+
combined.append(item)
|
|
66
|
+
merge_into[k] = combined
|
|
67
|
+
elif isinstance(merge_into_v, dict) and isinstance(merge_from_v, dict) and full_path not in merge_paths:
|
|
47
68
|
merge_dicts_with_paths(merge_into_v, merge_from_v, full_path, merge_paths)
|
|
48
69
|
else:
|
|
49
70
|
merge_into[k] = merge_from_v
|
|
@@ -657,9 +657,52 @@ def patch_litellm():
|
|
|
657
657
|
import litellm
|
|
658
658
|
|
|
659
659
|
if not hasattr(litellm, "_braintrust_wrapped"):
|
|
660
|
+
# Store originals for unpatch_litellm()
|
|
661
|
+
litellm._braintrust_original_completion = litellm.completion
|
|
662
|
+
litellm._braintrust_original_acompletion = litellm.acompletion
|
|
663
|
+
litellm._braintrust_original_responses = litellm.responses
|
|
664
|
+
litellm._braintrust_original_aresponses = litellm.aresponses
|
|
665
|
+
|
|
660
666
|
wrapped = wrap_litellm(litellm)
|
|
661
667
|
litellm.completion = wrapped.completion
|
|
662
668
|
litellm.acompletion = wrapped.acompletion
|
|
669
|
+
litellm.responses = wrapped.responses
|
|
670
|
+
litellm.aresponses = wrapped.aresponses
|
|
663
671
|
litellm._braintrust_wrapped = True
|
|
664
672
|
except ImportError:
|
|
665
673
|
pass # litellm not available
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def unpatch_litellm():
|
|
677
|
+
"""
|
|
678
|
+
Restore LiteLLM to its original state, removing Braintrust tracing.
|
|
679
|
+
|
|
680
|
+
This undoes the patching done by patch_litellm(), restoring the original
|
|
681
|
+
completion, acompletion, responses, and aresponses functions.
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
```python
|
|
685
|
+
import braintrust
|
|
686
|
+
braintrust.patch_litellm()
|
|
687
|
+
|
|
688
|
+
# ... use litellm with tracing ...
|
|
689
|
+
|
|
690
|
+
braintrust.unpatch_litellm() # restore original behavior
|
|
691
|
+
```
|
|
692
|
+
"""
|
|
693
|
+
try:
|
|
694
|
+
import litellm
|
|
695
|
+
|
|
696
|
+
if hasattr(litellm, "_braintrust_wrapped"):
|
|
697
|
+
litellm.completion = litellm._braintrust_original_completion
|
|
698
|
+
litellm.acompletion = litellm._braintrust_original_acompletion
|
|
699
|
+
litellm.responses = litellm._braintrust_original_responses
|
|
700
|
+
litellm.aresponses = litellm._braintrust_original_aresponses
|
|
701
|
+
|
|
702
|
+
delattr(litellm, "_braintrust_wrapped")
|
|
703
|
+
delattr(litellm, "_braintrust_original_completion")
|
|
704
|
+
delattr(litellm, "_braintrust_original_acompletion")
|
|
705
|
+
delattr(litellm, "_braintrust_original_responses")
|
|
706
|
+
delattr(litellm, "_braintrust_original_aresponses")
|
|
707
|
+
except ImportError:
|
|
708
|
+
pass # litellm not available
|
|
@@ -702,3 +702,76 @@ async def test_litellm_async_streaming_with_break(memory_logger):
|
|
|
702
702
|
span = spans[0]
|
|
703
703
|
metrics = span["metrics"]
|
|
704
704
|
assert metrics["time_to_first_token"] >= 0
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@pytest.mark.vcr
|
|
708
|
+
def test_patch_litellm_responses(memory_logger):
|
|
709
|
+
"""Test that patch_litellm() patches responses."""
|
|
710
|
+
from braintrust.wrappers.litellm import patch_litellm, unpatch_litellm
|
|
711
|
+
|
|
712
|
+
assert not memory_logger.pop()
|
|
713
|
+
|
|
714
|
+
patch_litellm()
|
|
715
|
+
try:
|
|
716
|
+
start = time.time()
|
|
717
|
+
# Call litellm.responses directly (not wrapped_litellm.responses)
|
|
718
|
+
response = litellm.responses(
|
|
719
|
+
model=TEST_MODEL,
|
|
720
|
+
input=TEST_PROMPT,
|
|
721
|
+
instructions="Just the number please",
|
|
722
|
+
)
|
|
723
|
+
end = time.time()
|
|
724
|
+
|
|
725
|
+
assert response
|
|
726
|
+
assert response.output
|
|
727
|
+
assert len(response.output) > 0
|
|
728
|
+
content = response.output[0].content[0].text
|
|
729
|
+
assert "24" in content or "twenty-four" in content.lower()
|
|
730
|
+
|
|
731
|
+
# Verify span was created
|
|
732
|
+
spans = memory_logger.pop()
|
|
733
|
+
assert len(spans) == 1
|
|
734
|
+
span = spans[0]
|
|
735
|
+
assert_metrics_are_valid(span["metrics"], start, end)
|
|
736
|
+
assert span["metadata"]["model"] == TEST_MODEL
|
|
737
|
+
assert span["metadata"]["provider"] == "litellm"
|
|
738
|
+
assert TEST_PROMPT in str(span["input"])
|
|
739
|
+
finally:
|
|
740
|
+
unpatch_litellm()
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
@pytest.mark.vcr
|
|
744
|
+
@pytest.mark.asyncio
|
|
745
|
+
async def test_patch_litellm_aresponses(memory_logger):
|
|
746
|
+
"""Test that patch_litellm() patches aresponses."""
|
|
747
|
+
from braintrust.wrappers.litellm import patch_litellm, unpatch_litellm
|
|
748
|
+
|
|
749
|
+
assert not memory_logger.pop()
|
|
750
|
+
|
|
751
|
+
patch_litellm()
|
|
752
|
+
try:
|
|
753
|
+
start = time.time()
|
|
754
|
+
# Call litellm.aresponses directly (not wrapped_litellm.aresponses)
|
|
755
|
+
response = await litellm.aresponses(
|
|
756
|
+
model=TEST_MODEL,
|
|
757
|
+
input=TEST_PROMPT,
|
|
758
|
+
instructions="Just the number please",
|
|
759
|
+
)
|
|
760
|
+
end = time.time()
|
|
761
|
+
|
|
762
|
+
assert response
|
|
763
|
+
assert response.output
|
|
764
|
+
assert len(response.output) > 0
|
|
765
|
+
content = response.output[0].content[0].text
|
|
766
|
+
assert "24" in content or "twenty-four" in content.lower()
|
|
767
|
+
|
|
768
|
+
# Verify span was created
|
|
769
|
+
spans = memory_logger.pop()
|
|
770
|
+
assert len(spans) == 1
|
|
771
|
+
span = spans[0]
|
|
772
|
+
assert_metrics_are_valid(span["metrics"], start, end)
|
|
773
|
+
assert span["metadata"]["model"] == TEST_MODEL
|
|
774
|
+
assert span["metadata"]["provider"] == "litellm"
|
|
775
|
+
assert TEST_PROMPT in str(span["input"])
|
|
776
|
+
finally:
|
|
777
|
+
unpatch_litellm()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/claude_agent_sdk/test_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_pydantic_ai_integration.py
RENAMED
|
File without changes
|
{braintrust-0.4.1 → braintrust-0.4.2}/src/braintrust/wrappers/test_pydantic_ai_wrap_openai.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|