167 lines
5.5 KiB
Python
167 lines
5.5 KiB
Python
|
|
"""
|
||
|
|
Mock HTTP client for Braintrust integration testing.
|
||
|
|
|
||
|
|
This module intercepts Braintrust API calls and returns successful mock responses,
|
||
|
|
allowing full code execution without making actual network calls.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
Set BRAINTRUST_MOCK=true in environment variables or config to enable mock mode.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
from urllib.parse import urlparse
|
||
|
|
|
||
|
|
from litellm._logging import verbose_logger
|
||
|
|
from litellm.integrations.mock_client_factory import (
|
||
|
|
MockClientConfig,
|
||
|
|
MockResponse,
|
||
|
|
create_mock_client_factory,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Use factory for should_use_mock and MockResponse
|
||
|
|
# Braintrust uses both HTTPHandler (sync) and AsyncHTTPHandler (async)
|
||
|
|
# Braintrust needs endpoint-specific responses, so we use custom HTTPHandler.post patching
|
||
|
|
_config = MockClientConfig(
|
||
|
|
"BRAINTRUST",
|
||
|
|
"BRAINTRUST_MOCK",
|
||
|
|
default_latency_ms=100,
|
||
|
|
default_status_code=200,
|
||
|
|
default_json_data={"id": "mock-project-id", "status": "success"},
|
||
|
|
url_matchers=[
|
||
|
|
".braintrustdata.com",
|
||
|
|
"braintrustdata.com",
|
||
|
|
".braintrust.dev",
|
||
|
|
"braintrust.dev",
|
||
|
|
],
|
||
|
|
patch_async_handler=True, # Patch AsyncHTTPHandler.post for async calls
|
||
|
|
patch_sync_client=False, # HTTPHandler uses self.client.send(), not self.client.post()
|
||
|
|
patch_http_handler=False, # We use custom patching for endpoint-specific responses
|
||
|
|
)
|
||
|
|
|
||
|
|
# Get should_use_mock and create_mock_client from factory
|
||
|
|
# We need to call the factory's create_mock_client to patch AsyncHTTPHandler.post
|
||
|
|
(
|
||
|
|
create_mock_braintrust_factory_client,
|
||
|
|
should_use_braintrust_mock,
|
||
|
|
) = create_mock_client_factory(_config)
|
||
|
|
|
||
|
|
# Store original HTTPHandler.post method (Braintrust-specific for sync calls with custom logic)
|
||
|
|
_original_http_handler_post = None
|
||
|
|
_mocks_initialized = False
|
||
|
|
|
||
|
|
# Default mock latency in seconds
|
||
|
|
_MOCK_LATENCY_SECONDS = float(os.getenv("BRAINTRUST_MOCK_LATENCY_MS", "100")) / 1000.0
|
||
|
|
|
||
|
|
|
||
|
|
def _is_braintrust_url(url: str) -> bool:
|
||
|
|
"""Check if URL is a Braintrust API URL."""
|
||
|
|
if not isinstance(url, str):
|
||
|
|
return False
|
||
|
|
|
||
|
|
parsed = urlparse(url)
|
||
|
|
host = (parsed.hostname or "").lower()
|
||
|
|
|
||
|
|
if not host:
|
||
|
|
return False
|
||
|
|
|
||
|
|
return (
|
||
|
|
host == "braintrustdata.com"
|
||
|
|
or host.endswith(".braintrustdata.com")
|
||
|
|
or host == "braintrust.dev"
|
||
|
|
or host.endswith(".braintrust.dev")
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _mock_http_handler_post(
|
||
|
|
self,
|
||
|
|
url,
|
||
|
|
data=None,
|
||
|
|
json=None,
|
||
|
|
params=None,
|
||
|
|
headers=None,
|
||
|
|
timeout=None,
|
||
|
|
stream=False,
|
||
|
|
files=None,
|
||
|
|
content=None,
|
||
|
|
logging_obj=None,
|
||
|
|
):
|
||
|
|
"""Monkey-patched HTTPHandler.post that intercepts Braintrust calls with endpoint-specific responses."""
|
||
|
|
# Only mock Braintrust API calls
|
||
|
|
if isinstance(url, str) and _is_braintrust_url(url):
|
||
|
|
verbose_logger.info(f"[BRAINTRUST MOCK] POST to {url}")
|
||
|
|
time.sleep(_MOCK_LATENCY_SECONDS)
|
||
|
|
# Return appropriate mock response based on endpoint
|
||
|
|
if "/project" in url:
|
||
|
|
# Project creation/retrieval/register endpoint
|
||
|
|
project_name = json.get("name", "litellm") if json else "litellm"
|
||
|
|
mock_data = {"id": f"mock-project-id-{project_name}", "name": project_name}
|
||
|
|
elif "/project_logs" in url:
|
||
|
|
# Log insertion endpoint
|
||
|
|
mock_data = {"status": "success"}
|
||
|
|
else:
|
||
|
|
mock_data = _config.default_json_data
|
||
|
|
return MockResponse(
|
||
|
|
status_code=_config.default_status_code,
|
||
|
|
json_data=mock_data,
|
||
|
|
url=url,
|
||
|
|
elapsed_seconds=_MOCK_LATENCY_SECONDS,
|
||
|
|
)
|
||
|
|
if _original_http_handler_post is not None:
|
||
|
|
return _original_http_handler_post(
|
||
|
|
self,
|
||
|
|
url=url,
|
||
|
|
data=data,
|
||
|
|
json=json,
|
||
|
|
params=params,
|
||
|
|
headers=headers,
|
||
|
|
timeout=timeout,
|
||
|
|
stream=stream,
|
||
|
|
files=files,
|
||
|
|
content=content,
|
||
|
|
logging_obj=logging_obj,
|
||
|
|
)
|
||
|
|
raise RuntimeError("Original HTTPHandler.post not available")
|
||
|
|
|
||
|
|
|
||
|
|
def create_mock_braintrust_client():
|
||
|
|
"""
|
||
|
|
Monkey-patch HTTPHandler.post to intercept Braintrust sync calls.
|
||
|
|
|
||
|
|
Braintrust uses HTTPHandler for sync calls and AsyncHTTPHandler for async calls.
|
||
|
|
HTTPHandler.post uses self.client.send(), not self.client.post(), so we need
|
||
|
|
custom patching for sync (similar to Helicone).
|
||
|
|
AsyncHTTPHandler.post is patched by the factory.
|
||
|
|
|
||
|
|
We use custom patching instead of factory's patch_http_handler because we need
|
||
|
|
endpoint-specific responses (different for /project vs /project_logs).
|
||
|
|
|
||
|
|
This function is idempotent - it only initializes mocks once, even if called multiple times.
|
||
|
|
"""
|
||
|
|
global _original_http_handler_post, _mocks_initialized
|
||
|
|
|
||
|
|
if _mocks_initialized:
|
||
|
|
return
|
||
|
|
|
||
|
|
verbose_logger.debug("[BRAINTRUST MOCK] Initializing Braintrust mock client...")
|
||
|
|
|
||
|
|
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||
|
|
|
||
|
|
if _original_http_handler_post is None:
|
||
|
|
_original_http_handler_post = HTTPHandler.post
|
||
|
|
HTTPHandler.post = _mock_http_handler_post # type: ignore
|
||
|
|
verbose_logger.debug("[BRAINTRUST MOCK] Patched HTTPHandler.post")
|
||
|
|
|
||
|
|
# CRITICAL: Call the factory's initialization function to patch AsyncHTTPHandler.post
|
||
|
|
# This is required for async calls to be mocked
|
||
|
|
create_mock_braintrust_factory_client()
|
||
|
|
|
||
|
|
verbose_logger.debug(
|
||
|
|
f"[BRAINTRUST MOCK] Mock latency set to {_MOCK_LATENCY_SECONDS*1000:.0f}ms"
|
||
|
|
)
|
||
|
|
verbose_logger.debug(
|
||
|
|
"[BRAINTRUST MOCK] Braintrust mock client initialization complete"
|
||
|
|
)
|
||
|
|
|
||
|
|
_mocks_initialized = True
|