Skip to content

Commit 12e80e8

Browse files
committed
feat: Add openrouter integration
1 parent 7a4328e commit 12e80e8

23 files changed

Lines changed: 1559 additions & 51 deletions

py/noxfile.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _pinned_python_version():
7070
"autoevals",
7171
"braintrust_core",
7272
"litellm",
73+
"openrouter",
7374
"opentelemetry-api",
7475
"opentelemetry-sdk",
7576
"opentelemetry-exporter-otlp-proto-http",
@@ -100,6 +101,7 @@ def _pinned_python_version():
100101
GENAI_VERSIONS = (LATEST,)
101102
DSPY_VERSIONS = (LATEST,)
102103
GOOGLE_ADK_VERSIONS = (LATEST, "1.14.1")
104+
OPENROUTER_VERSIONS = (LATEST, "0.6.0")
103105
# temporalio 1.19.0+ requires Python >= 3.10; skip Python 3.9 entirely
104106
TEMPORAL_VERSIONS = (LATEST, "1.20.0", "1.19.0")
105107
PYTEST_VERSIONS = (LATEST, "8.4.2")
@@ -210,6 +212,7 @@ def test_openai(session, version):
210212
# openai-agents requires Python >= 3.10
211213
_install(session, "openai-agents")
212214
_run_tests(session, f"{WRAPPER_DIR}/test_openai.py")
215+
_run_tests(session, f"{WRAPPER_DIR}/test_openai_openrouter_gateway.py")
213216
_run_core_tests(session)
214217

215218

@@ -224,11 +227,12 @@ def test_openai_http2_streaming(session):
224227

225228

226229
@nox.session()
227-
def test_openrouter(session):
228-
"""Test wrap_openai with OpenRouter. Requires OPENROUTER_API_KEY env var."""
230+
@nox.parametrize("version", OPENROUTER_VERSIONS, ids=OPENROUTER_VERSIONS)
231+
def test_openrouter(session, version):
232+
"""Test the native OpenRouter SDK integration."""
229233
_install_test_deps(session)
230-
_install(session, "openai")
231-
_run_tests(session, f"{WRAPPER_DIR}/test_openrouter.py")
234+
_install(session, "openrouter", version)
235+
_run_tests(session, f"{INTEGRATION_DIR}/openrouter/test_openrouter.py")
232236

233237

234238
@nox.session()

py/requirements-optional.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ google-adk==1.14.1
77
dspy==3.1.3
88
langsmith==0.7.12
99
litellm==1.82.0
10+
openrouter==0.7.11

py/src/braintrust/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def is_equal(expected, output):
7373
from .integrations.anthropic import (
7474
wrap_anthropic, # noqa: F401 # type: ignore[reportUnusedImport]
7575
)
76+
from .integrations.openrouter import (
77+
wrap_openrouter, # noqa: F401 # type: ignore[reportUnusedImport]
78+
)
7679
from .logger import *
7780
from .logger import (
7881
_internal_get_global_state, # noqa: F401 # type: ignore[reportUnusedImport]

py/src/braintrust/auto.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
AnthropicIntegration,
1414
ClaudeAgentSDKIntegration,
1515
GoogleGenAIIntegration,
16+
OpenRouterIntegration,
1617
)
1718

1819

@@ -39,6 +40,7 @@ def auto_instrument(
3940
litellm: bool = True,
4041
pydantic_ai: bool = True,
4142
google_genai: bool = True,
43+
openrouter: bool = True,
4244
agno: bool = True,
4345
claude_agent_sdk: bool = True,
4446
dspy: bool = True,
@@ -59,6 +61,7 @@ def auto_instrument(
5961
litellm: Enable LiteLLM instrumentation (default: True)
6062
pydantic_ai: Enable Pydantic AI instrumentation (default: True)
6163
google_genai: Enable Google GenAI instrumentation (default: True)
64+
openrouter: Enable OpenRouter instrumentation (default: True)
6265
agno: Enable Agno instrumentation (default: True)
6366
claude_agent_sdk: Enable Claude Agent SDK instrumentation (default: True)
6467
dspy: Enable DSPy instrumentation (default: True)
@@ -120,6 +123,8 @@ def auto_instrument(
120123
results["pydantic_ai"] = _instrument_pydantic_ai()
121124
if google_genai:
122125
results["google_genai"] = _instrument_integration(GoogleGenAIIntegration)
126+
if openrouter:
127+
results["openrouter"] = _instrument_integration(OpenRouterIntegration)
123128
if agno:
124129
results["agno"] = _instrument_integration(AgnoIntegration)
125130
if claude_agent_sdk:

py/src/braintrust/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .anthropic import AnthropicIntegration
44
from .claude_agent_sdk import ClaudeAgentSDKIntegration
55
from .google_genai import GoogleGenAIIntegration
6+
from .openrouter import OpenRouterIntegration
67

78

89
__all__ = [
@@ -11,4 +12,5 @@
1112
"AnthropicIntegration",
1213
"ClaudeAgentSDKIntegration",
1314
"GoogleGenAIIntegration",
15+
"OpenRouterIntegration",
1416
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Test auto_instrument for OpenRouter."""
2+
3+
import os
4+
5+
import openrouter
6+
from braintrust.auto import auto_instrument
7+
from braintrust.wrappers.test_utils import autoinstrument_test_context
8+
9+
10+
results = auto_instrument()
11+
assert results.get("openrouter") == True
12+
13+
results2 = auto_instrument()
14+
assert results2.get("openrouter") == True
15+
16+
with autoinstrument_test_context("test_auto_openrouter") as memory_logger:
17+
client = openrouter.OpenRouter(api_key=os.environ.get("OPENROUTER_API_KEY"))
18+
response = client.chat.send(
19+
model="openai/gpt-4o-mini",
20+
messages=[{"role": "user", "content": "What is 2+2? Reply with just the number."}],
21+
max_tokens=10,
22+
)
23+
assert "4" in response.choices[0].message.content
24+
25+
spans = memory_logger.pop()
26+
assert len(spans) == 1, f"Expected 1 span, got {len(spans)}"
27+
span = spans[0]
28+
assert span["metadata"]["provider"] == "openai"
29+
assert span["metadata"]["model"] == "gpt-4o-mini"
30+
assert "4" in span["output"][0]["message"]["content"]
31+
32+
print("SUCCESS")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Braintrust integration for the OpenRouter Python SDK."""
2+
3+
from .integration import OpenRouterIntegration
4+
from .tracing import wrap_openrouter
5+
6+
7+
__all__ = [
8+
"OpenRouterIntegration",
9+
"wrap_openrouter",
10+
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"user","content":"What is 2+2? Reply with just the
4+
number."}],"model":"openai/gpt-4o-mini","max_tokens":10.0,"stream":false,"temperature":1.0,"top_p":1.0}'
5+
headers:
6+
Accept:
7+
- application/json
8+
Accept-Encoding:
9+
- gzip, deflate
10+
Connection:
11+
- keep-alive
12+
Content-Length:
13+
- '175'
14+
Host:
15+
- openrouter.ai
16+
content-type:
17+
- application/json
18+
user-agent:
19+
- speakeasy-sdk/python 0.7.11 2.768.0 1.0.0 openrouter
20+
method: POST
21+
uri: https://openrouter.ai/api/v1/chat/completions
22+
response:
23+
body:
24+
string: "\n \n{\"id\":\"gen-1774558315-QxPl7aBABQW8bOKxzdxL\",\"object\":\"chat.completion\",\"created\":1774558315,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_f85b8886b6\",\"choices\":[{\"index\":0,\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"message\":{\"role\":\"assistant\",\"content\":\"4\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":1,\"total_tokens\":21,\"cost\":0.0000036,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.0000036,\"upstream_inference_prompt_cost\":0.000003,\"upstream_inference_completions_cost\":6e-7},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}"
25+
headers:
26+
Access-Control-Allow-Origin:
27+
- '*'
28+
CF-RAY:
29+
- 9e2909425a3b053a-SJC
30+
Connection:
31+
- keep-alive
32+
Content-Type:
33+
- application/json
34+
Date:
35+
- Thu, 26 Mar 2026 20:51:56 GMT
36+
Permissions-Policy:
37+
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com"
38+
"https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
39+
Referrer-Policy:
40+
- no-referrer, strict-origin-when-cross-origin
41+
Server:
42+
- cloudflare
43+
Transfer-Encoding:
44+
- chunked
45+
X-Content-Type-Options:
46+
- nosniff
47+
content-length:
48+
- '785'
49+
status:
50+
code: 200
51+
message: OK
52+
version: 1
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"user","content":"What is 3+3? Reply with just the
4+
number."}],"model":"openai/gpt-4o-mini","max_tokens":10.0,"stream":false,"temperature":1.0,"top_p":1.0}'
5+
headers:
6+
Accept:
7+
- application/json
8+
Accept-Encoding:
9+
- gzip, deflate
10+
Connection:
11+
- keep-alive
12+
Content-Length:
13+
- '175'
14+
Host:
15+
- openrouter.ai
16+
content-type:
17+
- application/json
18+
user-agent:
19+
- speakeasy-sdk/python 0.7.11 2.768.0 1.0.0 openrouter
20+
method: POST
21+
uri: https://openrouter.ai/api/v1/chat/completions
22+
response:
23+
body:
24+
string: "\n \n{\"id\":\"gen-1774558313-OLdiGZAnrgirxGutvM1d\",\"object\":\"chat.completion\",\"created\":1774558313,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_f85b8886b6\",\"choices\":[{\"index\":0,\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"message\":{\"role\":\"assistant\",\"content\":\"6\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":1,\"total_tokens\":21,\"cost\":0.0000036,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.0000036,\"upstream_inference_prompt_cost\":0.000003,\"upstream_inference_completions_cost\":6e-7},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}"
25+
headers:
26+
Access-Control-Allow-Origin:
27+
- '*'
28+
CF-RAY:
29+
- 9e2909316898c52e-SJC
30+
Connection:
31+
- keep-alive
32+
Content-Type:
33+
- application/json
34+
Date:
35+
- Thu, 26 Mar 2026 20:51:53 GMT
36+
Permissions-Policy:
37+
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com"
38+
"https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
39+
Referrer-Policy:
40+
- no-referrer, strict-origin-when-cross-origin
41+
Server:
42+
- cloudflare
43+
Transfer-Encoding:
44+
- chunked
45+
X-Content-Type-Options:
46+
- nosniff
47+
content-length:
48+
- '785'
49+
status:
50+
code: 200
51+
message: OK
52+
version: 1
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"user","content":"What is 5+5? Reply with just the
4+
number."}],"model":"openai/gpt-4o-mini","max_tokens":10.0,"stream":true,"temperature":1.0,"top_p":1.0}'
5+
headers:
6+
Accept:
7+
- text/event-stream
8+
Accept-Encoding:
9+
- gzip, deflate
10+
Connection:
11+
- keep-alive
12+
Content-Length:
13+
- '174'
14+
Host:
15+
- openrouter.ai
16+
content-type:
17+
- application/json
18+
user-agent:
19+
- speakeasy-sdk/python 0.7.11 2.768.0 1.0.0 openrouter
20+
method: POST
21+
uri: https://openrouter.ai/api/v1/chat/completions
22+
response:
23+
body:
24+
string: ': OPENROUTER PROCESSING
25+
26+
27+
data: {"id":"gen-1774558312-UWJRiYAjZxsKV2WOReFJ","object":"chat.completion.chunk","created":1774558312,"model":"openai/gpt-4o-mini","provider":"OpenAI","system_fingerprint":"fp_f85b8886b6","choices":[{"index":0,"delta":{"content":"10","role":"assistant"},"finish_reason":null,"native_finish_reason":null}]}
28+
29+
30+
data: {"id":"gen-1774558312-UWJRiYAjZxsKV2WOReFJ","object":"chat.completion.chunk","created":1774558312,"model":"openai/gpt-4o-mini","provider":"OpenAI","system_fingerprint":"fp_f85b8886b6","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":"stop","native_finish_reason":"stop"}]}
31+
32+
33+
data: {"id":"gen-1774558312-UWJRiYAjZxsKV2WOReFJ","object":"chat.completion.chunk","created":1774558312,"model":"openai/gpt-4o-mini","provider":"OpenAI","system_fingerprint":"fp_f85b8886b6","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":"stop","native_finish_reason":"stop"}],"usage":{"prompt_tokens":20,"completion_tokens":1,"total_tokens":21,"cost":0.0000036,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"cache_write_tokens":0,"audio_tokens":0,"video_tokens":0},"cost_details":{"upstream_inference_cost":0.0000036,"upstream_inference_prompt_cost":0.000003,"upstream_inference_completions_cost":6e-7},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0,"audio_tokens":0}}}
34+
35+
36+
data: [DONE]
37+
38+
39+
'
40+
headers:
41+
Access-Control-Allow-Origin:
42+
- '*'
43+
CF-RAY:
44+
- 9e29092c8dbc552b-SJC
45+
Cache-Control:
46+
- no-cache
47+
Connection:
48+
- keep-alive
49+
Content-Type:
50+
- text/event-stream
51+
Date:
52+
- Thu, 26 Mar 2026 20:51:52 GMT
53+
Permissions-Policy:
54+
- payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com"
55+
"https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")
56+
Referrer-Policy:
57+
- no-referrer, strict-origin-when-cross-origin
58+
Server:
59+
- cloudflare
60+
Transfer-Encoding:
61+
- chunked
62+
X-Content-Type-Options:
63+
- nosniff
64+
status:
65+
code: 200
66+
message: OK
67+
version: 1

0 commit comments

Comments
 (0)