Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/adk-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
48 changes: 37 additions & 11 deletions .github/workflows/checks.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ jobs:
with:
fetch-depth: 0 # Fetch full history for proper diff
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -33,21 +33,20 @@ jobs:
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@70c4af2ed5282c51ba40566d026d6647852ffa3e # v5.0.1

build:
smoke:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -59,10 +58,33 @@ jobs:
run: |
# This is already done by make install-dev, but we're keeping this as a separate step
# to explicitly verify that installation works
mise exec python@${{ matrix.python-version }} -- python -m uv pip install -e ./py[all]
mise exec python@${{ matrix.python-version }} -- uv pip install -e ./py[all]
- name: Test whether the Python SDK can be imported
run: |
mise exec python@${{ matrix.python-version }} -- python -c 'import braintrust'
mise exec python@${{ matrix.python-version }} -- uv run --active --no-project python -c 'import braintrust'

nox:
runs-on: ${{ matrix.os }}
timeout-minutes: 30

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, windows-latest]
shard: [0, 1]

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
install_args: python@${{ matrix.python-version }}
- name: Install dependencies
run: |
mise exec python@${{ matrix.python-version }} -- make -C py install-dev
- name: Run nox tests (shard ${{ matrix.shard }}/2)
shell: bash
run: |
Expand All@@ -75,13 +97,15 @@ jobs:
uses: ./.github/workflows/langchain-py-test.yaml

upload-wheel:
needs: build
needs:
- smoke
- nox
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand All@@ -100,7 +124,8 @@ jobs:
needs:
- lint
- ensure-pinned-actions
- build
- smoke
- nox
- adk-py
- langchain-py
- upload-wheel
Expand All@@ -124,7 +149,8 @@ jobs:

check_result "lint" "${{ needs.lint.result }}"
check_result "ensure-pinned-actions" "${{ needs.ensure-pinned-actions.result }}"
check_result "build" "${{ needs.build.result }}"
check_result "smoke" "${{ needs.smoke.result }}"
check_result "nox" "${{ needs.nox.result }}"
check_result "adk-py" "${{ needs.adk-py.result }}"
check_result "langchain-py" "${{ needs.langchain-py.result }}"
check_result "upload-wheel" "${{ needs.upload-wheel.result }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/langchain-py-test.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -71,7 +71,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-publish-py-sdk.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,7 @@ jobs:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand DownExpand Up@@ -91,7 +91,7 @@ jobs:
ref: ${{ env.COMMIT_SHA }}
fetch-depth: 0
- name: Set up mise
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
cache: true
experimental: true
Expand Down
5 changes: 3 additions & 2 deletions py/noxfile.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,8 +86,9 @@ def _pinned_python_version():
OPENAI_VERSIONS = (LATEST, "1.77.0", "1.71", "1.91", "1.92")
OPENAI_AGENTS_VERSIONS = (LATEST, "0.0.19")
# litellm latest requires Python >= 3.10
# Pin litellm because 1.82.7-1.82.8 are compromised: https://github.com/BerriAI/litellm/issues/24512
LITELLM_VERSIONS = ("1.82.0", "1.74.0")
# Pin litellm to a version without the 1.82.7-1.82.8 compromise and with the
# OIDC userinfo cache key collision fix from 1.83.0+
LITELLM_VERSIONS = ("1.83.0", "1.74.0")
# CLI bundling started in 0.1.10 - older versions require external Claude Code installation
CLAUDE_AGENT_SDK_VERSIONS = (LATEST, "0.1.10")
# Keep LATEST for newest API coverage, and pin 2.4.0 to cover the 2.4 -> 2.5 breaking change
Expand Down
2 changes: 1 addition & 1 deletion py/requirements-optional.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ langchain-anthropic==1.4.0
langchain-core==1.2.22
langchain-openai==1.1.12
langsmith==0.7.12
litellm==1.82.0
litellm==1.83.0
openai==2.24.0
openrouter==0.7.11
pydantic_ai==1.66.0
19 changes: 15 additions & 4 deletions py/setup.py
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import os
import sysconfig

import setuptools

Expand DownExpand Up@@ -27,14 +28,24 @@
"wrapt",
]

is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
is_windows = os.name == "nt"

# orjson is not compatible with PyPy or free-threaded Python, so only expose it
# for standard CPython builds where it is supported.
performance_require = [] if is_free_threaded else ["orjson; platform_python_implementation != 'PyPy'"]

# temporalio does not currently install cleanly on Windows free-threaded Python,
# so leave the optional integration available everywhere else.
temporal_require = [] if is_free_threaded and is_windows else ["temporalio>=1.19.0; python_version>='3.10'"]

extras_require = {
"cli": ["boto3", "psycopg2-binary", "uv", "starlette", "uvicorn"],
"cli": ["boto3", "uv", "starlette", "uvicorn"],
"doc": ["pydoc-markdown"],
"openai-agents": ["openai-agents"],
"otel": ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp-proto-http"],
# orjson is not compatible with PyPy, so we exclude it for that platform
"performance": ["orjson; platform_python_implementation != 'PyPy'"],
"temporal": ["temporalio>=1.19.0; python_version>='3.10'"],
"performance": performance_require,
"temporal": temporal_require,
}

extras_require["all"] = sorted({package for packages in extras_require.values() for package in packages})
Expand Down
8 changes: 6 additions & 2 deletions py/src/braintrust/integrations/pydantic_ai/patchers.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,14 +134,18 @@ class StreamedResponseSyncStartProducerPatcher(FunctionWrapperPatcher):

class _ToolManagerExecuteFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.execute_function_tool"
target_module = "pydantic_ai._tool_manager"
# Regression compatibility note: pydantic_ai 1.78.0 moved ToolManager out
# of the private ``pydantic_ai._tool_manager`` module into
# ``pydantic_ai.tool_manager``. ``pydantic_ai._agent_graph.ToolManager`` is
# a stable alias in both the old and new layouts, so patch that seam.
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._execute_function_tool_call"
wrapper = _tool_manager_execute_function_tool_wrapper


class _ToolManagerCallFunctionToolPatcher(FunctionWrapperPatcher):
name = "pydantic_ai.tool_manager.call_function_tool"
target_module = "pydantic_ai._tool_manager"
target_module = "pydantic_ai._agent_graph"
target_path = "ToolManager._call_function_tool"
wrapper = _tool_manager_call_function_tool_wrapper
superseded_by = (_ToolManagerExecuteFunctionToolPatcher,)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -2248,7 +2248,7 @@ def test_wrap_model_classes_is_deprecated(monkeypatch):


def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
import pydantic_ai._tool_manager as tool_manager_module
import pydantic_ai._agent_graph as agent_graph_module
import pydantic_ai.direct as direct_module
from braintrust.integrations.pydantic_ai.integration import PydanticAIIntegration
from pydantic_ai.agent.abstract import AbstractAgent
Expand All@@ -2257,15 +2257,15 @@ def test_setup_pydantic_ai_is_idempotent_across_new_patch_points():
prepare_model = direct_module.__dict__["_prepare_model"]
tool_method_name = (
"_execute_function_tool_call"
if "_execute_function_tool_call" in tool_manager_module.ToolManager.__dict__
if "_execute_function_tool_call" in agent_graph_module.ToolManager.__dict__
else "_call_function_tool"
)
tool_method = tool_manager_module.ToolManager.__dict__[tool_method_name]
tool_method = agent_graph_module.ToolManager.__dict__[tool_method_name]

assert PydanticAIIntegration.setup() is True
assert AbstractAgent.__dict__["run"] is run
assert direct_module.__dict__["_prepare_model"] is prepare_model
assert tool_manager_module.ToolManager.__dict__[tool_method_name] is tool_method
assert agent_graph_module.ToolManager.__dict__[tool_method_name] is tool_method


def test_serialize_content_part_with_binary_content():
Expand Down
Loading