braintrust 0.5.0__py3-none-any.whl → 0.5.3__py3-none-any.whl
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/__init__.py +14 -0
- braintrust/_generated_types.py +56 -3
- braintrust/auto.py +179 -0
- braintrust/conftest.py +23 -4
- braintrust/db_fields.py +10 -0
- braintrust/framework.py +18 -5
- braintrust/generated_types.py +3 -1
- braintrust/logger.py +369 -134
- braintrust/merge_row_batch.py +49 -109
- braintrust/oai.py +51 -0
- braintrust/test_bt_json.py +0 -5
- braintrust/test_context.py +1264 -0
- braintrust/test_framework.py +37 -0
- braintrust/test_http.py +444 -0
- braintrust/test_logger.py +179 -5
- braintrust/test_merge_row_batch.py +160 -0
- braintrust/test_util.py +58 -1
- braintrust/util.py +20 -0
- braintrust/version.py +2 -2
- braintrust/wrappers/agno/__init__.py +2 -3
- braintrust/wrappers/anthropic.py +64 -0
- braintrust/wrappers/claude_agent_sdk/__init__.py +2 -3
- braintrust/wrappers/claude_agent_sdk/test_wrapper.py +9 -0
- braintrust/wrappers/dspy.py +52 -1
- braintrust/wrappers/google_genai/__init__.py +9 -6
- braintrust/wrappers/litellm.py +6 -43
- braintrust/wrappers/pydantic_ai.py +2 -3
- braintrust/wrappers/test_agno.py +9 -0
- braintrust/wrappers/test_anthropic.py +156 -0
- braintrust/wrappers/test_dspy.py +117 -0
- braintrust/wrappers/test_google_genai.py +9 -0
- braintrust/wrappers/test_litellm.py +57 -55
- braintrust/wrappers/test_openai.py +253 -1
- braintrust/wrappers/test_pydantic_ai_integration.py +9 -0
- braintrust/wrappers/test_utils.py +79 -0
- braintrust/wrappers/threads.py +114 -0
- {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/METADATA +1 -1
- {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/RECORD +41 -37
- {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/WHEEL +1 -1
- braintrust/graph_util.py +0 -147
- {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/entry_points.txt +0 -0
- {braintrust-0.5.0.dist-info → braintrust-0.5.3.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
from braintrust import logger
|
|
10
10
|
from braintrust.test_helpers import init_test_logger
|
|
11
11
|
from braintrust.wrappers.anthropic import wrap_anthropic
|
|
12
|
+
from braintrust.wrappers.test_utils import run_in_subprocess, verify_autoinstrument_script
|
|
12
13
|
|
|
13
14
|
TEST_ORG_ID = "test-org-123"
|
|
14
15
|
PROJECT_NAME = "test-anthropic-app"
|
|
@@ -481,3 +482,158 @@ async def test_anthropic_beta_messages_streaming_async(memory_logger):
|
|
|
481
482
|
assert metrics["prompt_tokens"] == usage.input_tokens
|
|
482
483
|
assert metrics["completion_tokens"] == usage.output_tokens
|
|
483
484
|
assert metrics["tokens"] == usage.input_tokens + usage.output_tokens
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class TestPatchAnthropic:
|
|
488
|
+
"""Tests for patch_anthropic() / unpatch_anthropic()."""
|
|
489
|
+
|
|
490
|
+
def test_patch_anthropic_sets_wrapped_flag(self):
|
|
491
|
+
"""patch_anthropic() should set __braintrust_wrapped__ on anthropic module."""
|
|
492
|
+
result = run_in_subprocess("""
|
|
493
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
494
|
+
import anthropic
|
|
495
|
+
|
|
496
|
+
assert not hasattr(anthropic, "__braintrust_wrapped__")
|
|
497
|
+
patch_anthropic()
|
|
498
|
+
assert hasattr(anthropic, "__braintrust_wrapped__")
|
|
499
|
+
print("SUCCESS")
|
|
500
|
+
""")
|
|
501
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
502
|
+
assert "SUCCESS" in result.stdout
|
|
503
|
+
|
|
504
|
+
def test_patch_anthropic_wraps_new_clients(self):
|
|
505
|
+
"""After patch_anthropic(), new Anthropic() clients should be wrapped."""
|
|
506
|
+
result = run_in_subprocess("""
|
|
507
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
508
|
+
patch_anthropic()
|
|
509
|
+
|
|
510
|
+
import anthropic
|
|
511
|
+
client = anthropic.Anthropic(api_key="test-key")
|
|
512
|
+
|
|
513
|
+
# Check that messages is wrapped
|
|
514
|
+
messages_type = type(client.messages).__name__
|
|
515
|
+
print(f"messages_type={messages_type}")
|
|
516
|
+
print("SUCCESS")
|
|
517
|
+
""")
|
|
518
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
519
|
+
assert "SUCCESS" in result.stdout
|
|
520
|
+
|
|
521
|
+
def test_patch_anthropic_idempotent(self):
|
|
522
|
+
"""Multiple patch_anthropic() calls should be safe."""
|
|
523
|
+
result = run_in_subprocess("""
|
|
524
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
525
|
+
import anthropic
|
|
526
|
+
|
|
527
|
+
patch_anthropic()
|
|
528
|
+
first_class = anthropic.Anthropic
|
|
529
|
+
|
|
530
|
+
patch_anthropic() # Second call
|
|
531
|
+
second_class = anthropic.Anthropic
|
|
532
|
+
|
|
533
|
+
assert first_class is second_class
|
|
534
|
+
print("SUCCESS")
|
|
535
|
+
""")
|
|
536
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
537
|
+
assert "SUCCESS" in result.stdout
|
|
538
|
+
|
|
539
|
+
def test_patch_anthropic_creates_spans(self):
|
|
540
|
+
"""patch_anthropic() should create spans when making API calls."""
|
|
541
|
+
result = run_in_subprocess("""
|
|
542
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
543
|
+
from braintrust.test_helpers import init_test_logger
|
|
544
|
+
from braintrust import logger
|
|
545
|
+
|
|
546
|
+
# Set up memory logger
|
|
547
|
+
init_test_logger("test-auto")
|
|
548
|
+
with logger._internal_with_memory_background_logger() as memory_logger:
|
|
549
|
+
patch_anthropic()
|
|
550
|
+
|
|
551
|
+
import anthropic
|
|
552
|
+
client = anthropic.Anthropic()
|
|
553
|
+
|
|
554
|
+
# Make a call within a span context
|
|
555
|
+
import braintrust
|
|
556
|
+
with braintrust.start_span(name="test") as span:
|
|
557
|
+
try:
|
|
558
|
+
# This will fail without API key, but span should still be created
|
|
559
|
+
client.messages.create(
|
|
560
|
+
model="claude-3-5-haiku-latest",
|
|
561
|
+
max_tokens=100,
|
|
562
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
563
|
+
)
|
|
564
|
+
except Exception:
|
|
565
|
+
pass # Expected without API key
|
|
566
|
+
|
|
567
|
+
# Check that spans were logged
|
|
568
|
+
spans = memory_logger.pop()
|
|
569
|
+
# Should have at least the parent span
|
|
570
|
+
assert len(spans) >= 1, f"Expected spans, got {spans}"
|
|
571
|
+
print("SUCCESS")
|
|
572
|
+
""")
|
|
573
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
574
|
+
assert "SUCCESS" in result.stdout
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class TestPatchAnthropicSpans:
|
|
578
|
+
"""VCR-based tests verifying that patch_anthropic() produces spans."""
|
|
579
|
+
|
|
580
|
+
@pytest.mark.vcr
|
|
581
|
+
def test_patch_anthropic_creates_spans(self, memory_logger):
|
|
582
|
+
"""patch_anthropic() should create spans when making API calls."""
|
|
583
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
584
|
+
|
|
585
|
+
assert not memory_logger.pop()
|
|
586
|
+
|
|
587
|
+
patch_anthropic()
|
|
588
|
+
client = anthropic.Anthropic()
|
|
589
|
+
response = client.messages.create(
|
|
590
|
+
model="claude-3-5-haiku-latest",
|
|
591
|
+
max_tokens=100,
|
|
592
|
+
messages=[{"role": "user", "content": "Say hi"}],
|
|
593
|
+
)
|
|
594
|
+
assert response.content[0].text
|
|
595
|
+
|
|
596
|
+
# Verify span was created
|
|
597
|
+
spans = memory_logger.pop()
|
|
598
|
+
assert len(spans) == 1
|
|
599
|
+
span = spans[0]
|
|
600
|
+
assert span["metadata"]["provider"] == "anthropic"
|
|
601
|
+
assert "claude" in span["metadata"]["model"]
|
|
602
|
+
assert span["input"]
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class TestPatchAnthropicAsyncSpans:
|
|
606
|
+
"""VCR-based tests verifying that patch_anthropic() produces spans for async clients."""
|
|
607
|
+
|
|
608
|
+
@pytest.mark.vcr
|
|
609
|
+
@pytest.mark.asyncio
|
|
610
|
+
async def test_patch_anthropic_async_creates_spans(self, memory_logger):
|
|
611
|
+
"""patch_anthropic() should create spans for async API calls."""
|
|
612
|
+
from braintrust.wrappers.anthropic import patch_anthropic
|
|
613
|
+
|
|
614
|
+
assert not memory_logger.pop()
|
|
615
|
+
|
|
616
|
+
patch_anthropic()
|
|
617
|
+
client = anthropic.AsyncAnthropic()
|
|
618
|
+
response = await client.messages.create(
|
|
619
|
+
model="claude-3-5-haiku-latest",
|
|
620
|
+
max_tokens=100,
|
|
621
|
+
messages=[{"role": "user", "content": "Say hi async"}],
|
|
622
|
+
)
|
|
623
|
+
assert response.content[0].text
|
|
624
|
+
|
|
625
|
+
# Verify span was created
|
|
626
|
+
spans = memory_logger.pop()
|
|
627
|
+
assert len(spans) == 1
|
|
628
|
+
span = spans[0]
|
|
629
|
+
assert span["metadata"]["provider"] == "anthropic"
|
|
630
|
+
assert "claude" in span["metadata"]["model"]
|
|
631
|
+
assert span["input"]
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class TestAutoInstrumentAnthropic:
|
|
635
|
+
"""Tests for auto_instrument() with Anthropic."""
|
|
636
|
+
|
|
637
|
+
def test_auto_instrument_anthropic(self):
|
|
638
|
+
"""Test auto_instrument patches Anthropic, creates spans, and uninstrument works."""
|
|
639
|
+
verify_autoinstrument_script("test_auto_anthropic.py")
|
braintrust/wrappers/test_dspy.py
CHANGED
|
@@ -7,6 +7,7 @@ import pytest
|
|
|
7
7
|
from braintrust import logger
|
|
8
8
|
from braintrust.test_helpers import init_test_logger
|
|
9
9
|
from braintrust.wrappers.dspy import BraintrustDSpyCallback
|
|
10
|
+
from braintrust.wrappers.test_utils import run_in_subprocess, verify_autoinstrument_script
|
|
10
11
|
|
|
11
12
|
PROJECT_NAME = "test-dspy-app"
|
|
12
13
|
MODEL = "openai/gpt-4o-mini"
|
|
@@ -58,3 +59,119 @@ def test_dspy_callback(memory_logger):
|
|
|
58
59
|
|
|
59
60
|
# Verify span parenting (LM span should have parent)
|
|
60
61
|
assert lm_span.get("span_parents") # LM span should have parent
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestPatchDSPy:
|
|
65
|
+
"""Tests for patch_dspy() / unpatch_dspy()."""
|
|
66
|
+
|
|
67
|
+
def test_patch_dspy_sets_wrapped_flag(self):
|
|
68
|
+
"""patch_dspy() should set __braintrust_wrapped__ on dspy module."""
|
|
69
|
+
result = run_in_subprocess("""
|
|
70
|
+
dspy = __import__("dspy")
|
|
71
|
+
from braintrust.wrappers.dspy import patch_dspy
|
|
72
|
+
|
|
73
|
+
assert not hasattr(dspy, "__braintrust_wrapped__")
|
|
74
|
+
patch_dspy()
|
|
75
|
+
assert hasattr(dspy, "__braintrust_wrapped__")
|
|
76
|
+
print("SUCCESS")
|
|
77
|
+
""")
|
|
78
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
79
|
+
assert "SUCCESS" in result.stdout
|
|
80
|
+
|
|
81
|
+
def test_patch_dspy_wraps_configure(self):
|
|
82
|
+
"""After patch_dspy(), dspy.configure() should auto-add BraintrustDSpyCallback."""
|
|
83
|
+
result = run_in_subprocess("""
|
|
84
|
+
from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
|
|
85
|
+
patch_dspy()
|
|
86
|
+
|
|
87
|
+
import dspy
|
|
88
|
+
|
|
89
|
+
# Configure without explicitly adding callback
|
|
90
|
+
dspy.configure(lm=None)
|
|
91
|
+
|
|
92
|
+
# Check that BraintrustDSpyCallback was auto-added
|
|
93
|
+
from dspy.dsp.utils.settings import settings
|
|
94
|
+
callbacks = settings.callbacks
|
|
95
|
+
has_bt_callback = any(isinstance(cb, BraintrustDSpyCallback) for cb in callbacks)
|
|
96
|
+
assert has_bt_callback, f"Expected BraintrustDSpyCallback in {callbacks}"
|
|
97
|
+
print("SUCCESS")
|
|
98
|
+
""")
|
|
99
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
100
|
+
assert "SUCCESS" in result.stdout
|
|
101
|
+
|
|
102
|
+
def test_patch_dspy_preserves_existing_callbacks(self):
|
|
103
|
+
"""patch_dspy() should preserve user-provided callbacks."""
|
|
104
|
+
result = run_in_subprocess("""
|
|
105
|
+
from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
|
|
106
|
+
patch_dspy()
|
|
107
|
+
|
|
108
|
+
import dspy
|
|
109
|
+
from dspy.utils.callback import BaseCallback
|
|
110
|
+
|
|
111
|
+
class MyCallback(BaseCallback):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
my_callback = MyCallback()
|
|
115
|
+
dspy.configure(lm=None, callbacks=[my_callback])
|
|
116
|
+
|
|
117
|
+
from dspy.dsp.utils.settings import settings
|
|
118
|
+
callbacks = settings.callbacks
|
|
119
|
+
|
|
120
|
+
# Should have both callbacks
|
|
121
|
+
has_my_callback = any(cb is my_callback for cb in callbacks)
|
|
122
|
+
has_bt_callback = any(isinstance(cb, BraintrustDSpyCallback) for cb in callbacks)
|
|
123
|
+
|
|
124
|
+
assert has_my_callback, "User callback should be preserved"
|
|
125
|
+
assert has_bt_callback, "BraintrustDSpyCallback should be added"
|
|
126
|
+
print("SUCCESS")
|
|
127
|
+
""")
|
|
128
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
129
|
+
assert "SUCCESS" in result.stdout
|
|
130
|
+
|
|
131
|
+
def test_patch_dspy_does_not_duplicate_callback(self):
|
|
132
|
+
"""patch_dspy() should not add duplicate BraintrustDSpyCallback."""
|
|
133
|
+
result = run_in_subprocess("""
|
|
134
|
+
from braintrust.wrappers.dspy import patch_dspy, BraintrustDSpyCallback
|
|
135
|
+
patch_dspy()
|
|
136
|
+
|
|
137
|
+
import dspy
|
|
138
|
+
|
|
139
|
+
# User explicitly adds BraintrustDSpyCallback
|
|
140
|
+
bt_callback = BraintrustDSpyCallback()
|
|
141
|
+
dspy.configure(lm=None, callbacks=[bt_callback])
|
|
142
|
+
|
|
143
|
+
from dspy.dsp.utils.settings import settings
|
|
144
|
+
callbacks = settings.callbacks
|
|
145
|
+
|
|
146
|
+
# Should only have one BraintrustDSpyCallback
|
|
147
|
+
bt_callbacks = [cb for cb in callbacks if isinstance(cb, BraintrustDSpyCallback)]
|
|
148
|
+
assert len(bt_callbacks) == 1, f"Expected 1 BraintrustDSpyCallback, got {len(bt_callbacks)}"
|
|
149
|
+
print("SUCCESS")
|
|
150
|
+
""")
|
|
151
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
152
|
+
assert "SUCCESS" in result.stdout
|
|
153
|
+
|
|
154
|
+
def test_patch_dspy_idempotent(self):
|
|
155
|
+
"""Multiple patch_dspy() calls should be safe."""
|
|
156
|
+
result = run_in_subprocess("""
|
|
157
|
+
from braintrust.wrappers.dspy import patch_dspy
|
|
158
|
+
import dspy
|
|
159
|
+
|
|
160
|
+
patch_dspy()
|
|
161
|
+
patch_dspy() # Second call - should be no-op, not double-wrap
|
|
162
|
+
|
|
163
|
+
# Verify configure still works
|
|
164
|
+
lm = dspy.LM("openai/gpt-4o-mini")
|
|
165
|
+
dspy.configure(lm=lm)
|
|
166
|
+
print("SUCCESS")
|
|
167
|
+
""")
|
|
168
|
+
assert result.returncode == 0, f"Failed: {result.stderr}"
|
|
169
|
+
assert "SUCCESS" in result.stdout
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestAutoInstrumentDSPy:
|
|
173
|
+
"""Tests for auto_instrument() with DSPy."""
|
|
174
|
+
|
|
175
|
+
def test_auto_instrument_dspy(self):
|
|
176
|
+
"""Test auto_instrument patches DSPy, creates spans, and uninstrument works."""
|
|
177
|
+
verify_autoinstrument_script("test_auto_dspy.py")
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
from braintrust import logger
|
|
7
7
|
from braintrust.test_helpers import init_test_logger
|
|
8
8
|
from braintrust.wrappers.google_genai import setup_genai
|
|
9
|
+
from braintrust.wrappers.test_utils import verify_autoinstrument_script
|
|
9
10
|
from google.genai import types
|
|
10
11
|
from google.genai.client import Client
|
|
11
12
|
|
|
@@ -637,3 +638,11 @@ def test_attachment_with_pydantic_model(memory_logger):
|
|
|
637
638
|
|
|
638
639
|
# Attachment should be preserved
|
|
639
640
|
assert copied["context_file"] is attachment
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class TestAutoInstrumentGoogleGenAI:
|
|
644
|
+
"""Tests for auto_instrument() with Google GenAI."""
|
|
645
|
+
|
|
646
|
+
def test_auto_instrument_google_genai(self):
|
|
647
|
+
"""Test auto_instrument patches Google GenAI and creates spans."""
|
|
648
|
+
verify_autoinstrument_script("test_auto_google_genai.py")
|
|
@@ -6,7 +6,7 @@ import pytest
|
|
|
6
6
|
from braintrust import logger
|
|
7
7
|
from braintrust.test_helpers import assert_dict_matches, init_test_logger
|
|
8
8
|
from braintrust.wrappers.litellm import wrap_litellm
|
|
9
|
-
from braintrust.wrappers.test_utils import assert_metrics_are_valid
|
|
9
|
+
from braintrust.wrappers.test_utils import assert_metrics_are_valid, verify_autoinstrument_script
|
|
10
10
|
|
|
11
11
|
TEST_ORG_ID = "test-org-litellm-py-tracing"
|
|
12
12
|
PROJECT_NAME = "test-project-litellm-py-tracing"
|
|
@@ -697,71 +697,73 @@ async def test_litellm_async_streaming_with_break(memory_logger):
|
|
|
697
697
|
@pytest.mark.vcr
|
|
698
698
|
def test_patch_litellm_responses(memory_logger):
|
|
699
699
|
"""Test that patch_litellm() patches responses."""
|
|
700
|
-
from braintrust.wrappers.litellm import patch_litellm
|
|
700
|
+
from braintrust.wrappers.litellm import patch_litellm
|
|
701
701
|
|
|
702
702
|
assert not memory_logger.pop()
|
|
703
703
|
|
|
704
704
|
patch_litellm()
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
assert TEST_PROMPT in str(span["input"])
|
|
729
|
-
finally:
|
|
730
|
-
unpatch_litellm()
|
|
705
|
+
start = time.time()
|
|
706
|
+
# Call litellm.responses directly (not wrapped_litellm.responses)
|
|
707
|
+
response = litellm.responses(
|
|
708
|
+
model=TEST_MODEL,
|
|
709
|
+
input=TEST_PROMPT,
|
|
710
|
+
instructions="Just the number please",
|
|
711
|
+
)
|
|
712
|
+
end = time.time()
|
|
713
|
+
|
|
714
|
+
assert response
|
|
715
|
+
assert response.output
|
|
716
|
+
assert len(response.output) > 0
|
|
717
|
+
content = response.output[0].content[0].text
|
|
718
|
+
assert "24" in content or "twenty-four" in content.lower()
|
|
719
|
+
|
|
720
|
+
# Verify span was created
|
|
721
|
+
spans = memory_logger.pop()
|
|
722
|
+
assert len(spans) == 1
|
|
723
|
+
span = spans[0]
|
|
724
|
+
assert_metrics_are_valid(span["metrics"], start, end)
|
|
725
|
+
assert span["metadata"]["model"] == TEST_MODEL
|
|
726
|
+
assert span["metadata"]["provider"] == "litellm"
|
|
727
|
+
assert TEST_PROMPT in str(span["input"])
|
|
731
728
|
|
|
732
729
|
|
|
733
730
|
@pytest.mark.vcr
|
|
734
731
|
@pytest.mark.asyncio
|
|
735
732
|
async def test_patch_litellm_aresponses(memory_logger):
|
|
736
733
|
"""Test that patch_litellm() patches aresponses."""
|
|
737
|
-
from braintrust.wrappers.litellm import patch_litellm
|
|
734
|
+
from braintrust.wrappers.litellm import patch_litellm
|
|
738
735
|
|
|
739
736
|
assert not memory_logger.pop()
|
|
740
737
|
|
|
741
738
|
patch_litellm()
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
739
|
+
start = time.time()
|
|
740
|
+
# Call litellm.aresponses directly (not wrapped_litellm.aresponses)
|
|
741
|
+
response = await litellm.aresponses(
|
|
742
|
+
model=TEST_MODEL,
|
|
743
|
+
input=TEST_PROMPT,
|
|
744
|
+
instructions="Just the number please",
|
|
745
|
+
)
|
|
746
|
+
end = time.time()
|
|
747
|
+
|
|
748
|
+
assert response
|
|
749
|
+
assert response.output
|
|
750
|
+
assert len(response.output) > 0
|
|
751
|
+
content = response.output[0].content[0].text
|
|
752
|
+
assert "24" in content or "twenty-four" in content.lower()
|
|
753
|
+
|
|
754
|
+
# Verify span was created
|
|
755
|
+
spans = memory_logger.pop()
|
|
756
|
+
assert len(spans) == 1
|
|
757
|
+
span = spans[0]
|
|
758
|
+
assert_metrics_are_valid(span["metrics"], start, end)
|
|
759
|
+
assert span["metadata"]["model"] == TEST_MODEL
|
|
760
|
+
assert span["metadata"]["provider"] == "litellm"
|
|
761
|
+
assert TEST_PROMPT in str(span["input"])
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class TestAutoInstrumentLiteLLM:
|
|
765
|
+
"""Tests for auto_instrument() with LiteLLM."""
|
|
766
|
+
|
|
767
|
+
def test_auto_instrument_litellm(self):
|
|
768
|
+
"""Test auto_instrument patches LiteLLM, creates spans, and uninstrument works."""
|
|
769
|
+
verify_autoinstrument_script("test_auto_litellm.py")
|