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

3659 lines
130 KiB
Python

import json
from pathlib import Path
from typing import Any
from unittest import mock
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from starlette.testclient import TestClient
import mlflow
from mlflow.entities import (
FallbackConfig,
FallbackStrategy,
GatewayEndpointModelConfig,
GatewayModelLinkageType,
RoutingStrategy,
SpanType,
)
from mlflow.entities.gateway_guardrail import GuardrailAction, GuardrailStage
from mlflow.entities.trace_state import TraceState
from mlflow.exceptions import MlflowException
from mlflow.gateway.config import (
EndpointType,
GatewayRequestType,
GeminiConfig,
LiteLLMConfig,
MistralConfig,
OpenAIAPIType,
OpenAIConfig,
)
from mlflow.gateway.constants import MLFLOW_GATEWAY_DURATION_HEADER, MLFLOW_GATEWAY_OVERHEAD_HEADER
from mlflow.gateway.guardrails import _SANITIZE_BYPASS_HEADER, JudgeGuardrail
from mlflow.gateway.providers.anthropic import AnthropicProvider
from mlflow.gateway.providers.base import (
FallbackProvider,
TrafficRouteProvider,
)
from mlflow.gateway.providers.databricks import DatabricksConfig, DatabricksProvider
from mlflow.gateway.providers.gemini import GeminiProvider
from mlflow.gateway.providers.litellm import LiteLLMProvider
from mlflow.gateway.providers.mistral import MistralProvider
from mlflow.gateway.providers.openai import OpenAIProvider
from mlflow.gateway.providers.utils import provider_call_duration_ms
from mlflow.gateway.schemas import chat, embeddings
from mlflow.server.fastapi_app import add_gateway_timing_middleware
from mlflow.server.gateway_api import (
_build_endpoint_config,
_create_provider_from_endpoint_name,
anthropic_passthrough_messages,
chat_completions,
gateway_router,
gemini_passthrough_generate_content,
gemini_passthrough_stream_generate_content,
invocations,
openai_passthrough_chat,
openai_passthrough_embeddings,
openai_passthrough_responses,
openai_passthrough_responses_compact,
)
from mlflow.store.tracking.gateway.entities import GatewayEndpointConfig, GatewayModelConfig
from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
from mlflow.tracing.client import TracingClient
from mlflow.tracing.constant import (
SpanAttributeKey,
TokenUsageKey,
TraceMetadataKey,
)
pytestmark = pytest.mark.notrackingurimock
TEST_PASSPHRASE = "test-passphrase-for-gateway-api-tests"
@pytest.fixture(autouse=True)
def set_kek_passphrase(monkeypatch):
monkeypatch.setenv("MLFLOW_CRYPTO_KEK_PASSPHRASE", TEST_PASSPHRASE)
@pytest.fixture
def store(tmp_path: Path, db_uri: str):
artifact_uri = tmp_path / "artifacts"
artifact_uri.mkdir(exist_ok=True)
mlflow.set_tracking_uri(db_uri)
yield SqlAlchemyStore(db_uri, artifact_uri.as_uri())
mlflow.set_tracking_uri(None)
def create_mock_request(
cached_body: dict[str, Any] | None = None,
username: str | None = None,
user_id: int | str | None = None,
) -> MagicMock:
"""Create a mock request with proper state attributes for gateway tests."""
mock_request = MagicMock()
mock_request.state.cached_body = cached_body
mock_request.state.username = username
mock_request.state.user_id = user_id
return mock_request
def _make_model_config(provider="openai", model_name="gpt-4o"):
return GatewayModelConfig(
model_definition_id="md-test",
provider=provider,
model_name=model_name,
secret_value={"api_key": "sk-test"},
)
def test_build_endpoint_config_rejects_provider_not_in_allowed_list(monkeypatch):
monkeypatch.setenv("MLFLOW_GATEWAY_ALLOWED_PROVIDERS", "anthropic")
with pytest.raises(MlflowException, match="not allowed"):
_build_endpoint_config("test-ep", _make_model_config("openai"), EndpointType.LLM_V1_CHAT)
def test_build_endpoint_config_allows_provider_in_allowed_list(monkeypatch):
monkeypatch.setenv("MLFLOW_GATEWAY_ALLOWED_PROVIDERS", "openai")
config = _build_endpoint_config(
"test-ep", _make_model_config("openai"), EndpointType.LLM_V1_CHAT
)
assert config.name == "test-ep"
assert isinstance(config.model.config, OpenAIConfig)
def test_build_endpoint_config_allows_provider_when_no_filter():
config = _build_endpoint_config(
"test-ep", _make_model_config("openai"), EndpointType.LLM_V1_CHAT
)
assert config.name == "test-ep"
def test_create_provider_from_endpoint_name_openai(store: SqlAlchemyStore):
# Create test data
secret = store.create_gateway_secret(
secret_name="openai-key",
secret_value={"api_key": "sk-test-123"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="gpt-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name="test-openai-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, OpenAIProvider)
assert isinstance(provider.config.model.config, OpenAIConfig)
assert provider.config.model.config.openai_api_key == "sk-test-123"
def test_create_provider_from_endpoint_name_azure_openai(store: SqlAlchemyStore):
# Test Azure OpenAI configuration
secret = store.create_gateway_secret(
secret_name="azure-openai-key",
secret_value={"api_key": "azure-api-key-test"},
provider="openai",
auth_config={
"api_type": "azure",
"api_base": "https://my-resource.openai.azure.com",
"api_version": "2024-02-01",
},
)
model_def = store.create_gateway_model_definition(
name="azure-gpt-model",
secret_id=secret.secret_id,
provider="azure",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-azure-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, OpenAIProvider)
assert isinstance(provider.config.model.config, OpenAIConfig)
assert provider.config.model.config.openai_api_type == OpenAIAPIType.AZURE
assert provider.config.model.config.openai_api_base == "https://my-resource.openai.azure.com"
assert provider.config.model.config.openai_deployment_name == "gpt-4"
assert provider.config.model.config.openai_api_version == "2024-02-01"
assert provider.config.model.config.openai_api_key == "azure-api-key-test"
def test_create_provider_from_endpoint_name_azure_openai_with_azuread(store: SqlAlchemyStore):
# Test Azure OpenAI with AzureAD authentication
secret = store.create_gateway_secret(
secret_name="azuread-openai-key",
secret_value={"api_key": "azuread-api-key-test"},
provider="openai",
auth_config={
"api_type": "azuread",
"api_base": "https://my-resource-ad.openai.azure.com",
"deployment_name": "gpt-4-deployment-ad",
"api_version": "2024-02-01",
},
)
model_def = store.create_gateway_model_definition(
name="azuread-gpt-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-azuread-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, OpenAIProvider)
assert isinstance(provider.config.model.config, OpenAIConfig)
assert provider.config.model.config.openai_api_type == OpenAIAPIType.AZUREAD
assert provider.config.model.config.openai_api_base == "https://my-resource-ad.openai.azure.com"
assert provider.config.model.config.openai_deployment_name == "gpt-4-deployment-ad"
assert provider.config.model.config.openai_api_version == "2024-02-01"
assert provider.config.model.config.openai_api_key == "azuread-api-key-test"
def test_create_provider_from_endpoint_name_anthropic(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="anthropic-key",
secret_value={"api_key": "sk-ant-test"},
provider="anthropic",
)
model_def = store.create_gateway_model_definition(
name="claude-model",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-sonnet",
)
endpoint = store.create_gateway_endpoint(
name="test-anthropic-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, AnthropicProvider)
assert provider.config.model.config.anthropic_api_key == "sk-ant-test"
assert provider.base_url == "https://api.anthropic.com/v1"
def test_create_provider_from_endpoint_name_anthropic_with_api_base(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="anthropic-proxy-key",
secret_value={"api_key": "sk-ant-proxy-test"},
provider="anthropic",
auth_config={"api_base": "http://localhost:6655/anthropic/v1"},
)
model_def = store.create_gateway_model_definition(
name="claude-proxy-model",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-7-sonnet",
)
endpoint = store.create_gateway_endpoint(
name="test-anthropic-proxy-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, AnthropicProvider)
assert provider.config.model.config.anthropic_api_key == "sk-ant-proxy-test"
assert provider.config.model.config.anthropic_api_base == "http://localhost:6655/anthropic/v1"
assert provider.base_url == "http://localhost:6655/anthropic/v1"
def test_create_provider_from_endpoint_name_mistral(store: SqlAlchemyStore):
# Test Mistral provider
secret = store.create_gateway_secret(
secret_name="mistral-key",
secret_value={"api_key": "mistral-test-key"},
provider="mistral",
)
model_def = store.create_gateway_model_definition(
name="mistral-model",
secret_id=secret.secret_id,
provider="mistral",
model_name="mistral-large-latest",
)
endpoint = store.create_gateway_endpoint(
name="test-mistral-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, MistralProvider)
assert isinstance(provider.config.model.config, MistralConfig)
assert provider.config.model.config.mistral_api_key == "mistral-test-key"
def test_create_provider_from_endpoint_name_gemini(store: SqlAlchemyStore):
# Test Gemini provider
secret = store.create_gateway_secret(
secret_name="gemini-key",
secret_value={"api_key": "gemini-test-key"},
provider="gemini",
)
model_def = store.create_gateway_model_definition(
name="gemini-model",
secret_id=secret.secret_id,
provider="gemini",
model_name="gemini-1.5-pro",
)
endpoint = store.create_gateway_endpoint(
name="test-gemini-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, GeminiProvider)
assert isinstance(provider.config.model.config, GeminiConfig)
assert provider.config.model.config.gemini_api_key == "gemini-test-key"
def test_create_provider_from_endpoint_name_litellm(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="litellm-key",
secret_value={"api_key": "litellm-test-key"},
provider="litellm",
)
model_def = store.create_gateway_model_definition(
name="litellm-model",
secret_id=secret.secret_id,
provider="litellm",
model_name="claude-3-5-sonnet-20241022",
)
endpoint = store.create_gateway_endpoint(
name="test-litellm-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, LiteLLMProvider)
assert isinstance(provider.config.model.config, LiteLLMConfig)
assert provider.config.model.config.litellm_auth_config["api_key"] == "litellm-test-key"
assert provider.config.model.config.litellm_provider == "litellm"
# get_provider_name() returns the actual provider name for tracing/metrics
assert provider.get_provider_name() == "litellm"
def test_create_provider_from_endpoint_name_litellm_with_api_base(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="litellm-custom-key",
secret_value={"api_key": "litellm-custom-key"},
provider="litellm",
auth_config={"api_base": "https://custom-api.example.com"},
)
model_def = store.create_gateway_model_definition(
name="litellm-custom-model",
secret_id=secret.secret_id,
provider="litellm",
model_name="custom-model",
)
endpoint = store.create_gateway_endpoint(
name="test-litellm-custom-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, LiteLLMProvider)
assert isinstance(provider.config.model.config, LiteLLMConfig)
assert provider.config.model.config.litellm_auth_config["api_key"] == "litellm-custom-key"
assert (
provider.config.model.config.litellm_auth_config["api_base"]
== "https://custom-api.example.com"
)
assert provider.config.model.config.litellm_provider == "litellm"
@pytest.mark.parametrize(
"input_url",
[
"https://my-workspace.databricks.com",
"https://my-workspace.databricks.com/serving-endpoints",
],
)
def test_create_provider_from_endpoint_name_databricks_normalizes_base_url(
store: SqlAlchemyStore, input_url: str
):
secret = store.create_gateway_secret(
secret_name="databricks-key",
secret_value={"api_key": "databricks-token-123"},
provider="databricks",
auth_config={"api_base": input_url},
)
model_def = store.create_gateway_model_definition(
name="databricks-model",
secret_id=secret.secret_id,
provider="databricks",
model_name="databricks-dbrx-instruct",
)
endpoint = store.create_gateway_endpoint(
name="test-databricks-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, DatabricksProvider)
assert isinstance(provider.config.model.config, DatabricksConfig)
# Verify the base URL was normalized to include /serving-endpoints
assert provider._api_base == "https://my-workspace.databricks.com/serving-endpoints"
def test_api_key_not_read_from_file(store: SqlAlchemyStore, tmp_path: Path, monkeypatch):
monkeypatch.delenv("MLFLOW_GATEWAY_RESOLVE_API_KEY_FROM_FILE", raising=False)
# Create a file whose path will be used as the "api_key" value
secret_file = tmp_path / "secret.txt"
secret_file.write_text("file-content-should-not-appear")
secret = store.create_gateway_secret(
secret_name="lfi-test-key",
# Use the file path as the api_key — the gateway must NOT read the file
secret_value={"api_key": str(secret_file)},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="lfi-test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name="lfi-test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
# The key must be the literal file path string, NOT the file contents
assert provider.config.model.config.openai_api_key == str(secret_file)
assert provider.config.model.config.openai_api_key != "file-content-should-not-appear"
def test_create_provider_from_endpoint_name_nonexistent_endpoint(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found"):
_create_provider_from_endpoint_name(store, "nonexistent-id", EndpointType.LLM_V1_CHAT)
@pytest.mark.asyncio
async def test_invocations_handler_chat(store: SqlAlchemyStore):
# Create test data
secret = store.create_gateway_secret(
secret_name="chat-key",
secret_value={"api_key": "sk-test-chat"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="chat-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="chat-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock the provider's chat method
mock_response = chat.ResponsePayload(
id="test-id",
object="chat.completion",
created=1234567890,
model="gpt-4",
choices=[
chat.Choice(
index=0,
message=chat.ResponseMessage(role="assistant", content="Hello!"),
finish_reason="stop",
)
],
usage=chat.ChatUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
# Create a mock request with chat payload
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"messages": [{"role": "user", "content": "Hi"}],
"temperature": 0.7,
"stream": False,
}
)
# Patch the provider creation to return a mocked provider
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
# Call the handler
response = await invocations(endpoint.name, mock_request)
# Verify
assert response.id == "test-id"
assert response.choices[0].message.content == "Hello!"
assert mock_provider.chat.called
@pytest.mark.asyncio
async def test_invocations_handler_embeddings(store: SqlAlchemyStore):
# Create test data
secret = store.create_gateway_secret(
secret_name="embed-key",
secret_value={"api_key": "sk-test-embed"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="embed-model",
secret_id=secret.secret_id,
provider="openai",
model_name="text-embedding-ada-002",
)
endpoint = store.create_gateway_endpoint(
name="embed-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock the provider's embeddings method
mock_response = embeddings.ResponsePayload(
object="list",
data=[embeddings.EmbeddingObject(embedding=[0.1, 0.2, 0.3], index=0)],
model="text-embedding-ada-002",
usage=embeddings.EmbeddingsUsage(prompt_tokens=5, total_tokens=5),
)
# Create a mock request with embeddings payload
mock_request = create_mock_request()
mock_request.json = AsyncMock(return_value={"input": "test text"})
# Patch the provider creation to return a mocked provider
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_provider.embeddings = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
# Call the handler
response = await invocations(endpoint.name, mock_request)
# Verify
assert response.object == "list"
assert len(response.data) == 1
assert response.data[0].embedding == [0.1, 0.2, 0.3]
assert mock_provider.embeddings.called
def test_gateway_router_initialization():
assert gateway_router is not None
assert gateway_router.prefix == "/gateway"
@pytest.mark.asyncio
async def test_invocations_handler_invalid_json(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock request that raises exception when parsing JSON
mock_request = create_mock_request()
mock_request.json = AsyncMock(side_effect=ValueError("Invalid JSON"))
with pytest.raises(HTTPException, match="Invalid JSON payload: Invalid JSON") as exc_info:
await invocations(endpoint.name, mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_invocations_handler_missing_fields(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create request with neither messages nor input
mock_request = create_mock_request()
mock_request.json = AsyncMock(return_value={"temperature": 0.7})
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
with pytest.raises(
HTTPException, match="Invalid request: payload format must be either chat or embeddings"
) as exc_info:
await invocations(endpoint.name, mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_invocations_handler_invalid_chat_payload(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create request with invalid messages structure
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"messages": "not a list", # Should be a list
"stream": False,
}
)
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
with pytest.raises(HTTPException, match="Invalid chat payload") as exc_info:
await invocations(endpoint.name, mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_invocations_handler_invalid_embeddings_payload(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="text-embedding-ada-002",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create request with invalid input structure
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"input": 123, # Should be string or list of strings
}
)
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
with pytest.raises(HTTPException, match="Invalid embeddings payload") as exc_info:
await invocations(endpoint.name, mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_invocations_handler_streaming(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create streaming request
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"messages": [{"role": "user", "content": "Hi"}],
"stream": True,
}
)
# Mock streaming chunks
async def mock_stream():
yield chat.StreamResponsePayload(
id="test-id",
object="chat.completion.chunk",
created=1234567890,
model="gpt-4",
choices=[
chat.StreamChoice(
index=0,
delta=chat.StreamDelta(role="assistant", content="Hello"),
finish_reason=None,
)
],
)
with patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider:
mock_provider = MagicMock()
mock_provider.chat_stream = MagicMock(return_value=mock_stream())
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
response = await invocations(endpoint.name, mock_request)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
# chat_stream is inside a lazy async generator; consume the body to trigger execution
async for _ in response.body_iterator:
pass
assert mock_provider.chat_stream.called
def test_create_provider_from_endpoint_name_no_models(store: SqlAlchemyStore):
# Create a minimal endpoint to get an endpoint_name
secret = store.create_gateway_secret(
secret_name="test-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock get_endpoint_config to return an empty models list
with patch(
"mlflow.server.gateway_api.get_endpoint_config",
return_value=GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name="test-endpoint", models=[]
),
):
with pytest.raises(MlflowException, match="has no PRIMARY models configured"):
_create_provider_from_endpoint_name(store, endpoint.name, EndpointType.LLM_V1_CHAT)
# =============================================================================
# OpenAI-compatible chat completions endpoint tests
# =============================================================================
@pytest.mark.asyncio
async def test_chat_completions_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-compat-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-compat-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
store.create_gateway_endpoint(
name="my-chat-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock the provider's chat method
mock_response = chat.ResponsePayload(
id="test-id",
object="chat.completion",
created=1234567890,
model="gpt-4",
choices=[
chat.Choice(
index=0,
message=chat.ResponseMessage(role="assistant", content="Hello from OpenAI!"),
finish_reason="stop",
)
],
usage=chat.ChatUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
# Create a mock request with OpenAI-compatible format
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "my-chat-endpoint", # Endpoint name via model parameter
"messages": [{"role": "user", "content": "Hi"}],
"temperature": 0.7,
"stream": False,
}
)
# Patch the provider creation to return a mocked provider
with (
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
patch("mlflow.server.gateway_api.load_guardrails", return_value=[]),
):
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id="test-endpoint-id", endpoint_name="my-chat-endpoint", models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
# Call the handler
response = await chat_completions(mock_request)
# Verify
assert response.id == "test-id"
assert response.choices[0].message.content == "Hello from OpenAI!"
assert mock_provider.chat.called
def test_response_timing_headers(store: SqlAlchemyStore):
app = FastAPI()
app.include_router(gateway_router)
add_gateway_timing_middleware(app)
mock_response = chat.ResponsePayload(
id="test-id",
object="chat.completion",
created=1234567890,
model="gpt-4",
choices=[
chat.Choice(
index=0,
message=chat.ResponseMessage(role="assistant", content="Hello!"),
finish_reason="stop",
)
],
usage=chat.ChatUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id="test-endpoint-id", endpoint_name="my-endpoint", models=[]
)
async def _mock_chat_with_provider_timing(payload):
# Simulate a real provider call by setting the ContextVar as send_request would.
provider_call_duration_ms.set(50.0)
return mock_response
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch("mlflow.server.gateway_api.get_request_workspace", return_value=None),
patch("mlflow.server.gateway_api.check_budget_limit"),
patch("mlflow.server.gateway_api.load_guardrails", return_value=[]),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat = _mock_chat_with_provider_timing
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
client = TestClient(app)
response = client.post(
"/gateway/mlflow/v1/chat/completions",
json={"model": "my-endpoint", "messages": [{"role": "user", "content": "Hi"}]},
)
assert response.status_code == 200
duration = int(response.headers[MLFLOW_GATEWAY_DURATION_HEADER])
overhead = int(response.headers[MLFLOW_GATEWAY_OVERHEAD_HEADER])
assert duration >= 0
assert 0 <= overhead <= duration
def test_response_timing_headers_streaming(store: SqlAlchemyStore):
app = FastAPI()
app.include_router(gateway_router)
add_gateway_timing_middleware(app)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id="test-endpoint-id", endpoint_name="my-endpoint", models=[]
)
async def _mock_chat_stream(payload):
yield chat.StreamResponsePayload(
id="test-id",
object="chat.completion.chunk",
created=1234567890,
model="gpt-4",
choices=[
chat.StreamChoice(
index=0,
delta=chat.StreamDelta(role="assistant", content="Hello"),
finish_reason=None,
)
],
)
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch("mlflow.server.gateway_api.get_request_workspace", return_value=None),
patch("mlflow.server.gateway_api.check_budget_limit"),
patch("mlflow.server.gateway_api.load_guardrails", return_value=[]),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat_stream = MagicMock(return_value=_mock_chat_stream(None))
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
client = TestClient(app)
response = client.post(
"/gateway/mlflow/v1/chat/completions",
json={
"model": "my-endpoint",
"messages": [{"role": "user", "content": "Hi"}],
"stream": True,
},
)
assert response.status_code == 200
assert MLFLOW_GATEWAY_DURATION_HEADER in response.headers
assert int(response.headers[MLFLOW_GATEWAY_DURATION_HEADER]) >= 0
# Overhead header is omitted for streaming since provider_call_duration_ms is not set.
assert MLFLOW_GATEWAY_OVERHEAD_HEADER not in response.headers
def test_response_timing_headers_error(store: SqlAlchemyStore):
app = FastAPI()
app.include_router(gateway_router)
add_gateway_timing_middleware(app)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id="test-endpoint-id", endpoint_name="my-error-endpoint", models=[]
)
async def _mock_chat_raises(payload):
provider_call_duration_ms.set(30.0)
raise HTTPException(status_code=502, detail="Upstream provider error")
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch("mlflow.server.gateway_api.get_request_workspace", return_value=None),
patch("mlflow.server.gateway_api.check_budget_limit"),
patch("mlflow.server.gateway_api.load_guardrails", return_value=[]),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat = _mock_chat_raises
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
client = TestClient(app)
response = client.post(
"/gateway/mlflow/v1/chat/completions",
json={"model": "my-error-endpoint", "messages": [{"role": "user", "content": "Hi"}]},
)
assert response.status_code == 502
assert MLFLOW_GATEWAY_DURATION_HEADER in response.headers
duration = int(response.headers[MLFLOW_GATEWAY_DURATION_HEADER])
assert duration >= 0
assert MLFLOW_GATEWAY_OVERHEAD_HEADER in response.headers
overhead = int(response.headers[MLFLOW_GATEWAY_OVERHEAD_HEADER])
assert 0 <= overhead <= duration
@pytest.mark.asyncio
async def test_chat_completions_endpoint_streaming(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="stream-key",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="stream-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
store.create_gateway_endpoint(
name="stream-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create streaming request
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "stream-endpoint",
"messages": [{"role": "user", "content": "Hi"}],
"stream": True,
}
)
# Mock streaming chunks
async def mock_stream():
yield chat.StreamResponsePayload(
id="test-id",
object="chat.completion.chunk",
created=1234567890,
model="gpt-4",
choices=[
chat.StreamChoice(
index=0,
delta=chat.StreamDelta(role="assistant", content="Hello"),
finish_reason=None,
)
],
)
with (
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
patch("mlflow.server.gateway_api.load_guardrails", return_value=[]),
):
mock_provider = MagicMock()
mock_provider.chat_stream = MagicMock(return_value=mock_stream())
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id="test-endpoint-id", endpoint_name="stream-endpoint", models=[]
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
response = await chat_completions(mock_request)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
# chat_stream is inside a lazy async generator; consume the body to trigger execution
async for _ in response.body_iterator:
pass
assert mock_provider.chat_stream.called
@pytest.mark.asyncio
async def test_chat_completions_endpoint_missing_model_parameter(store: SqlAlchemyStore):
# Create request without model parameter
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"messages": [{"role": "user", "content": "Hi"}],
}
)
with pytest.raises(HTTPException, match="Missing required 'model' parameter") as exc_info:
await chat_completions(mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_chat_completions_endpoint_missing_messages(store: SqlAlchemyStore):
# Create test endpoint first so we can test payload validation
secret = store.create_gateway_secret(
secret_name="chat-missing-msg-key",
secret_value={"api_key": "sk-test-key"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="gpt-missing-msg-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
store.create_gateway_endpoint(
name="my-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create request without messages
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "my-endpoint",
"temperature": 0.7,
}
)
with pytest.raises(HTTPException, match="Invalid chat payload") as exc_info:
await chat_completions(mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_chat_completions_endpoint_invalid_json(store: SqlAlchemyStore):
mock_request = create_mock_request()
mock_request.json = AsyncMock(side_effect=ValueError("Invalid JSON"))
with pytest.raises(HTTPException, match="Invalid JSON payload: Invalid JSON") as exc_info:
await chat_completions(mock_request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_openai_passthrough_chat(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-passthrough-key",
secret_value={"api_key": "sk-test-passthrough"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-passthrough-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
store.create_gateway_endpoint(
name="openai-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock OpenAI API response
mock_response = {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello from passthrough!"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
# Create mock request
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-passthrough-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
}
)
# Mock send_request directly
with mock.patch(
"mlflow.gateway.providers.openai.send_request", return_value=mock_response
) as mock_send:
response = await openai_passthrough_chat(mock_request)
# Verify send_request was called
assert mock_send.called
call_kwargs = mock_send.call_args[1]
assert call_kwargs["path"] == "chat/completions"
assert call_kwargs["payload"]["model"] == "gpt-4o"
assert call_kwargs["payload"]["messages"] == [{"role": "user", "content": "Hello"}]
# Verify response is raw OpenAI format
assert response["id"] == "chatcmpl-123"
assert response["model"] == "gpt-4o"
assert response["choices"][0]["message"]["content"] == "Hello from passthrough!"
@pytest.mark.asyncio
async def test_openai_passthrough_embeddings(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-embed-passthrough-key",
secret_value={"api_key": "sk-test-embed"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-embed-passthrough-model",
secret_id=secret.secret_id,
provider="openai",
model_name="text-embedding-3-small",
)
store.create_gateway_endpoint(
name="openai-embed-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock OpenAI API response
mock_response = {
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "text-embedding-3-small",
"usage": {"prompt_tokens": 5, "total_tokens": 5},
}
# Create mock request
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-embed-passthrough-endpoint",
"input": "Test input",
}
)
# Mock send_request directly
with mock.patch(
"mlflow.gateway.providers.openai.send_request", return_value=mock_response
) as mock_send:
response = await openai_passthrough_embeddings(mock_request)
# Verify send_request was called
assert mock_send.called
call_kwargs = mock_send.call_args[1]
assert call_kwargs["path"] == "embeddings"
assert call_kwargs["payload"]["model"] == "text-embedding-3-small"
assert call_kwargs["payload"]["input"] == "Test input"
# Verify response is raw OpenAI format
assert response["model"] == "text-embedding-3-small"
assert response["data"][0]["embedding"] == [0.1, 0.2, 0.3]
@pytest.mark.asyncio
async def test_openai_passthrough_responses(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-responses-key",
secret_value={"api_key": "sk-test-responses"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-responses-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
store.create_gateway_endpoint(
name="openai-responses-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock OpenAI Responses API response (using correct Responses API schema)
mock_response = {
"id": "resp-123",
"object": "response",
"created": 1234567890,
"model": "gpt-4o",
"status": "completed",
"output": [
{
"role": "assistant",
"content": [{"type": "output_text", "text": "Response from Responses API"}],
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
# Create mock request
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-responses-endpoint",
"input": [{"role": "user", "content": "Hello"}],
"instructions": "You are a helpful assistant",
"response_format": {"type": "text"},
}
)
# Mock send_request directly
with mock.patch(
"mlflow.gateway.providers.openai.send_request", return_value=mock_response
) as mock_send:
response = await openai_passthrough_responses(mock_request)
# Verify send_request was called
assert mock_send.called
call_kwargs = mock_send.call_args[1]
assert call_kwargs["path"] == "responses"
assert call_kwargs["payload"]["model"] == "gpt-4o"
assert call_kwargs["payload"]["input"] == [{"role": "user", "content": "Hello"}]
assert call_kwargs["payload"]["instructions"] == "You are a helpful assistant"
assert call_kwargs["payload"]["response_format"] == {"type": "text"}
# Verify response is raw OpenAI Responses API format
assert response["id"] == "resp-123"
assert response["object"] == "response"
assert response["status"] == "completed"
assert response["output"][0]["content"][0]["text"] == "Response from Responses API"
@pytest.mark.asyncio
async def test_openai_passthrough_responses_compact(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-responses-compact-key",
secret_value={"api_key": "sk-test-responses-compact"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-responses-compact-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
store.create_gateway_endpoint(
name="openai-responses-compact-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Mock OpenAI Responses-compact response: same shape as /responses output
mock_response = {
"id": "resp-compact-123",
"object": "response",
"created": 1234567890,
"model": "gpt-4o",
"status": "completed",
"output": [
{
"role": "assistant",
"content": [{"type": "output_text", "text": "Compacted summary"}],
}
],
"usage": {"input_tokens": 50, "output_tokens": 10, "total_tokens": 60},
}
# Compaction request — typical body carries `previous_response_id` and `model`
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-responses-compact-endpoint",
"previous_response_id": "resp_abc123",
}
)
with mock.patch(
"mlflow.gateway.providers.openai.send_request", return_value=mock_response
) as mock_send:
response = await openai_passthrough_responses_compact(mock_request)
# Verify send_request was called with the /compact upstream path
assert mock_send.called
call_kwargs = mock_send.call_args[1]
assert call_kwargs["path"] == "responses/compact"
assert call_kwargs["payload"]["model"] == "gpt-4o"
assert call_kwargs["payload"]["previous_response_id"] == "resp_abc123"
# Verify response is the raw OpenAI Responses-compact format
assert response["id"] == "resp-compact-123"
assert response["object"] == "response"
assert response["status"] == "completed"
assert response["output"][0]["content"][0]["text"] == "Compacted summary"
@pytest.mark.asyncio
async def test_openai_passthrough_responses_compact_rejects_stream():
"""``/responses/compact`` is unary upstream; the handler must reject a
client-supplied ``stream=true`` with HTTP 400 before invoking the provider
(whose passthrough machinery treats all non-embeddings actions as
stream-capable and would otherwise open an SSE stream against an upstream
endpoint that does not support it).
"""
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-responses-compact-endpoint",
"previous_response_id": "resp_abc123",
"stream": True,
}
)
# send_request should never be called — the handler rejects before
# reaching the provider.
with (
mock.patch("mlflow.gateway.providers.openai.send_request") as mock_send,
pytest.raises(HTTPException, match="stream=true is not supported") as exc_info,
):
await openai_passthrough_responses_compact(mock_request)
assert exc_info.value.status_code == 400
assert not mock_send.called
@pytest.mark.asyncio
async def test_openai_passthrough_chat_streaming(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-stream-passthrough-key",
secret_value={"api_key": "sk-test-stream"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-stream-passthrough-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
store.create_gateway_endpoint(
name="openai-stream-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create mock request with streaming enabled
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-stream-passthrough-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
"stream": True,
}
)
# Mock streaming response chunks
mock_stream_chunks = [
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}\n\n', # noqa: E501
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]}\n\n', # noqa: E501
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', # noqa: E501
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.openai.send_stream_request",
return_value=mock_stream_generator(),
) as mock_send_stream:
response = await openai_passthrough_chat(mock_request)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
chunks = [chunk async for chunk in response.body_iterator]
assert mock_send_stream.called
assert len(chunks) == 3
assert b"Hello" in chunks[0]
assert b"world" in chunks[1]
assert b"stop" in chunks[2]
@pytest.mark.asyncio
async def test_openai_passthrough_responses_streaming(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-responses-stream-key",
secret_value={"api_key": "sk-test-responses-stream"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-responses-stream-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
store.create_gateway_endpoint(
name="openai-responses-stream-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create mock request with streaming enabled
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "openai-responses-stream-endpoint",
"input": [{"type": "text", "text": "Hello"}],
"instructions": "You are a helpful assistant",
"stream": True,
}
)
# Mock streaming response chunks for Responses API
mock_stream_chunks = [
b'data: {"type":"response.created","response":{"id":"resp_1","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}\n\n', # noqa: E501
b'data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_1","type":"message","status":"in_progress","role":"assistant","content":[]}}\n\n', # noqa: E501
b'data: {"type":"response.content_part.added","item_id":"msg_1","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n', # noqa: E501
b'data: {"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":"Hi"}\n\n', # noqa: E501
b'data: {"type":"response.output_text.done","item_id":"msg_1","output_index":0,"content_index":0,"text":"Hi there! How can I assist you today?"}\n\n', # noqa: E501
b'data: {"type":"response.content_part.done","item_id":"msg_1","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}}\n\n', # noqa: E501
b'data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}}\n\n', # noqa: E501
b'data: {"type":"response.completed","response":{"id":"resp_1","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4.1-2025-04-14","output":[{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}}\n\n', # noqa: E501
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.openai.send_stream_request",
return_value=mock_stream_generator(),
) as mock_send_stream:
response = await openai_passthrough_responses(mock_request)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
chunks = [chunk async for chunk in response.body_iterator]
assert mock_send_stream.called
assert len(chunks) == 8
assert b"response.created" in chunks[0]
assert b"response.output_item.added" in chunks[1]
assert b"response.content_part.added" in chunks[2]
assert b"response.output_text.delta" in chunks[3]
assert b"response.output_text.done" in chunks[4]
assert b"response.content_part.done" in chunks[5]
assert b"response.output_item.done" in chunks[6]
assert b"response.completed" in chunks[7]
# =============================================================================
# Anthropic Messages API passthrough endpoint tests
# =============================================================================
@pytest.mark.asyncio
async def test_anthropic_passthrough_messages(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="anthropic-passthrough-key",
secret_value={"api_key": "sk-ant-test"},
provider="anthropic",
)
model_def = store.create_gateway_model_definition(
name="anthropic-passthrough-model",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
store.create_gateway_endpoint(
name="anthropic-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "anthropic-passthrough-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 1024,
}
)
mock_response = {
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hello! How can I assist you today?"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 10, "output_tokens": 20},
}
with mock.patch(
"mlflow.gateway.providers.anthropic.send_request", return_value=mock_response
) as mock_send:
response = await anthropic_passthrough_messages(mock_request)
assert mock_send.called
call_args = mock_send.call_args
assert call_args[1]["path"] == "messages"
assert call_args[1]["payload"]["model"] == "claude-3-5-sonnet-20241022"
assert call_args[1]["payload"]["messages"] == [{"role": "user", "content": "Hello"}]
assert call_args[1]["payload"]["max_tokens"] == 1024
assert response["id"] == "msg_01XFDUDYJgAACzvnptvVoYEL"
assert response["model"] == "claude-3-5-sonnet-20241022"
assert response["content"][0]["text"] == "Hello! How can I assist you today?"
@pytest.mark.asyncio
async def test_anthropic_passthrough_messages_streaming(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="anthropic-stream-passthrough-key",
secret_value={"api_key": "sk-ant-test-stream"},
provider="anthropic",
)
model_def = store.create_gateway_model_definition(
name="anthropic-stream-passthrough-model",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
store.create_gateway_endpoint(
name="anthropic-stream-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": "anthropic-stream-passthrough-endpoint",
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 1024,
"stream": True,
}
)
mock_stream_chunks = [
b'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_01XFDUDYJgAACzvnptvVoYEL","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet-20241022","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0}}}\n\n', # noqa: E501
b'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n', # noqa: E501
b'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n', # noqa: E501
b'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}}\n\n', # noqa: E501
b'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
b'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":20}}\n\n', # noqa: E501
b'event: message_stop\ndata: {"type":"message_stop"}\n\n',
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.anthropic.send_stream_request",
return_value=mock_stream_generator(),
) as mock_send_stream:
response = await anthropic_passthrough_messages(mock_request)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
chunks = [chunk async for chunk in response.body_iterator]
assert mock_send_stream.called
assert len(chunks) == 7
assert b"message_start" in chunks[0]
assert b"content_block_start" in chunks[1]
assert b"content_block_delta" in chunks[2]
assert b"Hello" in chunks[2]
assert b"content_block_delta" in chunks[3]
assert b"!" in chunks[3]
assert b"content_block_stop" in chunks[4]
assert b"message_delta" in chunks[5]
assert b"message_stop" in chunks[6]
# =============================================================================
# Gemini generateContent/streamGenerateContent passthrough endpoint tests
# =============================================================================
@pytest.mark.asyncio
async def test_gemini_passthrough_generate_content(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="gemini-passthrough-key",
secret_value={"api_key": "test-key"},
provider="gemini",
)
model_def = store.create_gateway_model_definition(
name="gemini-passthrough-model",
secret_id=secret.secret_id,
provider="gemini",
model_name="gemini-2.0-flash",
)
store.create_gateway_endpoint(
name="gemini-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}],
}
]
}
)
mock_response = {
"candidates": [
{
"content": {
"parts": [{"text": "Hello! How can I assist you today?"}],
"role": "model",
},
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 5,
"candidatesTokenCount": 10,
"totalTokenCount": 15,
},
}
with mock.patch(
"mlflow.gateway.providers.gemini.send_request", return_value=mock_response
) as mock_send:
response = await gemini_passthrough_generate_content(
"gemini-passthrough-endpoint", mock_request
)
assert mock_send.called
call_args = mock_send.call_args
assert call_args[1]["path"] == "gemini-2.0-flash:generateContent"
assert call_args[1]["payload"]["contents"] == [
{"role": "user", "parts": [{"text": "Hello"}]}
]
assert (
response["candidates"][0]["content"]["parts"][0]["text"]
== "Hello! How can I assist you today?"
)
assert response["usageMetadata"]["totalTokenCount"] == 15
@pytest.mark.asyncio
async def test_gemini_passthrough_stream_generate_content(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="gemini-stream-passthrough-key",
secret_value={"api_key": "test-stream-key"},
provider="gemini",
)
model_def = store.create_gateway_model_definition(
name="gemini-stream-passthrough-model",
secret_id=secret.secret_id,
provider="gemini",
model_name="gemini-2.0-flash",
)
store.create_gateway_endpoint(
name="gemini-stream-passthrough-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}],
}
]
}
)
mock_stream_chunks = [
b'data: {"candidates":[{"content":{"parts":[{"text":"Hello"}],"role":"model"}}]}\n\n',
b'data: {"candidates":[{"content":{"parts":[{"text":"!"}],"role":"model"}}]}\n\n',
b'data: {"candidates":[{"content":{"parts":[{"text":" How can I help you?"}],"role":"model"},"finishReason":"STOP"}]}\n\n', # noqa: E501
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.gemini.send_stream_request",
return_value=mock_stream_generator(),
) as mock_send_stream:
response = await gemini_passthrough_stream_generate_content(
"gemini-stream-passthrough-endpoint", mock_request
)
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
chunks = [chunk async for chunk in response.body_iterator]
assert mock_send_stream.called
assert len(chunks) == 3
assert b"Hello" in chunks[0]
assert b"!" in chunks[1]
assert b"How can I help you?" in chunks[2]
assert b"STOP" in chunks[2]
def test_create_fallback_provider_single_model(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-fallback-key",
secret_value={"api_key": "sk-test-key"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="gpt-fallback-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-fallback-single-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=0,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=1,
),
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, FallbackProvider)
assert len(provider._providers) == 2
assert isinstance(provider._providers[0], TrafficRouteProvider)
assert isinstance(provider._providers[1], OpenAIProvider)
assert provider._max_attempts == 2
def test_create_fallback_provider_multiple_models(store: SqlAlchemyStore):
secret1 = store.create_gateway_secret(
secret_name="openai-primary-key",
secret_value={"api_key": "sk-primary-key"},
provider="openai",
)
model_def1 = store.create_gateway_model_definition(
name="gpt-primary-model",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
secret2 = store.create_gateway_secret(
secret_name="anthropic-fallback-key",
secret_value={"api_key": "sk-ant-fallback"},
provider="anthropic",
)
model_def2 = store.create_gateway_model_definition(
name="claude-fallback-model",
secret_id=secret2.secret_id,
provider="anthropic",
model_name="claude-3-sonnet",
)
endpoint = store.create_gateway_endpoint(
name="test-fallback-multi-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=1,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=2,
),
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, FallbackProvider)
assert len(provider._providers) == 3
primary = provider._providers[0]
assert isinstance(primary, TrafficRouteProvider)
assert isinstance(primary._providers[0], OpenAIProvider)
assert isinstance(primary._providers[1], AnthropicProvider)
assert isinstance(provider._providers[1], OpenAIProvider)
assert isinstance(provider._providers[2], AnthropicProvider)
assert provider._max_attempts == 3
def test_create_fallback_provider_max_attempts_exceeds_providers(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-fallback-key",
secret_value={"api_key": "sk-test-key"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="gpt-fallback-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-fallback-max-attempts-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=0,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=10,
),
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
# FallbackProvider is the outer provider, individual providers inside are wrapped
assert isinstance(provider, FallbackProvider)
assert provider._max_attempts == 2
def test_create_fallback_provider_no_max_attempts(store: SqlAlchemyStore):
secret1 = store.create_gateway_secret(
secret_name="openai-primary-key",
secret_value={"api_key": "sk-primary-key"},
provider="openai",
)
model_def1 = store.create_gateway_model_definition(
name="gpt-primary-model",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
secret2 = store.create_gateway_secret(
secret_name="anthropic-fallback-key",
secret_value={"api_key": "sk-ant-fallback"},
provider="anthropic",
)
model_def2 = store.create_gateway_model_definition(
name="claude-fallback-model",
secret_id=secret2.secret_id,
provider="anthropic",
model_name="claude-3-sonnet",
)
endpoint = store.create_gateway_endpoint(
name="test-fallback-no-max-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=1,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=None,
),
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
# FallbackProvider is the outer provider, individual providers inside are wrapped
assert isinstance(provider, FallbackProvider)
assert len(provider._providers) == 3
assert provider._max_attempts == 3
def test_create_provider_default_routing_single_model(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="openai-default-key",
secret_value={"api_key": "sk-test-key"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="gpt-default-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="test-default-routing-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
provider, _ = _create_provider_from_endpoint_name(
store, endpoint.name, EndpointType.LLM_V1_CHAT
)
assert isinstance(provider, OpenAIProvider)
assert not isinstance(provider, FallbackProvider)
# =============================================================================
# Gateway Tracing Tests
# =============================================================================
async def _call_invocations(endpoint_name: str, request, payload: dict[str, Any]):
# invocations doesn't use "model" field - endpoint is in URL
payload_without_model = {k: v for k, v in payload.items() if k != "model"}
request.json = AsyncMock(return_value=payload_without_model)
return await invocations(endpoint_name, request)
async def _call_chat_completions(endpoint_name: str, request, payload: dict[str, Any]):
# chat_completions uses "model" field to specify endpoint
request.json = AsyncMock(return_value=payload)
return await chat_completions(request)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"handler", [_call_invocations, _call_chat_completions], ids=["invocations", "chat_completions"]
)
async def test_gateway_creates_trace_with_usage(store: SqlAlchemyStore, handler):
endpoint_name = "tracing-test-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
# Create endpoint with usage tracking enabled
secret = store.create_gateway_secret(
secret_name="tracing-test-key",
secret_value={"api_key": "sk-test-tracing"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="tracing-test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
payload = {
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hi"}],
"stream": False,
}
# Mock the OpenAI send_request to return our mock response
with mock.patch(
"mlflow.gateway.providers.openai.send_request",
return_value={
"id": "test-id",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
},
):
response = await handler(endpoint_name, mock_request, payload)
assert response.id == "test-id"
assert response.choices[0].message.content == "Hello!"
# Verify trace was created
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify gateway metadata is present in trace
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.UNIFIED_CHAT
)
# Verify span has provider information (provider name is lowercased for cost lookup alignment)
span_names = {span.name for span in trace.data.spans}
assert "provider/openai/gpt-4" in span_names
# Find the provider span and check attributes
provider_span = next(
(span for span in trace.data.spans if span.name == "provider/openai/gpt-4"), None
)
assert provider_span is not None
assert provider_span.attributes.get(SpanAttributeKey.MODEL_PROVIDER) == "openai"
assert provider_span.attributes.get(SpanAttributeKey.MODEL) == "gpt-4"
# Verify token usage is captured on the provider span
token_usage = provider_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 10
assert token_usage[TokenUsageKey.OUTPUT_TOKENS] == 5
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 15
# Verify trace metadata has aggregated token usage (auto-generated from span attributes)
trace_token_usage = json.loads(trace.info.trace_metadata.get(TraceMetadataKey.TOKEN_USAGE))
assert trace_token_usage[TokenUsageKey.INPUT_TOKENS] == 10
assert trace_token_usage[TokenUsageKey.OUTPUT_TOKENS] == 5
assert trace_token_usage[TokenUsageKey.TOTAL_TOKENS] == 15
@pytest.mark.asyncio
@pytest.mark.parametrize(
"handler", [_call_invocations, _call_chat_completions], ids=["invocations", "chat_completions"]
)
async def test_gateway_streaming_creates_trace(store: SqlAlchemyStore, handler):
endpoint_name = "stream-tracing-test-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
# Create endpoint with usage tracking enabled
secret = store.create_gateway_secret(
secret_name="stream-tracing-test-key",
secret_value={"api_key": "sk-test-stream-tracing"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="stream-tracing-test-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
payload = {
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hi"}],
"stream": True,
}
# Mock streaming response chunks with usage in the final chunk
mock_stream_chunks = [
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}\n\n', # noqa: E501
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}\n\n', # noqa: E501
b'data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1234567890,"model":"gpt-4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}\n\n', # noqa: E501
b"data: [DONE]\n\n",
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.openai.send_stream_request",
return_value=mock_stream_generator(),
):
response = await handler(endpoint_name, mock_request, payload)
# Verify streaming response is returned
assert isinstance(response, StreamingResponse)
# Consume the response
chunks = [chunk async for chunk in response.body_iterator]
assert len(chunks) > 0
# Verify trace was created for the gateway invocation
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify gateway metadata is present in trace
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.UNIFIED_CHAT
)
# Verify gateway span exists with correct attributes
gateway_span = next(
(span for span in trace.data.spans if span.name == f"gateway/{endpoint_name}"), None
)
assert gateway_span is not None
assert gateway_span.attributes.get("endpoint_name") == endpoint_name
# Verify that streaming output is aggregated into a ChatCompletion-like response
output = gateway_span.outputs
assert output is not None
assert output["object"] == "chat.completion"
assert output["id"] == "chatcmpl-123"
assert output["model"] == "gpt-4"
assert len(output["choices"]) == 1
assert output["choices"][0]["index"] == 0
assert output["choices"][0]["message"]["role"] == "assistant"
assert output["choices"][0]["message"]["content"] == "Hello!"
assert output["choices"][0]["finish_reason"] == "stop"
assert output["usage"]["prompt_tokens"] == 10
assert output["usage"]["completion_tokens"] == 5
assert output["usage"]["total_tokens"] == 15
@pytest.mark.asyncio
@pytest.mark.parametrize(
"handler", [_call_invocations, _call_chat_completions], ids=["invocations", "chat_completions"]
)
async def test_gateway_trace_includes_user_metadata(store: SqlAlchemyStore, handler):
endpoint_name = "user-metadata-tracing-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
# Create endpoint with usage tracking enabled
secret = store.create_gateway_secret(
secret_name="user-metadata-tracing-key",
secret_value={"api_key": "sk-test-user-metadata"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="user-metadata-tracing-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
# Create mock request with user metadata set (as auth middleware would do)
mock_request = create_mock_request(username="test_user", user_id=42)
payload = {
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hi"}],
"stream": False,
}
# Mock the OpenAI send_request to return our mock response
with mock.patch(
"mlflow.gateway.providers.openai.send_request",
return_value={
"id": "test-id",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
},
):
response = await handler(endpoint_name, mock_request, payload)
assert response.id == "test-id"
assert response.choices[0].message.content == "Hello!"
# Verify trace was created
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify user metadata is present in trace info
assert trace.info.request_metadata.get(TraceMetadataKey.AUTH_USERNAME) == "test_user"
assert trace.info.request_metadata.get(TraceMetadataKey.AUTH_USER_ID) == "42"
# Verify gateway metadata is present alongside user metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.UNIFIED_CHAT
)
# Verify span attributes still include endpoint info
gateway_span = next(
(span for span in trace.data.spans if span.name == f"gateway/{endpoint_name}"), None
)
assert gateway_span is not None
assert gateway_span.attributes.get("endpoint_name") == endpoint_name
# =============================================================================
# Passthrough Token Usage Tracking Tests
# =============================================================================
@pytest.mark.asyncio
async def test_openai_passthrough_chat_token_usage_tracking(store: SqlAlchemyStore):
endpoint_name = "openai-passthrough-usage-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="openai-passthrough-usage-key",
secret_value={"api_key": "sk-test-usage"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-passthrough-usage-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hello"}],
}
)
mock_response = {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
with mock.patch("mlflow.gateway.providers.openai.send_request", return_value=mock_response):
response = await openai_passthrough_chat(mock_request)
assert response["usage"]["prompt_tokens"] == 10
assert response["usage"]["completion_tokens"] == 5
assert response["usage"]["total_tokens"] == 15
# Verify trace was created with token usage
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_CHAT
)
# Find the passthrough span and check token usage attributes
passthrough_span = next(
(span for span in trace.data.spans if "action" in span.attributes), None
)
assert passthrough_span is not None
assert passthrough_span.attributes.get("action") == "openai_chat"
token_usage = passthrough_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 10
assert token_usage[TokenUsageKey.OUTPUT_TOKENS] == 5
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 15
@pytest.mark.asyncio
async def test_openai_passthrough_embeddings_token_usage_tracking(store: SqlAlchemyStore):
endpoint_name = "openai-embed-usage-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="openai-embed-usage-key",
secret_value={"api_key": "sk-test-embed-usage"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-embed-usage-model",
secret_id=secret.secret_id,
provider="openai",
model_name="text-embedding-3-small",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": endpoint_name,
"input": "Test text for embedding",
}
)
mock_response = {
"object": "list",
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3]}],
"model": "text-embedding-3-small",
"usage": {"prompt_tokens": 5, "total_tokens": 5},
}
with mock.patch("mlflow.gateway.providers.openai.send_request", return_value=mock_response):
response = await openai_passthrough_embeddings(mock_request)
assert response["usage"]["prompt_tokens"] == 5
assert response["usage"]["total_tokens"] == 5
# Verify trace was created with token usage
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_EMBEDDINGS
)
# Find the passthrough span and check token usage attributes
passthrough_span = next(
(span for span in trace.data.spans if "action" in span.attributes), None
)
assert passthrough_span is not None
assert passthrough_span.attributes.get("action") == "openai_embeddings"
token_usage = passthrough_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 5
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 5
@pytest.mark.asyncio
async def test_openai_passthrough_responses_token_usage_tracking(store: SqlAlchemyStore):
endpoint_name = "openai-responses-usage-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="openai-responses-usage-key",
secret_value={"api_key": "sk-test-responses-usage"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-responses-usage-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": endpoint_name,
"input": [{"role": "user", "content": "Hello"}],
}
)
mock_response = {
"id": "resp-123",
"object": "response",
"created": 1234567890,
"model": "gpt-4o",
"status": "completed",
"output": [
{
"role": "assistant",
"content": [{"type": "output_text", "text": "Hello!"}],
}
],
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
}
with mock.patch("mlflow.gateway.providers.openai.send_request", return_value=mock_response):
response = await openai_passthrough_responses(mock_request)
assert response["usage"]["input_tokens"] == 10
assert response["usage"]["output_tokens"] == 5
assert response["usage"]["total_tokens"] == 15
# Verify trace was created with token usage
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_RESPONSES
)
# Find the passthrough span and check token usage attributes
passthrough_span = next(
(span for span in trace.data.spans if "action" in span.attributes), None
)
assert passthrough_span is not None
assert passthrough_span.attributes.get("action") == "openai_responses"
token_usage = passthrough_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 10
assert token_usage[TokenUsageKey.OUTPUT_TOKENS] == 5
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 15
@pytest.mark.asyncio
async def test_anthropic_passthrough_messages_token_usage_tracking(store: SqlAlchemyStore):
endpoint_name = "anthropic-usage-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="anthropic-usage-key",
secret_value={"api_key": "sk-ant-usage"},
provider="anthropic",
)
model_def = store.create_gateway_model_definition(
name="anthropic-usage-model",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 1024,
}
)
mock_response = {
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hello!"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 12, "output_tokens": 8},
}
with mock.patch("mlflow.gateway.providers.anthropic.send_request", return_value=mock_response):
response = await anthropic_passthrough_messages(mock_request)
assert response["usage"]["input_tokens"] == 12
assert response["usage"]["output_tokens"] == 8
# Verify trace was created with token usage
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_ANTHROPIC_MESSAGES
)
# Find the passthrough span and check token usage attributes
passthrough_span = next(
(span for span in trace.data.spans if "action" in span.attributes), None
)
assert passthrough_span is not None
assert passthrough_span.attributes.get("action") == "anthropic_messages"
token_usage = passthrough_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 12
assert token_usage[TokenUsageKey.OUTPUT_TOKENS] == 8
# Anthropic doesn't provide total_tokens, so we calculate it
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 20
@pytest.mark.asyncio
async def test_gemini_passthrough_generate_content_token_usage_tracking(store: SqlAlchemyStore):
endpoint_name = "gemini-usage-endpoint"
# Create experiment for tracing
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="gemini-usage-key",
secret_value={"api_key": "test-gemini-usage-key"},
provider="gemini",
)
model_def = store.create_gateway_model_definition(
name="gemini-usage-model",
secret_id=secret.secret_id,
provider="gemini",
model_name="gemini-2.0-flash",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"contents": [
{
"role": "user",
"parts": [{"text": "Hello"}],
}
]
}
)
mock_response = {
"candidates": [
{
"content": {
"parts": [{"text": "Hello! How can I help?"}],
"role": "model",
},
"finishReason": "STOP",
}
],
"usageMetadata": {
"promptTokenCount": 7,
"candidatesTokenCount": 9,
"totalTokenCount": 16,
},
}
with mock.patch("mlflow.gateway.providers.gemini.send_request", return_value=mock_response):
response = await gemini_passthrough_generate_content(endpoint_name, mock_request)
assert response["usageMetadata"]["promptTokenCount"] == 7
assert response["usageMetadata"]["candidatesTokenCount"] == 9
assert response["usageMetadata"]["totalTokenCount"] == 16
# Verify trace was created with token usage
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_GEMINI_GENERATE_CONTENT
)
# Find the passthrough span and check token usage attributes
passthrough_span = next(
(span for span in trace.data.spans if "action" in span.attributes), None
)
assert passthrough_span is not None
assert passthrough_span.attributes.get("action") == "gemini_generate_content"
token_usage = passthrough_span.attributes.get(SpanAttributeKey.CHAT_USAGE)
assert token_usage is not None
assert token_usage[TokenUsageKey.INPUT_TOKENS] == 7
assert token_usage[TokenUsageKey.OUTPUT_TOKENS] == 9
assert token_usage[TokenUsageKey.TOTAL_TOKENS] == 16
@pytest.mark.asyncio
async def test_openai_passthrough_streaming_captures_chunks(store: SqlAlchemyStore):
endpoint_name = "openai-passthrough-streaming-chunks"
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name="openai-stream-chunks-key",
secret_value={"api_key": "sk-test-stream-chunks"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="openai-stream-chunks-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
experiment_id=experiment_id,
)
mock_request = create_mock_request()
mock_request.json = AsyncMock(
return_value={
"model": endpoint_name,
"messages": [{"role": "user", "content": "Hello"}],
"stream": True,
}
)
mock_request.headers = {}
mock_stream_chunks = [
b'data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"Hi"}}]}\n\n',
b'data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"!"}}]}\n\n',
b"data: [DONE]\n\n",
]
async def mock_stream_generator():
for chunk in mock_stream_chunks:
yield chunk
with mock.patch(
"mlflow.gateway.providers.openai.send_stream_request",
return_value=mock_stream_generator(),
):
response = await openai_passthrough_chat(mock_request)
assert isinstance(response, StreamingResponse)
chunks = [chunk async for chunk in response.body_iterator]
assert len(chunks) == len(mock_stream_chunks)
# Verify trace was created
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
trace = traces[0]
assert trace.info.state == TraceState.OK
# Verify gateway metadata
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_ENDPOINT_ID)
== endpoint.endpoint_id
)
assert (
trace.info.request_metadata.get(TraceMetadataKey.GATEWAY_REQUEST_TYPE)
== GatewayRequestType.PASSTHROUGH_MODEL_OPENAI_CHAT
)
gateway_span = next(
(span for span in trace.data.spans if span.name == f"gateway/{endpoint_name}"), None
)
assert gateway_span is not None
# Verify streaming chunks are captured in outputs (raw SSE bytes decoded to strings)
assert gateway_span.outputs is not None
assert len(gateway_span.outputs) == len(mock_stream_chunks)
# Verify the outputs contain actual SSE data (not async generator object repr)
assert "data:" in gateway_span.outputs[0]
assert "chatcmpl-123" in gateway_span.outputs[0]
# ─── Guardrail end-to-end scenarios ──────────────────────────────────────────
class _SimpleScorer:
"""Minimal scorer that returns 'yes' or 'no' and tracks call count."""
def __init__(self, *, passing: bool = True) -> None:
self.call_count = 0
self._passing = passing
def __call__(self, **kwargs) -> str:
self.call_count += 1
return "yes" if self._passing else "no"
def _make_guardrail_judge(stage, action=GuardrailAction.VALIDATION, *, passing=True):
scorer = _SimpleScorer(passing=passing)
return JudgeGuardrail(
scorer=scorer,
stage=GuardrailStage(stage),
action=GuardrailAction(action),
name=f"test-{stage.lower()}",
)
def _make_guardrail_chat_response(content: str = "Hello!") -> chat.ResponsePayload:
return chat.ResponsePayload(
id="resp-id",
object="chat.completion",
created=1234567890,
model="gpt-4",
choices=[
chat.Choice(
index=0,
message=chat.ResponseMessage(role="assistant", content=content),
finish_reason="stop",
)
],
usage=chat.ChatUsage(prompt_tokens=5, completion_tokens=5, total_tokens=10),
)
def _make_guardrail_mock_request(body: dict[str, Any], headers: dict[str, str] | None = None):
req = MagicMock()
req.state.cached_body = None
req.state.username = None
req.state.user_id = None
req.json = AsyncMock(return_value=body)
req.headers = headers or {}
req.base_url = "http://localhost:5000/"
return req
_GUARDRAIL_SERIALIZED_SCORER = json.dumps({"name": "safety", "builtin_scorer_class": "Safety"})
def _setup_db_guardrail(
store: SqlAlchemyStore,
endpoint_name: str,
stage: str,
action: str,
action_endpoint_name: str | None = None,
execution_order: int | None = None,
name: str | None = None,
):
"""Create scorer + guardrail in DB and attach it to the endpoint."""
guardrail_name = name or f"guardrail-{endpoint_name}-{stage}"
experiment_id = store.create_experiment(f"exp-{guardrail_name}")
scorer_ver = store.register_scorer(
experiment_id, f"scorer-{guardrail_name}", _GUARDRAIL_SERIALIZED_SCORER
)
action_endpoint_id = None
if action_endpoint_name:
action_endpoint_id = store.get_gateway_endpoint(name=action_endpoint_name).endpoint_id
guardrail = store.create_gateway_guardrail(
name=guardrail_name,
scorer_id=scorer_ver.scorer_id,
scorer_version=scorer_ver.scorer_version,
stage=GuardrailStage(stage),
action=GuardrailAction(action),
action_endpoint_id=action_endpoint_id,
)
endpoint = store.get_gateway_endpoint(name=endpoint_name)
store.add_guardrail_to_endpoint(
endpoint.endpoint_id, guardrail.guardrail_id, execution_order=execution_order
)
return guardrail, scorer_ver
def _setup_guardrail_endpoint(store: SqlAlchemyStore, name: str):
secret = store.create_gateway_secret(
secret_name=f"key-{name}",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name=f"model-{name}",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
return store.create_gateway_endpoint(
name=name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
)
],
)
@pytest.mark.asyncio
async def test_invocations_bypass_header_skips_guardrails(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-bypass")
mock_response = _make_guardrail_chat_response("Bypass response")
mock_request = _make_guardrail_mock_request(
{"messages": [{"role": "user", "content": "hello"}]},
headers={_SANITIZE_BYPASS_HEADER: "1"},
)
with (
patch("mlflow.server.gateway_api._create_provider_from_endpoint_name") as mock_create,
patch("mlflow.server.gateway_api.load_guardrails") as mock_load,
):
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_create.return_value = (
mock_provider,
GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id, endpoint_name=endpoint.name, models=[]
),
)
response = await invocations(endpoint.name, mock_request)
mock_load.assert_not_called()
assert response.choices[0].message.content == "Bypass response"
@pytest.mark.asyncio
async def test_invocations_bypass_header_wrong_value_runs_guardrails(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-bypass-wrong-val")
_setup_db_guardrail(store, "ep-bypass-wrong-val", "BEFORE", "VALIDATION")
mock_request = _make_guardrail_mock_request(
{"messages": [{"role": "user", "content": "hello"}]},
headers={_SANITIZE_BYPASS_HEADER: "true"}, # wrong value — must not bypass
)
blocking_scorer = _SimpleScorer(passing=False)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=blocking_scorer),
patch("mlflow.gateway.providers.openai.OpenAIProvider.chat", AsyncMock()),
):
with pytest.raises(HTTPException, match="400"):
await invocations(endpoint.name, mock_request)
assert blocking_scorer.call_count == 1
@pytest.mark.asyncio
async def test_real_db_pre_llm_guardrail_passes(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "real-ep-pre-llm-pass")
_setup_db_guardrail(store, "real-ep-pre-llm-pass", "BEFORE", "VALIDATION")
mock_response = _make_guardrail_chat_response("Safe response")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "hello"}]
})
passing_scorer = _SimpleScorer(passing=True)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=passing_scorer),
patch(
"mlflow.gateway.providers.openai.OpenAIProvider.chat",
AsyncMock(return_value=mock_response),
),
):
response = await invocations(endpoint.name, mock_request)
assert response.choices[0].message.content == "Safe response"
assert passing_scorer.call_count == 1
@pytest.mark.asyncio
async def test_real_db_pre_llm_guardrail_blocks(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "real-ep-pre-llm-block")
_setup_db_guardrail(store, "real-ep-pre-llm-block", "BEFORE", "VALIDATION")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "bad input"}]
})
blocking_scorer = _SimpleScorer(passing=False)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=blocking_scorer),
patch(
"mlflow.gateway.providers.openai.OpenAIProvider.chat",
AsyncMock(),
) as mock_chat,
):
with pytest.raises(HTTPException, match="400"):
await invocations(endpoint.name, mock_request)
assert not mock_chat.called
assert blocking_scorer.call_count == 1
@pytest.mark.asyncio
async def test_real_db_post_llm_guardrail_blocks(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "real-ep-post-llm-block")
_setup_db_guardrail(store, "real-ep-post-llm-block", "AFTER", "VALIDATION")
mock_response = _make_guardrail_chat_response("Unsafe output")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "hello"}]
})
blocking_scorer = _SimpleScorer(passing=False)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=blocking_scorer),
patch(
"mlflow.gateway.providers.openai.OpenAIProvider.chat",
AsyncMock(return_value=mock_response),
) as mock_chat,
):
with pytest.raises(HTTPException, match="400"):
await invocations(endpoint.name, mock_request)
assert mock_chat.called
assert blocking_scorer.call_count == 1
@pytest.mark.asyncio
async def test_invocations_before_sanitize_rewrites_request(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-sanitize-before")
sanitizer = _setup_guardrail_endpoint(store, "ep-sanitizer")
_setup_db_guardrail(
store, "ep-sanitize-before", "BEFORE", "SANITIZATION", action_endpoint_name=sanitizer.name
)
sanitized_body = {"messages": [{"role": "user", "content": "cleaned input"}]}
mock_response = _make_guardrail_chat_response("Response to cleaned input")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "bad input"}]
})
failing_scorer = _SimpleScorer(passing=False)
captured_payloads: list[Any] = []
async def fake_chat(payload):
captured_payloads.append(payload)
return mock_response
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=failing_scorer),
patch(
"mlflow.gateway.guardrails.send_request",
AsyncMock(
return_value={"choices": [{"message": {"content": json.dumps(sanitized_body)}}]}
),
),
patch("mlflow.gateway.providers.openai.OpenAIProvider.chat", side_effect=fake_chat),
):
response = await invocations(endpoint.name, mock_request)
assert response.choices[0].message.content == "Response to cleaned input"
assert failing_scorer.call_count == 1
assert captured_payloads[0].messages[0].content == "cleaned input"
@pytest.mark.asyncio
async def test_invocations_after_sanitize_rewrites_response(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-sanitize-after")
sanitizer = _setup_guardrail_endpoint(store, "ep-sanitizer-after")
_setup_db_guardrail(
store, "ep-sanitize-after", "AFTER", "SANITIZATION", action_endpoint_name=sanitizer.name
)
sanitized_response = {
"id": "resp-sanitized",
"object": "chat.completion",
"created": 1234567890,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "polite output"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 5, "completion_tokens": 5, "total_tokens": 10},
}
mock_response = _make_guardrail_chat_response("rude output")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "hello"}]
})
failing_scorer = _SimpleScorer(passing=False)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=failing_scorer),
patch(
"mlflow.gateway.guardrails.send_request",
AsyncMock(
return_value={"choices": [{"message": {"content": json.dumps(sanitized_response)}}]}
),
),
patch(
"mlflow.gateway.providers.openai.OpenAIProvider.chat",
AsyncMock(return_value=mock_response),
),
):
response = await invocations(endpoint.name, mock_request)
assert response.choices[0].message.content == "polite output"
assert failing_scorer.call_count == 1
@pytest.mark.asyncio
async def test_invocations_sanitize_no_action_endpoint_blocks(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-sanitize-no-ep")
_setup_db_guardrail(store, "ep-sanitize-no-ep", "BEFORE", "SANITIZATION")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "bad input"}]
})
failing_scorer = _SimpleScorer(passing=False)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=failing_scorer),
patch("mlflow.gateway.providers.openai.OpenAIProvider.chat", AsyncMock()),
):
with pytest.raises(HTTPException, match="400"):
await invocations(endpoint.name, mock_request)
@pytest.mark.asyncio
async def test_chat_completions_before_sanitize_rewrites_request(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-cc-sanitize-before")
sanitizer = _setup_guardrail_endpoint(store, "ep-cc-sanitizer")
_setup_db_guardrail(
store,
"ep-cc-sanitize-before",
"BEFORE",
"SANITIZATION",
action_endpoint_name=sanitizer.name,
)
sanitized_body = {"messages": [{"role": "user", "content": "cleaned input"}]}
mock_response = _make_guardrail_chat_response("Response to cleaned input")
mock_request = _make_guardrail_mock_request({
"model": endpoint.name,
"messages": [{"role": "user", "content": "bad input"}],
})
failing_scorer = _SimpleScorer(passing=False)
captured_payloads: list[Any] = []
async def fake_chat(payload):
captured_payloads.append(payload)
return mock_response
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=failing_scorer),
patch(
"mlflow.gateway.guardrails.send_request",
AsyncMock(
return_value={"choices": [{"message": {"content": json.dumps(sanitized_body)}}]}
),
),
patch("mlflow.gateway.providers.openai.OpenAIProvider.chat", side_effect=fake_chat),
):
from mlflow.server.gateway_api import chat_completions
response = await chat_completions(mock_request)
assert response.choices[0].message.content == "Response to cleaned input"
assert failing_scorer.call_count == 1
assert captured_payloads[0].messages[0].content == "cleaned input"
@pytest.mark.asyncio
async def test_guardrails_run_in_execution_order(store: SqlAlchemyStore):
endpoint = _setup_guardrail_endpoint(store, "ep-order-test")
# Register in reverse order (5→1) to ensure DB insertion order != execution order.
for i in range(5, 0, -1):
_setup_db_guardrail(
store,
"ep-order-test",
"BEFORE",
"VALIDATION",
execution_order=i,
name=f"g-order-{i}",
)
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "hello"}]
})
call_order: list[str] = []
def make_scorer(label: str, passing: bool):
def scorer(**kwargs):
call_order.append(label)
return "yes" if passing else "no"
return scorer
# Guardrails 1-4 pass; guardrail 5 blocks — so all 5 must run in order 1→2→3→4→5.
scorers = [make_scorer(f"order-{i}", passing=(i < 5)) for i in range(1, 6)]
call_count = {"n": 0}
def model_validate_side_effect(serialized):
scorer = scorers[call_count["n"]]
call_count["n"] += 1
return scorer
with (
patch(
"mlflow.genai.scorers.base.Scorer.model_validate",
side_effect=model_validate_side_effect,
),
patch("mlflow.gateway.providers.openai.OpenAIProvider.chat", AsyncMock()),
):
with pytest.raises(HTTPException, match="400"):
await invocations(endpoint.name, mock_request)
assert call_order == ["order-1", "order-2", "order-3", "order-4", "order-5"]
@pytest.mark.asyncio
async def test_guardrail_spans_created_when_usage_tracking_on(store: SqlAlchemyStore):
endpoint_name = "ep-guardrail-tracing"
experiment_id = store.create_experiment(f"gateway/{endpoint_name}")
secret = store.create_gateway_secret(
secret_name=f"key-{endpoint_name}",
secret_value={"api_key": "sk-test"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name=f"model-{endpoint_name}",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name=endpoint_name,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
)
],
usage_tracking=True,
experiment_id=experiment_id,
)
_setup_db_guardrail(store, endpoint_name, "BEFORE", "VALIDATION", name="safety-check")
mock_response = _make_guardrail_chat_response("Safe response")
mock_request = _make_guardrail_mock_request({
"messages": [{"role": "user", "content": "hello"}]
})
passing_scorer = _SimpleScorer(passing=True)
with (
patch("mlflow.genai.scorers.base.Scorer.model_validate", return_value=passing_scorer),
patch(
"mlflow.gateway.providers.openai.OpenAIProvider.chat",
AsyncMock(return_value=mock_response),
),
):
response = await invocations(endpoint.name, mock_request)
assert response.choices[0].message.content == "Safe response"
traces = TracingClient().search_traces(locations=[experiment_id])
assert len(traces) == 1
span_map = {s.name: s for s in traces[0].data.spans}
assert "guardrail/safety-check" in span_map
assert "judge" in span_map
gspan = span_map["guardrail/safety-check"]
jspan = span_map["judge"]
assert gspan.span_type == SpanType.GUARDRAIL
assert jspan.span_type == SpanType.EVALUATOR
assert jspan.outputs["passed"] is True
assert jspan.parent_id == gspan.span_id