braintrust 0.3.11__py3-none-any.whl → 0.3.12__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.
@@ -10,6 +10,7 @@ from ..logger import BraintrustState
10
10
  ORIGIN_HEADER = "origin"
11
11
  BRAINTRUST_AUTH_TOKEN_HEADER = "x-bt-auth-token"
12
12
  BRAINTRUST_ORG_NAME_HEADER = "x-bt-org-name"
13
+ BRAINTRUST_PROJECT_ID_HEADER = "x-bt-project-id"
13
14
 
14
15
 
15
16
  @dataclass
@@ -17,6 +18,7 @@ class RequestContext:
17
18
  app_origin: Optional[str]
18
19
  token: Optional[str]
19
20
  org_name: Optional[str]
21
+ project_id: Optional[str]
20
22
  state: Optional[BraintrustState]
21
23
 
22
24
 
@@ -56,6 +58,7 @@ class AuthorizationMiddleware(BaseHTTPMiddleware):
56
58
  app_origin=extract_allowed_origin(request.headers.get(ORIGIN_HEADER)),
57
59
  token=None,
58
60
  org_name=request.headers.get(BRAINTRUST_ORG_NAME_HEADER),
61
+ project_id=request.headers.get(BRAINTRUST_PROJECT_ID_HEADER),
59
62
  state=None,
60
63
  )
61
64
 
@@ -18,6 +18,7 @@ ALLOWED_HEADERS = [
18
18
  "x-bt-auth-token",
19
19
  "x-bt-parent",
20
20
  "x-bt-org-name",
21
+ "x-bt-project-id",
21
22
  "x-bt-stream-fmt",
22
23
  "x-bt-use-cache",
23
24
  "x-stainless-os",
@@ -196,7 +196,7 @@ async def run_eval(request: Request) -> Union[JSONResponse, StreamingResponse]:
196
196
  "state": state,
197
197
  "scores": evaluator.scores
198
198
  + [
199
- make_scorer(state, score["name"], score["function_id"])
199
+ make_scorer(state, score["name"], score["function_id"], ctx.project_id)
200
200
  for score in eval_data.get("scores", [])
201
201
  ],
202
202
  "stream": stream_fn,
@@ -305,7 +305,7 @@ def snake_to_camel(snake_str: str) -> str:
305
305
  return components[0] + "".join(x.title() for x in components[1:]) if components else snake_str
306
306
 
307
307
 
308
- def make_scorer(state: BraintrustState, name: str, score: FunctionId) -> EvalScorer[Any, Any]:
308
+ def make_scorer(state: BraintrustState, name: str, score: FunctionId, project_id: Optional[str] = None) -> EvalScorer[Any, Any]:
309
309
  def scorer_fn(input, output, expected, metadata):
310
310
  request = {
311
311
  **score,
@@ -315,7 +315,10 @@ def make_scorer(state: BraintrustState, name: str, score: FunctionId) -> EvalSco
315
315
  "mode": "auto",
316
316
  "strict": True,
317
317
  }
318
- result = state.proxy_conn().post("function/invoke", json=request, headers={"Accept": "application/json"})
318
+ headers = {"Accept": "application/json"}
319
+ if project_id:
320
+ headers["x-bt-project-id"] = project_id
321
+ result = state.proxy_conn().post("function/invoke", json=request, headers=headers)
319
322
  result.raise_for_status()
320
323
  data = result.json()
321
324
  return data
braintrust/version.py CHANGED
@@ -1,4 +1,4 @@
1
- VERSION = "0.3.11"
1
+ VERSION = "0.3.12"
2
2
 
3
3
  # this will be templated during the build
4
- GIT_COMMIT = "7cffe54f8b960931055ae20f5983b4ea34515eff"
4
+ GIT_COMMIT = "8c2b2995c2097f5699c101f4ab5e653ff40014a1"
@@ -1690,35 +1690,3 @@ def test_braintrust_tracing_processor_trace_metadata_logging(memory_logger):
1690
1690
  spans = memory_logger.pop()
1691
1691
  root_span = spans[0]
1692
1692
  assert root_span["metadata"]["conversation_id"] == "test-12345", "Should log trace metadata"
1693
-
1694
-
1695
- def test_parse_metrics_excludes_booleans():
1696
- """Test that boolean fields in usage objects are excluded from metrics.
1697
-
1698
- Reproduces issue where OpenRouter returns is_byok (a boolean) in the usage
1699
- object, which caused API validation errors: "Expected number, received boolean".
1700
-
1701
- In Python, bool is a subclass of int, so isinstance(True, int) returns True.
1702
- The fix ensures _is_numeric explicitly excludes booleans.
1703
- """
1704
- from braintrust.oai import _parse_metrics_from_usage
1705
-
1706
- # Simulate OpenRouter's usage object with boolean field
1707
- usage = {
1708
- "completion_tokens": 11,
1709
- "prompt_tokens": 8,
1710
- "total_tokens": 19,
1711
- "cost": 0.000104,
1712
- "is_byok": False, # This boolean should be filtered out
1713
- }
1714
-
1715
- metrics = _parse_metrics_from_usage(usage)
1716
-
1717
- # Numeric fields should be included
1718
- assert metrics["completion_tokens"] == 11
1719
- assert metrics["prompt_tokens"] == 8
1720
- assert metrics["tokens"] == 19 # total_tokens gets renamed
1721
- assert metrics["cost"] == 0.000104
1722
-
1723
- # Boolean field should NOT be in metrics (this was the bug)
1724
- assert "is_byok" not in metrics
@@ -0,0 +1,144 @@
1
+ """
2
+ Tests to ensure wrap_openai works correctly with OpenRouter.
3
+
4
+ OpenRouter is a popular API gateway that provides access to multiple LLM providers
5
+ through an OpenAI-compatible interface. This test validates that our wrapper handles
6
+ OpenRouter-specific response fields correctly (e.g., boolean `is_byok` in usage).
7
+ """
8
+
9
+ import os
10
+ import time
11
+
12
+ import pytest
13
+ from braintrust import logger, wrap_openai
14
+ from braintrust.test_helpers import init_test_logger
15
+ from braintrust.wrappers.test_utils import assert_metrics_are_valid
16
+ from openai import AsyncOpenAI, OpenAI
17
+
18
+ PROJECT_NAME = "test-openrouter"
19
+ TEST_MODEL = "openai/gpt-4o-mini"
20
+
21
+
22
+ @pytest.fixture(scope="module")
23
+ def vcr_config():
24
+ return {
25
+ "filter_headers": [
26
+ "authorization",
27
+ ]
28
+ }
29
+
30
+
31
+ @pytest.fixture
32
+ def memory_logger():
33
+ init_test_logger(PROJECT_NAME)
34
+ with logger._internal_with_memory_background_logger() as bgl:
35
+ yield bgl
36
+
37
+
38
+ def _get_client():
39
+ return OpenAI(
40
+ base_url="https://openrouter.ai/api/v1",
41
+ api_key=os.environ.get("OPENROUTER_API_KEY"),
42
+ )
43
+
44
+
45
+ def _get_async_client():
46
+ return AsyncOpenAI(
47
+ base_url="https://openrouter.ai/api/v1",
48
+ api_key=os.environ.get("OPENROUTER_API_KEY"),
49
+ )
50
+
51
+
52
+ @pytest.mark.vcr
53
+ def test_openrouter_chat_completion_sync(memory_logger):
54
+ assert not memory_logger.pop()
55
+
56
+ client = wrap_openai(_get_client())
57
+
58
+ start = time.time()
59
+ response = client.chat.completions.create(
60
+ model=TEST_MODEL,
61
+ messages=[{"role": "user", "content": "What is 2+2? Reply with just the number."}],
62
+ max_tokens=10,
63
+ )
64
+ end = time.time()
65
+
66
+ assert response
67
+ assert response.choices[0].message.content
68
+ assert "4" in response.choices[0].message.content
69
+
70
+ spans = memory_logger.pop()
71
+ assert len(spans) == 1
72
+ span = spans[0]
73
+
74
+ metrics = span["metrics"]
75
+ assert_metrics_are_valid(metrics, start, end)
76
+
77
+ # Ensure no boolean values in metrics (the original bug with is_byok)
78
+ for key, value in metrics.items():
79
+ assert not isinstance(value, bool), f"Metric {key} should not be a boolean"
80
+
81
+
82
+ @pytest.mark.vcr
83
+ @pytest.mark.asyncio
84
+ async def test_openrouter_chat_completion_async(memory_logger):
85
+ """Test that wrap_openai works with OpenRouter's async client."""
86
+ assert not memory_logger.pop()
87
+
88
+ client = wrap_openai(_get_async_client())
89
+
90
+ start = time.time()
91
+ response = await client.chat.completions.create(
92
+ model=TEST_MODEL,
93
+ messages=[{"role": "user", "content": "What is 3+3? Reply with just the number."}],
94
+ max_tokens=10,
95
+ )
96
+ end = time.time()
97
+
98
+ assert response
99
+ assert response.choices[0].message.content
100
+ assert "6" in response.choices[0].message.content
101
+
102
+ spans = memory_logger.pop()
103
+ assert len(spans) == 1
104
+ span = spans[0]
105
+
106
+ metrics = span["metrics"]
107
+ assert_metrics_are_valid(metrics, start, end)
108
+
109
+ for key, value in metrics.items():
110
+ assert not isinstance(value, bool), f"Metric {key} should not be a boolean"
111
+
112
+
113
+ @pytest.mark.vcr
114
+ def test_openrouter_streaming_sync(memory_logger):
115
+ """Test that wrap_openai works with OpenRouter's streaming responses."""
116
+ assert not memory_logger.pop()
117
+
118
+ client = wrap_openai(_get_client())
119
+
120
+ start = time.time()
121
+ chunks = []
122
+ stream = client.chat.completions.create(
123
+ model=TEST_MODEL,
124
+ messages=[{"role": "user", "content": "What is 5+5? Reply with just the number."}],
125
+ max_tokens=10,
126
+ stream=True,
127
+ )
128
+ for chunk in stream:
129
+ chunks.append(chunk)
130
+ end = time.time()
131
+
132
+ assert chunks
133
+ content = "".join(c.choices[0].delta.content or "" for c in chunks if c.choices)
134
+ assert "10" in content
135
+
136
+ spans = memory_logger.pop()
137
+ assert len(spans) == 1
138
+ span = spans[0]
139
+
140
+ metrics = span["metrics"]
141
+ assert_metrics_are_valid(metrics, start, end)
142
+
143
+ for key, value in metrics.items():
144
+ assert not isinstance(value, bool), f"Metric {key} should not be a boolean"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrust
3
- Version: 0.3.11
3
+ Version: 0.3.12
4
4
  Summary: SDK for integrating Braintrust
5
5
  Home-page: https://www.braintrust.dev
6
6
  Author: Braintrust
@@ -42,7 +42,7 @@ braintrust/test_span_components.py,sha256=UnF6ZL4k41XZ-CnfbjuqLeK4MZLtHTMdID3CMh
42
42
  braintrust/test_util.py,sha256=gyqe2JspRP7oXlp6ENztZe2fdRTOEMZMKpQi00y1DSc,4538
43
43
  braintrust/test_version.py,sha256=hk5JKjEFbNJ_ONc1VEkqHquflzre34RpFhCEYLTK8iA,1051
44
44
  braintrust/util.py,sha256=Ec6sRkQw5BckGrFjdA4YTyu_2BaKmHh4tWDwAi_ysOw,7227
45
- braintrust/version.py,sha256=vQReyh_dMfpWVANMPnGfk8BBp3hMUza8lWOySpnsHEg,118
45
+ braintrust/version.py,sha256=i1ljewUfl4VA2SYv_e8CfC5C1RUiCNBL8mOBlRusJHk,118
46
46
  braintrust/xact_ids.py,sha256=bdyp88HjlyIkglgLSqYlCYscdSH6EWVyE14sR90Xl1s,658
47
47
  braintrust/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  braintrust/cli/__main__.py,sha256=wCBKHGVmn3IT_yMXk5qfDwyI2SV2gf1tLr0NTxm9T8k,1519
@@ -58,13 +58,13 @@ braintrust/contrib/__init__.py,sha256=Dh-0yGnjLhjK34QTV8ZRnjRIXbLu3q8C1h9GQsRKP6
58
58
  braintrust/contrib/temporal/__init__.py,sha256=kyV2FwryszPGb6sR5H3xR9FijFbJslDC3k-d7RNZufE,16519
59
59
  braintrust/contrib/temporal/test_temporal.py,sha256=UJxogj9z6qXgVCPQ_FiUSIfJXvz2LMsfLKMgrboyLw0,18491
60
60
  braintrust/devserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- braintrust/devserver/auth.py,sha256=rRHNe31dbCKt8jSRsIx0DjyRqi98PZ5LtJ-ScGu2TfE,2683
61
+ braintrust/devserver/auth.py,sha256=ZCtfLI16u35elXx6vDHGJTcpEV8wJVVfJALuuVHZxW0,2840
62
62
  braintrust/devserver/cache.py,sha256=UPl_Co4D4WqWK8ly8lozkLBEk609qlXiF5DMrOWpTHw,1804
63
- braintrust/devserver/cors.py,sha256=7spdtvhXHEJ6FLIQQygwi1S7D52PPTY2RteG8sw7YQw,5568
63
+ braintrust/devserver/cors.py,sha256=Tp0C3KuR0jEbLcjnBA0HyEgzesW14OybYI-xqxHfMzk,5591
64
64
  braintrust/devserver/dataset.py,sha256=93iWWvmuWQut2uG2AJD63Q8fPZGA0Nc9y_r6X_XJRFM,2315
65
65
  braintrust/devserver/eval_hooks.py,sha256=T5LWmAZjqa8kiXDKXYZlEsx_VhbIoiv8_X10s2v9C7M,1767
66
66
  braintrust/devserver/schemas.py,sha256=q9_I1DoaF5clLmMCkQvpB1DvFhy2GE1dZCumAc0S76M,9098
67
- braintrust/devserver/server.py,sha256=8lU0WNweoGM9sK8IiKQj4RtnNupKVlNEBZpbcXP07fc,12455
67
+ braintrust/devserver/server.py,sha256=vmpLYR8OihAINpygkvPPNbg0t4M1OPeCb8rSco6QY9s,12606
68
68
  braintrust/devserver/test_cached_login.py,sha256=8RxzS3UuBeGx2L5j4mzXSQnJzF7pL0d4AhS3MNzyqi8,3741
69
69
  braintrust/devserver/test_lru_cache.py,sha256=5YYJ5uFj7k4Z4PQQ-UOV7bLP5zBYVo-5jV5_hpthtgM,4164
70
70
  braintrust/devserver/test_server_integration.py,sha256=ygIYDsTCSqwa_rzQDtdX0mjkXQVy7qyJjntdoCgtjKM,6711
@@ -95,7 +95,8 @@ braintrust/wrappers/test_anthropic.py,sha256=tSHAnroaLK1QYhLhhPFAxSDEJUybzffp2o4
95
95
  braintrust/wrappers/test_dspy.py,sha256=x5vXu-NoFFjWGIcvFvXICVsDasQEQzPuSlxgzbHJcwo,1953
96
96
  braintrust/wrappers/test_google_genai.py,sha256=20e4DqLUZmcpVGircj7vN0IxsS5FVjvveqOu0JNZ5jo,17596
97
97
  braintrust/wrappers/test_litellm.py,sha256=sNId6qnbQibT66aB_lsxX_HlaPF6GoMFaawgnF1A2a4,21428
98
- braintrust/wrappers/test_openai.py,sha256=Wu9iemfl86Bp29apgNBBBxTR3ukWbstpf2o6P_bDKAE,61374
98
+ braintrust/wrappers/test_openai.py,sha256=UqKtoIlZlqiLC6x7Rbv9zROgDdE5zvXZObbFMKpmifw,60237
99
+ braintrust/wrappers/test_openrouter.py,sha256=9wIYuwObFkADH8h5pzHFB4C9TcL_HTeq1KlODYhSMLs,3987
99
100
  braintrust/wrappers/test_pydantic_ai.py,sha256=uqWbk1D8iX3q9qh_Q8PkopNUfPQCBCvOSa4KkpuIqqE,5057
100
101
  braintrust/wrappers/test_utils.py,sha256=Qz7LYG5V0DK2KuTJ_YLGpO_Zr_LJFfJgZX_Ps8tlM_c,505
101
102
  braintrust/wrappers/agno/__init__.py,sha256=V0dnEhu2U0FabyEmwDbWv5x6rP1rdI8z9JLAB_8XuXk,2416
@@ -108,8 +109,8 @@ braintrust/wrappers/claude_agent_sdk/__init__.py,sha256=CSXJWy-z2fHF7h4VJjLSnXJv
108
109
  braintrust/wrappers/claude_agent_sdk/_wrapper.py,sha256=uzElIOwwPmF_Y5fbWcKWEPC8HnSzW7byzpiuVKK0TXE,15613
109
110
  braintrust/wrappers/claude_agent_sdk/test_wrapper.py,sha256=0NmohdECudFvWtc-5PbANtTXzexkkwIJhGbujydDrT8,6826
110
111
  braintrust/wrappers/google_genai/__init__.py,sha256=PGFMuR3c4Gc3SUt24eP7z5AzdS2Dc1uF1d3QPCnLnuo,16018
111
- braintrust-0.3.11.dist-info/METADATA,sha256=1oszinC18YZo77yRQKmnesXudHOcX2jGmjAyxpGDEMM,3131
112
- braintrust-0.3.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
113
- braintrust-0.3.11.dist-info/entry_points.txt,sha256=Zpc0_09g5xm8as5jHqqFq7fhwO0xHSNct_TrEMONS7Q,60
114
- braintrust-0.3.11.dist-info/top_level.txt,sha256=hw1-y-UFMf60RzAr8x_eM7SThbIuWfQsQIbVvqSF83A,11
115
- braintrust-0.3.11.dist-info/RECORD,,
112
+ braintrust-0.3.12.dist-info/METADATA,sha256=LMIW6iDtc0F1i6QsqMWoUcJn6RREF0H9XJ0n6mHjYb4,3131
113
+ braintrust-0.3.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
114
+ braintrust-0.3.12.dist-info/entry_points.txt,sha256=Zpc0_09g5xm8as5jHqqFq7fhwO0xHSNct_TrEMONS7Q,60
115
+ braintrust-0.3.12.dist-info/top_level.txt,sha256=hw1-y-UFMf60RzAr8x_eM7SThbIuWfQsQIbVvqSF83A,11
116
+ braintrust-0.3.12.dist-info/RECORD,,