Files
2026-07-13 13:22:34 +08:00

1516 lines
59 KiB
Python

"""
Database-backed Gateway API endpoints for MLflow Server.
This module provides dynamic gateway endpoints that are configured from the database
rather than from a static YAML configuration file. It integrates the AI Gateway
functionality directly into the MLflow tracking server.
"""
import functools
import logging
import sys
import time
from collections.abc import AsyncIterable, Callable
from typing import Any
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from mlflow.entities.gateway_endpoint import GatewayModelLinkageType
from mlflow.exceptions import MlflowException
from mlflow.gateway.budget import check_budget_limit, make_budget_on_complete
from mlflow.gateway.config import (
AmazonBedrockConfig,
AnthropicConfig,
EndpointConfig,
EndpointType,
GatewayRequestType,
GeminiConfig,
LiteLLMConfig,
MistralConfig,
OpenAIAPIType,
OpenAIConfig,
Provider,
VertexAIConfig,
_AuthConfigKey,
_OpenAICompatibleConfig,
)
from mlflow.gateway.constants import MLFLOW_GATEWAY_CALLER_HEADER, GatewayCaller
from mlflow.gateway.guardrail_utils import (
extract_auth_headers,
load_guardrails,
run_post_llm_guardrails,
run_post_llm_guardrails_passthrough,
run_pre_llm_guardrails,
)
from mlflow.gateway.guardrails import (
_SANITIZE_BYPASS_HEADER,
GuardrailViolation,
JudgeGuardrail,
)
from mlflow.gateway.providers import get_provider
from mlflow.gateway.providers.base import (
PASSTHROUGH_ROUTES,
BaseProvider,
FallbackProvider,
PassthroughAction,
TrafficRouteProvider,
)
from mlflow.gateway.providers.utils import provider_call_duration_ms
from mlflow.gateway.schemas import chat, embeddings
from mlflow.gateway.tracing_utils import (
aggregate_anthropic_messages_stream_chunks,
aggregate_chat_stream_chunks,
aggregate_gemini_stream_generate_content_chunks,
aggregate_openai_responses_stream_chunks,
maybe_traced_gateway_call,
)
from mlflow.gateway.utils import safe_stream, to_sse_chunk, translate_http_exception
from mlflow.protos.databricks_pb2 import RESOURCE_DOES_NOT_EXIST
from mlflow.store.tracking.abstract_store import AbstractStore
from mlflow.store.tracking.gateway.config_resolver import get_endpoint_config
from mlflow.store.tracking.gateway.entities import (
GatewayEndpointConfig,
GatewayModelConfig,
RoutingStrategy,
)
from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
from mlflow.telemetry.events import GatewayInvocationEvent, GatewayInvocationType
from mlflow.telemetry.track import _record_event
from mlflow.tracing.constant import TraceMetadataKey
from mlflow.tracking._tracking_service.utils import _get_store
from mlflow.types.chat import ChatCompletionRequest
from mlflow.utils.provider_filter import is_provider_allowed, normalize_provider_name
from mlflow.utils.workspace_context import get_request_workspace
_logger = logging.getLogger(__name__)
gateway_router = APIRouter(prefix="/gateway", tags=["gateway"])
async def _get_request_body(request: Request) -> dict[str, Any]:
"""
Get request body, using cached version if available.
The auth middleware may have already parsed the request body for permission
validation. Since Starlette request body can only be read once, we cache
the parsed body in request.state.cached_body for reuse by route handlers.
Args:
request: The FastAPI Request object.
Returns:
Parsed JSON body as a dictionary.
Raises:
HTTPException: If the request body is not valid JSON.
"""
# Check if body was already parsed by auth middleware
cached_body = getattr(request.state, "cached_body", None)
if isinstance(cached_body, dict):
return cached_body
# Otherwise parse it now
try:
return await request.json()
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {e!s}")
def _get_user_metadata(request: Request) -> dict[str, Any]:
"""
Extract user metadata from request state for tracing.
The auth middleware stores the authenticated user's info in request.state.
Args:
request: The FastAPI Request object.
Returns:
Dictionary with user metadata (username and user_id if available).
"""
metadata = {}
if username := getattr(request.state, "username", None):
metadata[TraceMetadataKey.AUTH_USERNAME] = username
if user_id := getattr(request.state, "user_id", None):
metadata[TraceMetadataKey.AUTH_USER_ID] = str(user_id)
return metadata
def _record_gateway_invocation(invocation_type: GatewayInvocationType) -> Callable[..., Any]:
"""
Decorator for gateway invocation endpoints that records telemetry:
success/failure status, duration, streaming mode, and caller.
As a side effect, relays provider call duration to the gateway timing middleware by
writing `request.state.gateway_provider_duration_ms`. This is required because
Starlette's call_next() copies the ContextVar context for the handler task, so
mutations to provider_call_duration_ms don't propagate back to the middleware.
Timing headers (X-MLflow-Gateway-Duration-Ms, X-MLflow-Gateway-Overhead-Duration-Ms)
are injected by gateway_timing_middleware in fastapi_app.py.
Args:
invocation_type: The type of invocation endpoint.
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.perf_counter()
success = True
result = None
# Extract caller header from the Request object if present,
# only accepting known caller values to avoid logging arbitrary input.
caller = None
request = next((a for a in (*args, *kwargs.values()) if isinstance(a, Request)), None)
if request is not None:
raw_caller = request.headers.get(MLFLOW_GATEWAY_CALLER_HEADER)
if raw_caller in {e.value for e in GatewayCaller}:
caller = raw_caller
try:
result = await func(*args, **kwargs)
except Exception:
success = False
raise
finally:
duration_ms = int((time.perf_counter() - start_time) * 1000)
provider_duration = int(provider_call_duration_ms.get())
is_streaming = isinstance(result, StreamingResponse)
params = {
"is_streaming": is_streaming,
"invocation_type": invocation_type,
}
# provider_call_duration_ms is only updated by send_request()
# (non-streaming); send_stream_request() never sets it, so
# timing fields would always be 0 for streaming responses.
if not is_streaming:
params["provider_duration_ms"] = provider_duration
params["gateway_overhead_ms"] = max(0, duration_ms - provider_duration)
if caller:
params["caller"] = caller
if request is not None:
params["has_traceparent"] = request.headers.get("traceparent") is not None
auth_mod = sys.modules.get("mlflow.server.auth")
params["auth_enabled"] = auth_mod.is_auth_enabled() if auth_mod else False
if endpoint_id := getattr(request.state, "endpoint_id", None):
params["endpoint_id"] = endpoint_id
# Prefer the actual provider from the response (set by
# BaseProvider after the call) over the endpoint config
# estimate, which may not reflect traffic-split/fallback.
actual_provider = getattr(result, "provider", None)
if provider := (actual_provider or getattr(request.state, "provider", None)):
params["provider"] = provider
_record_event(
GatewayInvocationEvent,
params=params,
success=success,
duration_ms=duration_ms,
)
# Relay provider timing to the middleware via request.state.
# ContextVar values set in the handler task don't propagate back
# to the middleware task (Starlette copies the context for call_next).
if request is not None:
request.state.gateway_provider_duration_ms = int(
provider_call_duration_ms.get()
)
return result
return wrapper
return decorator
def _set_gateway_telemetry_state(request: Request, endpoint_config) -> None:
"""Set endpoint_id and provider on request.state for telemetry attribution."""
request.state.endpoint_id = endpoint_config.endpoint_id
if endpoint_config.models:
primary_model = next(
(
m
for m in endpoint_config.models
if m.linkage_type == GatewayModelLinkageType.PRIMARY
),
endpoint_config.models[0],
)
request.state.provider = str(primary_model.provider)
def _build_openai_compatible_config(model_config: "GatewayModelConfig"):
"""Build an _OpenAICompatibleConfig for providers that use the OpenAI API format."""
auth_config = model_config.auth_config or {}
return _OpenAICompatibleConfig(
api_key=model_config.secret_value.get(_AuthConfigKey.API_KEY),
api_base=auth_config.get(_AuthConfigKey.API_BASE),
)
def _build_endpoint_config(
endpoint_name: str,
model_config: GatewayModelConfig,
endpoint_type: EndpointType,
) -> EndpointConfig:
"""
Build an EndpointConfig from model configuration.
This function combines provider config building and endpoint config building
into a single operation.
Args:
endpoint_name: The endpoint name.
model_config: The model configuration object with decrypted secrets.
endpoint_type: Endpoint type (chat or embeddings).
Returns:
EndpointConfig instance ready for provider instantiation.
Raises:
MlflowException: If provider configuration is invalid.
"""
provider_name = model_config.provider
if not is_provider_allowed(provider_name):
_logger.debug(
"Provider '%s' blocked by MLFLOW_GATEWAY_ALLOWED_PROVIDERS",
provider_name,
)
raise MlflowException.invalid_parameter_value(
f"Provider '{provider_name}' is not allowed by the current gateway provider policy."
)
provider_config = None
if model_config.provider == Provider.OPENAI:
auth_config = model_config.auth_config or {}
openai_config = {
"openai_api_key": model_config.secret_value.get(_AuthConfigKey.API_KEY),
}
# Check if this is Azure OpenAI (requires api_type, deployment_name, api_base, api_version)
if "api_type" in auth_config and auth_config["api_type"] in ("azure", "azuread"):
openai_config["openai_api_type"] = auth_config["api_type"]
openai_config["openai_api_base"] = auth_config.get(_AuthConfigKey.API_BASE)
openai_config["openai_deployment_name"] = auth_config.get("deployment_name")
openai_config["openai_api_version"] = auth_config.get("api_version")
else:
# Standard OpenAI
if _AuthConfigKey.API_BASE in auth_config:
openai_config["openai_api_base"] = auth_config[_AuthConfigKey.API_BASE]
if "organization" in auth_config:
openai_config["openai_organization"] = auth_config["organization"]
provider_config = OpenAIConfig(**openai_config)
elif model_config.provider == Provider.AZURE:
auth_config = model_config.auth_config or {}
model_config.provider = Provider.OPENAI
provider_config = OpenAIConfig(
openai_api_type=OpenAIAPIType.AZURE,
openai_api_key=model_config.secret_value.get(_AuthConfigKey.API_KEY),
openai_api_base=auth_config.get(_AuthConfigKey.API_BASE),
openai_deployment_name=model_config.model_name,
openai_api_version=auth_config.get("api_version"),
)
elif model_config.provider == Provider.ANTHROPIC:
anthropic_config = {
"anthropic_api_key": model_config.secret_value.get(_AuthConfigKey.API_KEY),
}
if model_config.auth_config:
if "version" in model_config.auth_config:
anthropic_config["anthropic_version"] = model_config.auth_config["version"]
if _AuthConfigKey.API_BASE in model_config.auth_config:
anthropic_config["anthropic_api_base"] = model_config.auth_config[
_AuthConfigKey.API_BASE
]
provider_config = AnthropicConfig(**anthropic_config)
elif model_config.provider == Provider.MISTRAL:
provider_config = MistralConfig(
mistral_api_key=model_config.secret_value.get(_AuthConfigKey.API_KEY),
)
elif model_config.provider == Provider.GEMINI:
provider_config = GeminiConfig(
gemini_api_key=model_config.secret_value.get(_AuthConfigKey.API_KEY),
)
elif model_config.provider in {
Provider.GROQ,
Provider.DEEPSEEK,
Provider.XAI,
Provider.OPENROUTER,
Provider.OLLAMA,
Provider.PORTKEY,
}:
provider_config = _build_openai_compatible_config(model_config)
elif normalize_provider_name(model_config.provider) == Provider.DATABRICKS:
from mlflow.gateway.providers.databricks import DatabricksConfig
auth_config = model_config.auth_config or {}
auth_mode = auth_config.get(_AuthConfigKey.AUTH_MODE, "pat_token")
config_kwargs = {}
if api_base := auth_config.get(_AuthConfigKey.API_BASE):
config_kwargs["host"] = api_base
if auth_mode == "oauth_m2m":
config_kwargs["client_id"] = auth_config.get("client_id")
config_kwargs["client_secret"] = model_config.secret_value.get("client_secret")
else:
config_kwargs["token"] = model_config.secret_value.get(_AuthConfigKey.API_KEY)
provider_config = DatabricksConfig(**config_kwargs)
model_config.provider = Provider.DATABRICKS
elif normalize_provider_name(model_config.provider) == Provider.BEDROCK:
auth_config = model_config.auth_config or {}
auth_mode = auth_config.get(_AuthConfigKey.AUTH_MODE, "api_key")
if auth_mode == "api_key":
# Bearer token auth — bypasses boto3 SigV4
provider_config = AmazonBedrockConfig(
aws_config={
"aws_bearer_token": model_config.secret_value.get(_AuthConfigKey.API_KEY),
"aws_region": auth_config.get("aws_region_name"),
}
)
elif auth_mode == "access_keys":
provider_config = AmazonBedrockConfig(
aws_config={
"aws_access_key_id": model_config.secret_value.get("aws_access_key_id"),
"aws_secret_access_key": model_config.secret_value.get("aws_secret_access_key"),
"aws_region": auth_config.get("aws_region_name"),
}
)
elif auth_mode == "iam_role":
provider_config = AmazonBedrockConfig(
aws_config={
"aws_role_arn": auth_config.get("aws_role_name"),
"aws_region": auth_config.get("aws_region_name"),
}
)
else:
# default_chain — boto3 resolves credentials from the
# environment (env vars, ~/.aws/credentials, instance profile, etc.)
aws_config = {"aws_region": auth_config.get("aws_region_name")}
if role_arn := auth_config.get("aws_role_name"):
aws_config["aws_role_arn"] = role_arn
provider_config = AmazonBedrockConfig(aws_config=aws_config)
model_config.provider = Provider.BEDROCK
elif model_config.provider == Provider.VERTEX_AI:
auth_config = model_config.auth_config or {}
provider_config = VertexAIConfig(
vertex_project=auth_config.get("vertex_project"),
vertex_location=auth_config.get("vertex_location"),
vertex_credentials=model_config.secret_value.get("vertex_credentials"),
)
else:
# Use LiteLLM as fallback for unsupported providers
# Store the original provider name for LiteLLM's provider/model format
original_provider = model_config.provider
auth_config = model_config.auth_config or {}
# Merge auth_config with secret_value (secret_value contains api_key and other secrets)
litellm_config = {
"litellm_provider": original_provider,
"litellm_auth_config": auth_config | model_config.secret_value,
}
provider_config = LiteLLMConfig(**litellm_config)
model_config.provider = Provider.LITELLM
# Build and return EndpointConfig
return EndpointConfig(
name=endpoint_name,
endpoint_type=endpoint_type,
model={
"name": model_config.model_name,
"provider": model_config.provider,
"config": provider_config.model_dump(),
},
)
def _create_provider(
endpoint_config: GatewayEndpointConfig,
endpoint_type: EndpointType,
enable_tracing: bool = False,
) -> BaseProvider:
"""
Create a provider instance based on endpoint routing strategy.
Fallback is independent of routing strategy - if fallback_config is present,
the provider is wrapped with FallbackProvider.
Args:
endpoint_config: The endpoint configuration with model details and routing config.
endpoint_type: Endpoint type (chat or embeddings).
Returns:
Provider instance (standard provider, TrafficRouteProvider, or FallbackProvider).
Raises:
MlflowException: If endpoint configuration is invalid or has no models.
"""
# Get PRIMARY models
primary_models = [
model
for model in endpoint_config.models
if model.linkage_type == GatewayModelLinkageType.PRIMARY
]
if not primary_models:
raise MlflowException(
f"Endpoint '{endpoint_config.endpoint_name}' has no PRIMARY models configured",
error_code=RESOURCE_DOES_NOT_EXIST,
)
# Create base provider based on routing strategy
if endpoint_config.routing_strategy == RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT:
# Traffic split: distribute requests based on weights
configs = []
weights = []
for model_config in primary_models:
gateway_endpoint_config = _build_endpoint_config(
endpoint_name=endpoint_config.endpoint_name,
model_config=model_config,
endpoint_type=endpoint_type,
)
configs.append(gateway_endpoint_config)
weights.append(int(model_config.weight * 100)) # Convert to percentage
primary_provider = TrafficRouteProvider(
configs=configs,
traffic_splits=weights,
routing_strategy="TRAFFIC_SPLIT",
enable_tracing=enable_tracing,
)
else:
# Default: use the first PRIMARY model
model_config = primary_models[0]
gateway_endpoint_config = _build_endpoint_config(
endpoint_config.endpoint_name, model_config, endpoint_type
)
provider_class = get_provider(model_config.provider)
primary_provider = provider_class(gateway_endpoint_config, enable_tracing=enable_tracing)
# Wrap with FallbackProvider if fallback configuration exists
if endpoint_config.fallback_config:
fallback_models = [
model
for model in endpoint_config.models
if model.linkage_type == GatewayModelLinkageType.FALLBACK
]
if not fallback_models:
_logger.debug(
f"Endpoint '{endpoint_config.endpoint_name}' has fallback_config "
"but no FALLBACK models configured"
)
return primary_provider
# Sort fallback models by fallback_order
fallback_models.sort(
key=lambda m: m.fallback_order if m.fallback_order is not None else float("inf")
)
fallback_providers = [
get_provider(model_config.provider)(
_build_endpoint_config(
endpoint_name=endpoint_config.endpoint_name,
model_config=model_config,
endpoint_type=endpoint_type,
),
enable_tracing=enable_tracing,
)
for model_config in fallback_models
]
max_attempts = endpoint_config.fallback_config.max_attempts or len(fallback_models)
# FallbackProvider expects all providers (primary + fallback)
all_providers = [primary_provider] + fallback_providers
return FallbackProvider(
providers=all_providers,
max_attempts=max_attempts + 1, # +1 to include primary
strategy=endpoint_config.fallback_config.strategy,
enable_tracing=enable_tracing,
)
return primary_provider
def _create_provider_from_endpoint_name(
store: SqlAlchemyStore,
endpoint_name: str,
endpoint_type: EndpointType,
enable_tracing: bool = True,
) -> tuple[BaseProvider, GatewayEndpointConfig]:
"""
Create a provider from an endpoint name.
Args:
store: The SQLAlchemy store instance.
endpoint_name: The endpoint name.
endpoint_type: Endpoint type (chat or embeddings).
enable_tracing: If True, enables MLflow tracing for provider calls.
Returns:
Tuple of (provider instance, endpoint config)
"""
endpoint_config = get_endpoint_config(endpoint_name=endpoint_name, store=store)
return _create_provider(
endpoint_config, endpoint_type, enable_tracing=enable_tracing
), endpoint_config
def _validate_store(store: AbstractStore) -> None:
if not isinstance(store, SqlAlchemyStore):
raise HTTPException(
status_code=500,
detail="Gateway endpoints are only available with SqlAlchemyStore, "
f"got {type(store).__name__}.",
)
def _extract_endpoint_name_from_model(body: dict[str, Any]) -> str:
"""
Extract and validate the endpoint name from the 'model' parameter in the request body.
Args:
body: The request body dictionary
Returns:
The endpoint name extracted from the 'model' parameter
Raises:
HTTPException: If the 'model' parameter is missing
"""
endpoint_name = body.get("model")
if not endpoint_name:
raise HTTPException(
status_code=400,
detail="Missing required 'model' parameter in request body",
)
return endpoint_name
def _get_guardrails_and_auth(
store, endpoint_config, request: Request
) -> tuple[list[JudgeGuardrail], dict[str, str]]:
"""Load guardrails and extract auth headers, skipping guardrails for internal bypass calls."""
headers = dict(request.headers)
bypass = headers.get(_SANITIZE_BYPASS_HEADER) == "1"
guardrails = [] if bypass else load_guardrails(store, endpoint_config, request)
return guardrails, extract_auth_headers(headers)
@gateway_router.post("/{endpoint_name}/mlflow/invocations", response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.MLFLOW_INVOCATIONS)
async def invocations(endpoint_name: str, request: Request):
"""
Unified invocations endpoint handler that supports both chat and embeddings.
The handler automatically detects the request type based on the payload structure:
- If payload has "messages" field -> chat endpoint
- If payload has "input" field -> embeddings endpoint
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
headers = dict(request.headers)
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
endpoint_config = get_endpoint_config(endpoint_name=endpoint_name, store=store)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
# Detect request type based on payload structure
if "messages" in body:
# Chat request
endpoint_type = EndpointType.LLM_V1_CHAT
try:
payload = chat.RequestPayload(**body)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid chat payload: {e!s}")
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, endpoint_type
)
if payload.stream:
# Post-LLM guardrails are not applied to streaming responses.
# Pre-LLM guardrails run inside the trace as child spans; violations
# are surfaced as SSE error chunks via safe_stream.
async def _guarded_stream(
payload: chat.RequestPayload,
):
request_dict = await run_pre_llm_guardrails(
guardrails,
payload.model_dump(),
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
payload_schema=ChatCompletionRequest.model_json_schema(),
)
async for chunk in provider.chat_stream(chat.RequestPayload(**request_dict)):
yield chunk
stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
output_reducer=aggregate_chat_stream_chunks,
request_headers=headers,
request_type=GatewayRequestType.UNIFIED_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)(payload)
return StreamingResponse(
safe_stream(to_sse_chunk(chunk.model_dump_json()) async for chunk in stream),
media_type="text/event-stream",
)
else:
async def _guarded_chat(
payload: chat.RequestPayload,
) -> chat.ResponsePayload:
request_dict = await run_pre_llm_guardrails(
guardrails,
payload.model_dump(),
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
payload_schema=ChatCompletionRequest.model_json_schema(),
)
modified_payload = chat.RequestPayload(**request_dict)
response = await provider.chat(modified_payload)
return await run_post_llm_guardrails(
guardrails,
request_dict,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_chat,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.UNIFIED_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)(payload)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
elif "input" in body:
# Embeddings request
endpoint_type = EndpointType.LLM_V1_EMBEDDINGS
try:
payload = embeddings.RequestPayload(**body)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid embeddings payload: {e!s}")
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, endpoint_type
)
return await maybe_traced_gateway_call(
provider.embeddings,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.UNIFIED_EMBEDDINGS,
on_complete=make_budget_on_complete(store, workspace),
)(payload)
else:
raise HTTPException(
status_code=400,
detail="Invalid request: payload format must be either chat or embeddings",
)
@gateway_router.post("/mlflow/v1/chat/completions", response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.MLFLOW_CHAT_COMPLETIONS)
async def chat_completions(request: Request):
"""
OpenAI-compatible chat completions endpoint.
This endpoint follows the OpenAI API format where the endpoint name is specified
via the "model" parameter in the request body, allowing clients to use the
standard OpenAI SDK.
Example:
POST /gateway/mlflow/v1/chat/completions
{
"model": "my-endpoint-name",
"messages": [{"role": "user", "content": "Hello"}]
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
headers = dict(request.headers)
# Extract endpoint name from "model" parameter
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
try:
payload = chat.RequestPayload(**body)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid chat payload: {e!s}")
if payload.stream:
# Post-LLM guardrails are not applied to streaming responses.
# Pre-LLM guardrails run inside the trace as child spans; violations
# are surfaced as SSE error chunks via safe_stream.
async def _guarded_stream(
payload: chat.RequestPayload,
):
request_dict = await run_pre_llm_guardrails(
guardrails,
payload.model_dump(),
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
payload_schema=ChatCompletionRequest.model_json_schema(),
)
async for chunk in provider.chat_stream(chat.RequestPayload(**request_dict)):
yield chunk
stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
output_reducer=aggregate_chat_stream_chunks,
request_headers=headers,
request_type=GatewayRequestType.UNIFIED_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)(payload)
return StreamingResponse(
safe_stream(to_sse_chunk(chunk.model_dump_json()) async for chunk in stream),
media_type="text/event-stream",
)
else:
async def _guarded_chat(
payload: chat.RequestPayload,
) -> chat.ResponsePayload:
request_dict = await run_pre_llm_guardrails(
guardrails,
payload.model_dump(),
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
payload_schema=ChatCompletionRequest.model_json_schema(),
)
modified_payload = chat.RequestPayload(**request_dict)
response = await provider.chat(modified_payload)
return await run_post_llm_guardrails(
guardrails,
request_dict,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_chat,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.UNIFIED_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)(payload)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
@gateway_router.post(PASSTHROUGH_ROUTES[PassthroughAction.OPENAI_CHAT], response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.OPENAI_PASSTHROUGH_CHAT)
async def openai_passthrough_chat(request: Request):
"""
OpenAI passthrough endpoint for chat completions.
This endpoint accepts raw OpenAI API format and passes it through to the
OpenAI provider with the configured API key and model. The 'model' parameter
in the request specifies which MLflow endpoint to use.
Supports streaming responses when the 'stream' parameter is set to true.
Example:
POST /gateway/openai/v1/chat/completions
{
"model": "my-openai-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
"temperature": 0.7,
"stream": true
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
if body.get("stream", False):
# Post-LLM guardrails are not applied to streaming responses.
async def _guarded_stream(body: dict[str, Any]):
request_dict = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
stream = await provider.passthrough(
action=PassthroughAction.OPENAI_CHAT, payload=request_dict, headers=headers
)
async for chunk in stream:
yield chunk
traced_stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)
return StreamingResponse(
safe_stream(traced_stream(body), as_bytes=True), media_type="text/event-stream"
)
async def _guarded_passthrough(body: dict[str, Any]) -> dict[str, Any]:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
response = await provider.passthrough(
action=PassthroughAction.OPENAI_CHAT, payload=body, headers=headers
)
return await run_post_llm_guardrails_passthrough(
guardrails,
body,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_passthrough,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_CHAT,
on_complete=make_budget_on_complete(store, workspace),
)(body)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
@gateway_router.post(PASSTHROUGH_ROUTES[PassthroughAction.OPENAI_EMBEDDINGS], response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.OPENAI_PASSTHROUGH_EMBEDDINGS)
async def openai_passthrough_embeddings(request: Request):
"""
OpenAI passthrough endpoint for embeddings.
This endpoint accepts raw OpenAI API format and passes it through to the
OpenAI provider with the configured API key and model. The 'model' parameter
in the request specifies which MLflow endpoint to use.
Example:
POST /gateway/openai/v1/embeddings
{
"model": "my-openai-endpoint",
"input": "The food was delicious and the waiter..."
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_EMBEDDINGS
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
try:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
traced_passthrough = maybe_traced_gateway_call(
provider.passthrough,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_EMBEDDINGS,
on_complete=make_budget_on_complete(store, workspace),
)
# Post-LLM guardrails are skipped for embeddings: responses are float vectors
# that content judges cannot meaningfully evaluate.
return await traced_passthrough(
action=PassthroughAction.OPENAI_EMBEDDINGS, payload=body, headers=headers
)
async def _openai_responses_passthrough_unary(
request: Request,
body: dict[str, Any],
action: PassthroughAction,
request_type: GatewayRequestType,
) -> dict[str, Any]:
"""Shared body for the unary (non-streaming) OpenAI Responses-shaped
passthrough routes — the non-streaming branch of ``/responses`` and the
unary-only ``/responses/compact``.
Resolves the configured endpoint from ``body["model"]``, runs the usual
telemetry / budget / guardrail setup, wraps the upstream call in pre/post
guardrails, and returns the raw upstream response.
``GuardrailViolation`` is translated to HTTP 400. Each call site is
responsible for reading the request body (so callers can also reject
``stream=true`` when their endpoint doesn't support streaming, as
``/responses/compact`` does).
"""
user_metadata = _get_user_metadata(request)
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
async def _guarded_passthrough(body: dict[str, Any]) -> dict[str, Any]:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
response = await provider.passthrough(action=action, payload=body, headers=headers)
return await run_post_llm_guardrails_passthrough(
guardrails,
body,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_passthrough,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=request_type,
on_complete=make_budget_on_complete(store, workspace),
)(body)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
@gateway_router.post(PASSTHROUGH_ROUTES[PassthroughAction.OPENAI_RESPONSES], response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.OPENAI_PASSTHROUGH_RESPONSES)
async def openai_passthrough_responses(request: Request):
"""
OpenAI passthrough endpoint for the Responses API.
This endpoint accepts raw OpenAI Responses API format and passes it through to the
OpenAI provider with the configured API key and model. The 'model' parameter
in the request specifies which MLflow endpoint to use.
Supports streaming responses when the 'stream' parameter is set to true.
Example:
POST /gateway/openai/v1/responses
{
"model": "my-openai-endpoint",
"input": [{"type": "text", "text": "Hello"}],
"instructions": "You are a helpful assistant",
"stream": true
}
"""
body = await _get_request_body(request)
if body.get("stream", False):
# Post-LLM guardrails are not applied to streaming responses. Streaming
# keeps its own inline setup because the closure below captures these
# variables directly; the unary path delegates to the shared helper.
user_metadata = _get_user_metadata(request)
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
async def _guarded_stream(body: dict[str, Any]):
request_dict = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
stream = await provider.passthrough(
action=PassthroughAction.OPENAI_RESPONSES, payload=request_dict, headers=headers
)
async for chunk in stream:
yield chunk
traced_stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
output_reducer=aggregate_openai_responses_stream_chunks,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_RESPONSES,
on_complete=make_budget_on_complete(store, workspace),
)
return StreamingResponse(
safe_stream(traced_stream(body), as_bytes=True), media_type="text/event-stream"
)
return await _openai_responses_passthrough_unary(
request,
body,
action=PassthroughAction.OPENAI_RESPONSES,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_RESPONSES,
)
@gateway_router.post(
PASSTHROUGH_ROUTES[PassthroughAction.OPENAI_RESPONSES_COMPACT], response_model=None
)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.OPENAI_PASSTHROUGH_RESPONSES)
async def openai_passthrough_responses_compact(request: Request):
"""
OpenAI passthrough endpoint for the Responses API ``/compact`` route.
Mirrors :func:`openai_passthrough_responses` for session compaction calls
(e.g. those issued by the OpenAI Agents SDK's ``OpenAIResponsesCompactionSession``).
Compaction is a unary request — there is no streaming variant. A request
that sets ``stream=true`` is rejected with HTTP 400 to avoid the underlying
provider passthrough (which treats all non-embeddings actions as
stream-capable) attempting an SSE stream against an upstream endpoint that
does not support it.
Example:
POST /gateway/openai/v1/responses/compact
{
"model": "my-openai-endpoint",
"previous_response_id": "resp_abc123"
}
"""
body = await _get_request_body(request)
# /responses/compact is unary upstream; explicitly reject `stream=true`
# before the provider's passthrough machinery tries to open an SSE stream.
if body.get("stream", False):
raise HTTPException(
status_code=400,
detail=(
"stream=true is not supported on /responses/compact; compaction is a unary request."
),
)
return await _openai_responses_passthrough_unary(
request,
body,
action=PassthroughAction.OPENAI_RESPONSES_COMPACT,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_RESPONSES,
)
@gateway_router.post(PASSTHROUGH_ROUTES[PassthroughAction.ANTHROPIC_MESSAGES], response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.ANTHROPIC_PASSTHROUGH_MESSAGES)
async def anthropic_passthrough_messages(request: Request):
"""
Anthropic passthrough endpoint for the Messages API.
This endpoint accepts raw Anthropic API format and passes it through to the
Anthropic provider with the configured API key and model. The 'model' parameter
in the request specifies which MLflow endpoint to use.
Supports streaming responses when the 'stream' parameter is set to true.
Example:
POST /gateway/anthropic/v1/messages
{
"model": "my-anthropic-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 1024,
"stream": true
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
endpoint_name = _extract_endpoint_name_from_model(body)
body.pop("model")
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
if body.get("stream", False):
# Post-LLM guardrails are not applied to streaming responses.
async def _guarded_stream(body: dict[str, Any]):
request_dict = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
stream = await provider.passthrough(
action=PassthroughAction.ANTHROPIC_MESSAGES, payload=request_dict, headers=headers
)
async for chunk in stream:
yield chunk
traced_stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
output_reducer=aggregate_anthropic_messages_stream_chunks,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_ANTHROPIC_MESSAGES,
on_complete=make_budget_on_complete(store, workspace),
message_format="anthropic",
)
return StreamingResponse(
safe_stream(traced_stream(body), as_bytes=True), media_type="text/event-stream"
)
async def _guarded_passthrough(body: dict[str, Any]) -> dict[str, Any]:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
response = await provider.passthrough(
action=PassthroughAction.ANTHROPIC_MESSAGES, payload=body, headers=headers
)
return await run_post_llm_guardrails_passthrough(
guardrails,
body,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_passthrough,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_ANTHROPIC_MESSAGES,
on_complete=make_budget_on_complete(store, workspace),
message_format="anthropic",
)(body)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
@gateway_router.post(
PASSTHROUGH_ROUTES[PassthroughAction.GEMINI_GENERATE_CONTENT], response_model=None
)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.GEMINI_PASSTHROUGH_GENERATE_CONTENT)
async def gemini_passthrough_generate_content(endpoint_name: str, request: Request):
"""
Gemini passthrough endpoint for generateContent API (non-streaming).
This endpoint accepts raw Gemini API format and passes it through to the
Gemini provider with the configured API key. The endpoint_name in the URL path
specifies which MLflow endpoint to use.
Example:
POST /gateway/gemini/v1beta/models/my-gemini-endpoint:generateContent
{
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}]
}
]
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
async def _guarded_passthrough(body: dict[str, Any]) -> dict[str, Any]:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
response = await provider.passthrough(
action=PassthroughAction.GEMINI_GENERATE_CONTENT, payload=body, headers=headers
)
return await run_post_llm_guardrails_passthrough(
guardrails,
body,
response,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
try:
return await maybe_traced_gateway_call(
_guarded_passthrough,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_GEMINI_GENERATE_CONTENT,
on_complete=make_budget_on_complete(store, workspace),
message_format="gemini",
)(body)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
@gateway_router.post(
PASSTHROUGH_ROUTES[PassthroughAction.GEMINI_STREAM_GENERATE_CONTENT], response_model=None
)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.GEMINI_PASSTHROUGH_STREAM_GENERATE_CONTENT)
async def gemini_passthrough_stream_generate_content(endpoint_name: str, request: Request):
"""
Gemini passthrough endpoint for streamGenerateContent API (streaming).
This endpoint accepts raw Gemini API format and passes it through to the
Gemini provider with the configured API key. The endpoint_name in the URL path
specifies which MLflow endpoint to use.
Example:
POST /gateway/gemini/v1beta/models/my-gemini-endpoint:streamGenerateContent
{
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}]
}
]
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
# Post-LLM guardrails are not applied to streaming responses.
async def _guarded_stream(body: dict[str, Any]):
request_dict = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
stream = await provider.passthrough(
action=PassthroughAction.GEMINI_STREAM_GENERATE_CONTENT,
payload=request_dict,
headers=headers,
)
async for chunk in stream:
yield chunk
traced_stream = maybe_traced_gateway_call(
_guarded_stream,
endpoint_config,
user_metadata,
output_reducer=aggregate_gemini_stream_generate_content_chunks,
request_headers=headers,
request_type=GatewayRequestType.PASSTHROUGH_MODEL_GEMINI_GENERATE_CONTENT,
on_complete=make_budget_on_complete(store, workspace),
message_format="gemini",
)
return StreamingResponse(
safe_stream(traced_stream(body), as_bytes=True), media_type="text/event-stream"
)
@gateway_router.post("/proxy/{endpoint_name}/{path:path}", response_model=None)
@translate_http_exception
@_record_gateway_invocation(GatewayInvocationType.RAW_PROXY)
async def raw_proxy(endpoint_name: str, path: str, request: Request):
"""
Raw proxy endpoint.
Routes the request payload as-is to the upstream provider at
<provider_base_url>/<path>, using the credentials configured for the named
endpoint. Unlike the typed passthrough routes, the ``model`` field in the
payload is NOT replaced with the value from the endpoint config.
Streaming is detected automatically from the response Content-Type
(``text/event-stream`` or ``application/x-ndjson``), so this endpoint
supports providers like Gemini that signal streaming via Content-Type rather
than a ``"stream": true`` request flag.
Example:
POST /gateway/proxy/my-openai-endpoint/chat/completions
{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
}
"""
body = await _get_request_body(request)
user_metadata = _get_user_metadata(request)
store = _get_store()
workspace = get_request_workspace()
_validate_store(store)
headers = dict(request.headers)
# Preserve upstream query parameters (e.g. Gemini's ?alt=sse for SSE streaming).
if qs := request.url.query:
path = f"{path}?{qs}"
# The proxy route is type-agnostic; EndpointType is only used to satisfy
# EndpointConfig construction and has no effect on proxy behavior (the proxy
# bypasses typed routing entirely). LLM_V1_CHAT is used as the default since
# the DB does not store a per-endpoint type for DB-backed gateway endpoints.
provider, endpoint_config = _create_provider_from_endpoint_name(
store, endpoint_name, EndpointType.LLM_V1_CHAT
)
_set_gateway_telemetry_state(request, endpoint_config)
check_budget_limit(store, endpoint_config, workspace=workspace)
guardrails, auth_headers = _get_guardrails_and_auth(store, endpoint_config, request)
# _do_proxy is always an async generator so maybe_traced_gateway_call can wrap it
# consistently regardless of whether the upstream responds with a streaming or
# non-streaming body. For streaming responses it yields raw bytes; for
# non-streaming responses it yields a single dict. The route handler peeks at
# the first item to decide how to respond to the client.
async def _do_proxy(body: dict[str, Any]):
try:
body = await run_pre_llm_guardrails(
guardrails,
body,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
result = await provider.proxy(path, body, headers)
if isinstance(result, AsyncIterable):
# Post-LLM guardrails are not applied to streaming responses.
async for chunk in result:
yield chunk
else:
try:
result = await run_post_llm_guardrails_passthrough(
guardrails,
body,
result,
auth_headers=auth_headers,
usage_tracking=endpoint_config.usage_tracking,
)
except GuardrailViolation as e:
raise HTTPException(status_code=400, detail=str(e))
yield result
traced_proxy = maybe_traced_gateway_call(
_do_proxy,
endpoint_config,
user_metadata,
request_headers=headers,
request_type=GatewayRequestType.RAW_PROXY,
on_complete=make_budget_on_complete(store, workspace),
)
gen = traced_proxy(body)
try:
first = await gen.__anext__()
except StopAsyncIteration:
# _do_proxy raised before yielding (e.g. guardrail violation, provider 501).
# The original HTTPException already propagated; re-raise a generic 500 as fallback.
raise HTTPException(status_code=500, detail="Proxy handler exited without a response.")
if isinstance(first, bytes):
async def _prepend(first_chunk, rest):
yield first_chunk
async for chunk in rest:
yield chunk
return StreamingResponse(
safe_stream(_prepend(first, gen), as_bytes=True), media_type="text/event-stream"
)
return first