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

279 lines
8.4 KiB
Python

from unittest import mock
import pytest
from fastapi.testclient import TestClient
from mlflow.exceptions import MlflowException
from mlflow.gateway.app import create_app_from_config, create_app_from_env
from mlflow.gateway.config import GatewayConfig
from mlflow.gateway.constants import (
MLFLOW_GATEWAY_CRUD_ENDPOINT_V3_BASE,
MLFLOW_GATEWAY_CRUD_ROUTE_BASE,
MLFLOW_GATEWAY_CRUD_ROUTE_V3_BASE,
MLFLOW_GATEWAY_ROUTE_BASE,
)
from tests.gateway.tools import MockAsyncResponse
@pytest.fixture
def client() -> TestClient:
config = GatewayConfig(**{
"endpoints": [
{
"name": "completions-gpt4",
"endpoint_type": "llm/v1/completions",
"model": {
"name": "gpt-4",
"provider": "openai",
"config": {
"openai_api_key": "mykey",
"openai_api_base": "https://api.openai.com/v1",
"openai_api_version": "2023-05-10",
"openai_api_type": "openai",
},
},
},
{
"name": "chat-gpt4",
"endpoint_type": "llm/v1/chat",
"model": {
"name": "gpt-4",
"provider": "openai",
"config": {
"openai_api_key": "MY_API_KEY",
},
},
},
{
"name": "chat-gpt5",
"endpoint_type": "llm/v1/chat",
"model": {
"name": "gpt-5",
"provider": "openai",
"config": {
"openai_api_key": "MY_API_KEY",
},
},
},
],
"routes": [
{
"name": "traffic_route1",
"task_type": "llm/v1/chat",
"destinations": [
{
"name": "chat-gpt4",
"traffic_percentage": 80,
},
{
"name": "chat-gpt5",
"traffic_percentage": 20,
},
],
},
],
})
app = create_app_from_config(config)
return TestClient(app)
def test_index(client: TestClient):
response = client.get("/", follow_redirects=False)
assert response.status_code == 307
assert response.headers["Location"] == "/docs"
def test_health(client: TestClient):
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "OK"}
def test_favicon(client: TestClient):
response = client.get("/favicon.ico")
assert response.status_code == 200
def test_docs(client: TestClient):
response = client.get("/docs")
assert response.status_code == 200
def test_search_routes(client: TestClient):
response = client.get(MLFLOW_GATEWAY_CRUD_ROUTE_BASE)
assert response.status_code == 200
assert response.json()["routes"] == [
{
"name": "completions-gpt4",
"route_type": "llm/v1/completions",
"route_url": "/gateway/completions-gpt4/invocations",
"model": {
"name": "gpt-4",
"provider": "openai",
},
"limit": None,
},
{
"name": "chat-gpt4",
"route_type": "llm/v1/chat",
"route_url": "/gateway/chat-gpt4/invocations",
"model": {
"name": "gpt-4",
"provider": "openai",
},
"limit": None,
},
{
"name": "chat-gpt5",
"route_type": "llm/v1/chat",
"route_url": "/gateway/chat-gpt5/invocations",
"model": {
"name": "gpt-5",
"provider": "openai",
},
"limit": None,
},
]
def test_get_route(client: TestClient):
response = client.get(f"{MLFLOW_GATEWAY_CRUD_ROUTE_BASE}chat-gpt4")
assert response.status_code == 200
assert response.json() == {
"name": "chat-gpt4",
"route_type": "llm/v1/chat",
"route_url": "/gateway/chat-gpt4/invocations",
"model": {
"name": "gpt-4",
"provider": "openai",
},
"limit": None,
}
def test_get_endpoint_v3(client: TestClient):
response = client.get(f"{MLFLOW_GATEWAY_CRUD_ENDPOINT_V3_BASE}chat-gpt4")
assert response.status_code == 200
assert response.json() == {
"name": "chat-gpt4",
"endpoint_type": "llm/v1/chat",
"model": {"name": "gpt-4", "provider": "openai"},
"endpoint_url": "/gateway/chat-gpt4/invocations",
"limit": None,
}
def test_get_route_v3(client: TestClient):
response = client.get(f"{MLFLOW_GATEWAY_CRUD_ROUTE_V3_BASE}traffic_route1")
assert response.status_code == 200
assert response.json() == {
"name": "traffic_route1",
"task_type": "llm/v1/chat",
"destinations": [
{"name": "chat-gpt4", "traffic_percentage": 80},
{"name": "chat-gpt5", "traffic_percentage": 20},
],
"routing_strategy": "TRAFFIC_SPLIT",
}
def test_dynamic_route():
config = GatewayConfig(**{
"endpoints": [
{
"name": "chat",
"endpoint_type": "llm/v1/chat",
"model": {
"name": "gpt-4",
"provider": "openai",
"config": {
"openai_api_key": "mykey",
"openai_api_base": "https://api.openai.com/v1",
},
},
"limit": None,
}
],
"routes": [
{
"name": "traffic_route",
"task_type": "llm/v1/chat",
"destinations": [
{
"name": "chat",
"traffic_percentage": 100,
}
],
}
],
})
app = create_app_from_config(config)
client = TestClient(app)
resp = {
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-4o-mini",
"usage": {
"prompt_tokens": 13,
"completion_tokens": 7,
"total_tokens": 20,
},
"choices": [
{
"message": {
"role": "assistant",
"content": "\n\nThis is a test!",
"refusal": None,
},
"finish_reason": "stop",
"index": 0,
}
],
"headers": {"Content-Type": "application/json"},
}
with mock.patch(
"aiohttp.ClientSession.post", return_value=MockAsyncResponse(resp)
) as mock_post:
for name in ["chat", "traffic_route"]:
resp = client.post(
f"{MLFLOW_GATEWAY_ROUTE_BASE}{name}/invocations",
json={"messages": [{"role": "user", "content": "Tell me a joke"}]},
)
mock_post.assert_called_once()
assert resp.status_code == 200
assert resp.json() == {
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-4o-mini",
"provider": "openai",
"usage": {
"prompt_tokens": 13,
"completion_tokens": 7,
"total_tokens": 20,
},
"choices": [
{
"message": {
"role": "assistant",
"content": "\n\nThis is a test!",
"tool_calls": None,
"refusal": None,
},
"finish_reason": "stop",
"index": 0,
}
],
}
mock_post.reset_mock()
def test_create_app_from_env_fails_if_MLFLOW_GATEWAY_CONFIG_is_not_set(monkeypatch):
monkeypatch.delenv("MLFLOW_GATEWAY_CONFIG", raising=False)
with pytest.raises(MlflowException, match="'MLFLOW_GATEWAY_CONFIG' is not set"):
create_app_from_env()