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

3103 lines
108 KiB
Python

import json
import uuid
from pathlib import Path
from unittest import mock
import pytest
from mlflow.entities import (
BudgetAction,
BudgetDuration,
BudgetDurationUnit,
BudgetTargetScope,
BudgetUnit,
FallbackConfig,
FallbackStrategy,
GatewayBudgetPolicy,
GatewayEndpoint,
GatewayEndpointBinding,
GatewayEndpointModelConfig,
GatewayEndpointModelMapping,
GatewayEndpointTag,
GatewayGuardrail,
GatewayGuardrailConfig,
GatewayModelDefinition,
GatewayModelLinkageType,
GatewaySecretInfo,
GuardrailAction,
GuardrailStage,
RoutingStrategy,
)
from mlflow.environment_variables import MLFLOW_ENABLE_WORKSPACES, MLFLOW_TRACKING_URI
from mlflow.exceptions import MlflowException
from mlflow.protos.databricks_pb2 import (
INVALID_PARAMETER_VALUE,
INVALID_STATE,
RESOURCE_ALREADY_EXISTS,
RESOURCE_DOES_NOT_EXIST,
ErrorCode,
)
from mlflow.store.tracking.dbmodels.models import (
SqlExperiment,
SqlExperimentTag,
SqlGatewayBudgetPolicy,
SqlGatewayEndpoint,
SqlGatewayEndpointBinding,
SqlGatewayEndpointModelMapping,
SqlGatewayEndpointTag,
SqlGatewayGuardrail,
SqlGatewayGuardrailConfig,
SqlGatewayModelDefinition,
SqlGatewaySecret,
SqlOnlineScoringConfig,
SqlScorer,
SqlScorerVersion,
SqlSpan,
SqlSpanMetrics,
SqlTraceInfo,
SqlTraceMetadata,
)
from mlflow.store.tracking.gateway.config_resolver import (
get_endpoint_config,
get_resource_endpoint_configs,
)
from mlflow.store.tracking.gateway.entities import GatewayEndpointConfig, GatewayModelConfig
from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
from mlflow.store.tracking.sqlalchemy_workspace_store import WorkspaceAwareSqlAlchemyStore
from mlflow.tracing.constant import SpanMetricKey, TraceMetadataKey
from mlflow.utils.workspace_context import WorkspaceContext
from mlflow.utils.workspace_utils import DEFAULT_WORKSPACE_NAME
pytestmark = pytest.mark.notrackingurimock
TEST_PASSPHRASE = "test-passphrase-for-gateway-tests"
@pytest.fixture(autouse=True)
def set_kek_passphrase(monkeypatch):
monkeypatch.setenv("MLFLOW_CRYPTO_KEK_PASSPHRASE", TEST_PASSPHRASE)
def _cleanup_database(store: SqlAlchemyStore):
"""Clean up gateway-specific tables after each test."""
with store.ManagedSessionMaker(read_only=False) as session:
# Delete all rows in gateway tables in dependency order
for model in (
SqlGatewayGuardrailConfig,
SqlGatewayGuardrail,
SqlGatewayBudgetPolicy,
SqlGatewayEndpointTag,
SqlGatewayEndpointBinding,
SqlGatewayEndpointModelMapping,
SqlGatewayEndpoint,
SqlGatewayModelDefinition,
SqlGatewaySecret,
SqlOnlineScoringConfig,
SqlScorerVersion,
SqlScorer,
SqlSpanMetrics,
SqlSpan,
SqlTraceMetadata,
SqlTraceInfo,
SqlExperimentTag,
SqlExperiment,
):
session.query(model).delete()
# Ensure the default experiment exists in the default workspace (ID 0).
with WorkspaceContext(DEFAULT_WORKSPACE_NAME):
store._create_default_experiment(session)
@pytest.fixture(autouse=True, params=[False, True], ids=["workspace-disabled", "workspace-enabled"])
def workspaces_enabled(request, monkeypatch):
enabled = request.param
monkeypatch.setenv(MLFLOW_ENABLE_WORKSPACES.name, "true" if enabled else "false")
if enabled:
# Use a unique workspace per test to avoid name collisions on shared DBs.
workspace_name = f"gateway-test-{uuid.uuid4().hex}"
with WorkspaceContext(workspace_name):
yield enabled
else:
yield enabled
@pytest.fixture
def store(tmp_path: Path, db_uri: str, workspaces_enabled):
artifact_uri = tmp_path / "artifacts"
artifact_uri.mkdir(exist_ok=True)
store_cls = WorkspaceAwareSqlAlchemyStore if workspaces_enabled else SqlAlchemyStore
if db_uri_env := MLFLOW_TRACKING_URI.get():
s = store_cls(db_uri_env, artifact_uri.as_uri())
yield s
_cleanup_database(s)
else:
s = store_cls(db_uri, artifact_uri.as_uri())
yield s
_cleanup_database(s)
# =============================================================================
# Secret Operations
# =============================================================================
def test_create_gateway_secret(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="my-api-key",
secret_value={"api_key": "sk-test-123456"},
provider="openai",
created_by="test-user",
)
assert isinstance(secret, GatewaySecretInfo)
assert secret.secret_id.startswith("s-")
assert secret.secret_name == "my-api-key"
assert secret.provider == "openai"
assert secret.created_by == "test-user"
assert isinstance(secret.masked_values, dict)
assert "api_key" in secret.masked_values
assert "sk-test-123456" not in secret.masked_values["api_key"]
def test_create_gateway_secret_with_auth_config(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="bedrock-creds",
secret_value={"api_key": "aws-secret-key"},
provider="bedrock",
auth_config={"region": "us-east-1", "project_id": "my-project"},
)
assert secret.secret_name == "bedrock-creds"
assert secret.provider == "bedrock"
def test_create_gateway_secret_with_dict_value(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="multi-secret",
secret_value={
"aws_access_key_id": "AKIA1234567890",
"aws_secret_access_key": "secret-key-here",
},
provider="bedrock",
auth_config={"auth_mode": "access_keys", "aws_region_name": "us-west-2"},
)
assert secret.secret_name == "multi-secret"
assert secret.provider == "bedrock"
assert isinstance(secret.masked_values, dict)
assert "aws_access_key_id" in secret.masked_values
assert "aws_secret_access_key" in secret.masked_values
assert "AKIA1234567890" not in secret.masked_values["aws_access_key_id"]
assert "secret-key-here" not in secret.masked_values["aws_secret_access_key"]
def test_create_gateway_secret_duplicate_name_raises(store: SqlAlchemyStore):
store.create_gateway_secret(secret_name="duplicate-name", secret_value={"api_key": "value1"})
with pytest.raises(MlflowException, match="already exists") as exc:
store.create_gateway_secret(
secret_name="duplicate-name", secret_value={"api_key": "value2"}
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_ALREADY_EXISTS)
def test_get_gateway_secret_info_by_id(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="test-secret",
secret_value={"api_key": "secret-value"},
provider="anthropic",
)
retrieved = store.get_secret_info(secret_id=created.secret_id)
assert retrieved.secret_id == created.secret_id
assert retrieved.secret_name == "test-secret"
assert retrieved.provider == "anthropic"
def test_get_gateway_secret_info_by_name(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="named-secret",
secret_value={"api_key": "secret-value"},
)
retrieved = store.get_secret_info(secret_name="named-secret")
assert retrieved.secret_id == created.secret_id
assert retrieved.secret_name == "named-secret"
def test_get_gateway_secret_info_requires_one_of_id_or_name(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="Exactly one of") as exc:
store.get_secret_info()
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
with pytest.raises(MlflowException, match="Exactly one of") as exc:
store.get_secret_info(secret_id="id", secret_name="name")
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_get_gateway_secret_info_not_found(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found") as exc:
store.get_secret_info(secret_id="nonexistent")
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_update_gateway_secret(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="rotate-me",
secret_value={"api_key": "old-value"},
)
original_updated_at = created.last_updated_at
updated = store.update_gateway_secret(
secret_id=created.secret_id,
secret_value={"api_key": "new-value"},
updated_by="rotator-user",
)
assert updated.secret_id == created.secret_id
assert updated.last_updated_by == "rotator-user"
assert updated.last_updated_at > original_updated_at
def test_update_gateway_secret_with_auth_config(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="auth-update",
secret_value={"api_key": "value"},
auth_config={"region": "us-east-1"},
)
store.update_gateway_secret(
secret_id=created.secret_id,
secret_value={"api_key": "new-value"},
auth_config={"region": "eu-west-1", "new_key": "new_value"},
)
def test_update_gateway_secret_clear_auth_config(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="clear-auth",
secret_value={"api_key": "value"},
auth_config={"region": "us-east-1"},
)
store.update_gateway_secret(
secret_id=created.secret_id,
secret_value={"api_key": "new-value"},
auth_config={},
)
def test_delete_gateway_secret(store: SqlAlchemyStore):
created = store.create_gateway_secret(
secret_name="to-delete", secret_value={"api_key": "value"}
)
store.delete_gateway_secret(created.secret_id)
with pytest.raises(MlflowException, match="not found"):
store.get_secret_info(secret_id=created.secret_id)
def test_list_gateway_secret_infos(store: SqlAlchemyStore):
s1 = store.create_gateway_secret(
secret_name="openai-1", secret_value={"api_key": "v1"}, provider="openai"
)
s2 = store.create_gateway_secret(
secret_name="openai-2", secret_value={"api_key": "v2"}, provider="openai"
)
s3 = store.create_gateway_secret(
secret_name="anthropic-1", secret_value={"api_key": "v3"}, provider="anthropic"
)
created_ids = {s1.secret_id, s2.secret_id, s3.secret_id}
all_secrets = store.list_secret_infos()
all_ids = {s.secret_id for s in all_secrets}
assert created_ids.issubset(all_ids)
openai_secrets = store.list_secret_infos(provider="openai")
openai_ids = {s.secret_id for s in openai_secrets}
assert {s1.secret_id, s2.secret_id}.issubset(openai_ids)
assert s3.secret_id not in openai_ids
assert all(s.provider == "openai" for s in openai_secrets)
def test_secret_id_and_name_are_immutable_at_database_level(store: SqlAlchemyStore):
"""
Verify that secret_id and secret_name cannot be modified at the database level.
These fields are used as AAD (Additional Authenticated Data) in AES-GCM encryption.
If they are modified, decryption will fail. A database trigger enforces this immutability
to prevent any code path from accidentally allowing mutation.
"""
from sqlalchemy import text
from sqlalchemy.exc import DatabaseError, IntegrityError, OperationalError
secret = store.create_gateway_secret(
secret_name="immutable-test",
secret_value={"api_key": "test-value"},
provider="openai",
)
def attempt_mutation(session):
session.execute(
text("UPDATE secrets SET secret_name = :new_name WHERE secret_id = :id"),
{"new_name": "modified-name", "id": secret.secret_id},
)
session.flush()
with store.ManagedSessionMaker(read_only=False) as session:
with pytest.raises((DatabaseError, IntegrityError, OperationalError)):
attempt_mutation(session)
retrieved = store.get_secret_info(secret_id=secret.secret_id)
assert retrieved.secret_name == "immutable-test"
# =============================================================================
# Model Definition Operations
# =============================================================================
def test_create_gateway_model_definition(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="test-key", secret_value={"api_key": "value"})
model_def = store.create_gateway_model_definition(
name="gpt-4-turbo",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4-turbo-preview",
created_by="test-user",
)
assert isinstance(model_def, GatewayModelDefinition)
assert model_def.model_definition_id.startswith("d-")
assert model_def.name == "gpt-4-turbo"
assert model_def.secret_id == secret.secret_id
assert model_def.secret_name == "test-key"
assert model_def.provider == "openai"
assert model_def.model_name == "gpt-4-turbo-preview"
def test_create_gateway_model_definition_duplicate_name_raises(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="dup-key", secret_value={"api_key": "value"})
store.create_gateway_model_definition(
name="duplicate-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
with pytest.raises(MlflowException, match="already exists") as exc:
store.create_gateway_model_definition(
name="duplicate-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_ALREADY_EXISTS)
def test_create_gateway_model_definition_nonexistent_secret_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found") as exc:
store.create_gateway_model_definition(
name="orphan-model",
secret_id="nonexistent",
provider="openai",
model_name="gpt-4",
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_get_gateway_model_definition_by_id(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="get-key", secret_value={"api_key": "value"})
created = store.create_gateway_model_definition(
name="model-by-id",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3-sonnet",
)
retrieved = store.get_gateway_model_definition(model_definition_id=created.model_definition_id)
assert retrieved.model_definition_id == created.model_definition_id
assert retrieved.name == "model-by-id"
def test_get_gateway_model_definition_by_name(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="name-key", secret_value={"api_key": "value"})
created = store.create_gateway_model_definition(
name="model-by-name",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
retrieved = store.get_gateway_model_definition(name="model-by-name")
assert retrieved.model_definition_id == created.model_definition_id
def test_get_gateway_model_definition_requires_one_of_id_or_name(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="Exactly one of") as exc:
store.get_gateway_model_definition()
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_list_gateway_model_definitions(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="list-key", secret_value={"api_key": "value"})
store.create_gateway_model_definition(
name="list-model-1", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
store.create_gateway_model_definition(
name="list-model-2",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3",
)
all_defs = store.list_gateway_model_definitions()
assert len(all_defs) >= 2
openai_defs = store.list_gateway_model_definitions(provider="openai")
assert all(d.provider == "openai" for d in openai_defs)
def test_update_gateway_model_definition(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="update-key", secret_value={"api_key": "value"}
)
created = store.create_gateway_model_definition(
name="update-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
updated = store.update_gateway_model_definition(
model_definition_id=created.model_definition_id,
model_name="gpt-4-turbo",
updated_by="updater",
)
assert updated.model_name == "gpt-4-turbo"
assert updated.last_updated_by == "updater"
def test_delete_gateway_model_definition(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="delete-key", secret_value={"api_key": "value"}
)
created = store.create_gateway_model_definition(
name="delete-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
store.delete_gateway_model_definition(created.model_definition_id)
with pytest.raises(MlflowException, match="not found"):
store.get_gateway_model_definition(model_definition_id=created.model_definition_id)
def test_delete_gateway_model_definition_in_use_raises(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="in-use-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="in-use-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
store.create_gateway_endpoint(
name="uses-model",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
with pytest.raises(MlflowException, match="currently in use") as exc:
store.delete_gateway_model_definition(model_def.model_definition_id)
assert exc.value.error_code == ErrorCode.Name(INVALID_STATE)
# =============================================================================
# Endpoint Operations
# =============================================================================
def test_create_gateway_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="ep-key", secret_value={"api_key": "value"})
model_def = store.create_gateway_model_definition(
name="ep-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
# Create an experiment to link with the endpoint
experiment_id = store.create_experiment("test-experiment")
endpoint = 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,
),
],
created_by="test-user",
usage_tracking=True,
experiment_id=experiment_id,
)
assert isinstance(endpoint, GatewayEndpoint)
assert endpoint.endpoint_id.startswith("e-")
assert endpoint.name == "my-endpoint"
assert len(endpoint.model_mappings) == 1
assert endpoint.model_mappings[0].model_definition_id == model_def.model_definition_id
assert endpoint.usage_tracking is True
assert endpoint.experiment_id == experiment_id
def test_create_gateway_endpoint_auto_creates_experiment(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="auto-exp-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="auto-exp-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="auto-exp-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
)
assert endpoint.usage_tracking is True
assert endpoint.experiment_id is not None
experiment = store.get_experiment(endpoint.experiment_id)
assert experiment.name == "gateway/auto-exp-endpoint"
assert experiment.tags.get("mlflow.experiment.sourceType") == "GATEWAY"
assert experiment.tags.get("mlflow.experiment.sourceId") == endpoint.endpoint_id
assert experiment.tags.get("mlflow.experiment.isGateway") == "true"
def test_create_gateway_endpoint_usage_tracking_defaults_to_true(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="default-ut-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="default-ut-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
# Create endpoint without specifying usage_tracking
endpoint = store.create_gateway_endpoint(
name="default-ut-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
assert endpoint.usage_tracking is True
# An experiment should be auto-created since usage_tracking defaults to True
assert endpoint.experiment_id is not None
def test_create_gateway_endpoint_empty_models_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="at least one") as exc:
store.create_gateway_endpoint(name="empty-endpoint", model_configs=[])
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_create_gateway_endpoint_nonexistent_model_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found") as exc:
store.create_gateway_endpoint(
name="orphan-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id="nonexistent",
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_get_gateway_endpoint_by_id(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="get-ep-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="get-ep-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
created = store.create_gateway_endpoint(
name="get-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
retrieved = store.get_gateway_endpoint(endpoint_id=created.endpoint_id)
assert retrieved.endpoint_id == created.endpoint_id
assert retrieved.name == "get-endpoint"
def test_get_gateway_endpoint_by_name(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="name-ep-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="name-ep-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
created = store.create_gateway_endpoint(
name="named-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
retrieved = store.get_gateway_endpoint(name="named-endpoint")
assert retrieved.endpoint_id == created.endpoint_id
def test_get_gateway_endpoint_requires_one_of_id_or_name(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="Exactly one of") as exc:
store.get_gateway_endpoint()
assert exc.value.error_code == ErrorCode.Name(INVALID_PARAMETER_VALUE)
def test_update_gateway_endpoint(store: SqlAlchemyStore):
secret1 = store.create_gateway_secret(
secret_name="upd-ep-key1", secret_value={"api_key": "value1"}
)
secret2 = store.create_gateway_secret(
secret_name="upd-ep-key2", secret_value={"api_key": "value2"}
)
secret3 = store.create_gateway_secret(
secret_name="upd-ep-key3", secret_value={"api_key": "value3"}
)
secret4 = store.create_gateway_secret(
secret_name="upd-ep-key4", secret_value={"api_key": "value4"}
)
model_def1 = store.create_gateway_model_definition(
name="upd-ep-model1",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="upd-ep-model2",
secret_id=secret2.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
model_def3 = store.create_gateway_model_definition(
name="upd-ep-model3",
secret_id=secret3.secret_id,
provider="cohere",
model_name="command-r-plus",
)
model_def4 = store.create_gateway_model_definition(
name="upd-ep-model4",
secret_id=secret4.secret_id,
provider="openai",
model_name="gpt-4o",
)
# Create endpoint with model1 as PRIMARY
created = store.create_gateway_endpoint(
name="update-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Verify initial state
assert len(created.model_mappings) == 1
assert created.model_mappings[0].model_definition_id == model_def1.model_definition_id
assert created.model_mappings[0].linkage_type == GatewayModelLinkageType.PRIMARY
assert created.routing_strategy is None
assert created.fallback_config is None
# Test 1: Basic update - rename endpoint
renamed = store.update_gateway_endpoint(
endpoint_id=created.endpoint_id,
name="renamed-endpoint",
updated_by="updater",
)
assert renamed.name == "renamed-endpoint"
assert renamed.last_updated_by == "updater"
# Test 2: Update with routing strategy and fallback config
with_fallback = store.update_gateway_endpoint(
endpoint_id=created.endpoint_id,
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=2,
),
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.FALLBACK,
weight=1.0,
fallback_order=0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def3.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=1,
),
],
updated_by="updater2",
)
assert with_fallback.routing_strategy == RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT
assert with_fallback.fallback_config is not None
assert with_fallback.fallback_config.strategy == FallbackStrategy.SEQUENTIAL
assert with_fallback.fallback_config.max_attempts == 2
assert len(with_fallback.model_mappings) == 3
fallback_mappings = [
m
for m in with_fallback.model_mappings
if m.linkage_type == GatewayModelLinkageType.FALLBACK
]
assert len(fallback_mappings) == 2
assert with_fallback.last_updated_by == "updater2"
# Test 3: Update PRIMARY models and FALLBACK models
with_new_models = store.update_gateway_endpoint(
endpoint_id=created.endpoint_id,
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def3.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def4.model_definition_id,
linkage_type=GatewayModelLinkageType.FALLBACK,
weight=1.0,
fallback_order=0,
),
],
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=1,
),
updated_by="updater3",
)
# Verify PRIMARY models were replaced (in model_mappings)
assert len(with_new_models.model_mappings) == 3
primary_mappings = [
m
for m in with_new_models.model_mappings
if m.linkage_type == GatewayModelLinkageType.PRIMARY
]
assert len(primary_mappings) == 2
primary_model_ids = {m.model_definition_id for m in primary_mappings}
assert primary_model_ids == {model_def2.model_definition_id, model_def3.model_definition_id}
def test_delete_gateway_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="del-ep-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="del-ep-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
created = store.create_gateway_endpoint(
name="delete-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.delete_gateway_endpoint(created.endpoint_id)
with pytest.raises(MlflowException, match="not found"):
store.get_gateway_endpoint(endpoint_id=created.endpoint_id)
def test_list_gateway_endpoints(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="list-ep-key", secret_value={"api_key": "value"}
)
secret_fallback = store.create_gateway_secret(
secret_name="list-ep-fallback-key", secret_value={"api_key": "fallback-value"}
)
model_def = store.create_gateway_model_definition(
name="list-ep-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
model_def_fallback = store.create_gateway_model_definition(
name="list-ep-fallback-model",
secret_id=secret_fallback.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
# Create endpoint without fallback
ep1 = store.create_gateway_endpoint(
name="list-endpoint-1",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Create endpoint with fallback config and routing strategy
ep2 = store.create_gateway_endpoint(
name="list-endpoint-2",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
GatewayEndpointModelConfig(
model_definition_id=model_def_fallback.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=2,
),
)
endpoints = store.list_gateway_endpoints()
assert len(endpoints) >= 2
# Find our test endpoints
found_ep1 = next((e for e in endpoints if e.endpoint_id == ep1.endpoint_id), None)
found_ep2 = next((e for e in endpoints if e.endpoint_id == ep2.endpoint_id), None)
assert found_ep1 is not None
assert found_ep2 is not None
# =============================================================================
# Model Mapping Operations
# =============================================================================
def test_attach_model_to_gateway_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="attach-key", secret_value={"api_key": "value"}
)
model_def1 = store.create_gateway_model_definition(
name="attach-model-1", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
model_def2 = store.create_gateway_model_definition(
name="attach-model-2",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3",
)
endpoint = store.create_gateway_endpoint(
name="attach-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
mapping = store.attach_model_to_endpoint(
endpoint_id=endpoint.endpoint_id,
model_config=GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=2.0,
),
created_by="attacher",
)
assert isinstance(mapping, GatewayEndpointModelMapping)
assert mapping.mapping_id.startswith("m-")
assert mapping.endpoint_id == endpoint.endpoint_id
assert mapping.model_definition_id == model_def2.model_definition_id
assert mapping.weight == 2.0
def test_attach_duplicate_model_raises(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="dup-attach-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="dup-attach-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="dup-attach-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
with pytest.raises(MlflowException, match="already attached") as exc:
store.attach_model_to_endpoint(
endpoint_id=endpoint.endpoint_id,
model_config=GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_ALREADY_EXISTS)
def test_detach_model_from_gateway_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="detach-key", secret_value={"api_key": "value"}
)
model_def1 = store.create_gateway_model_definition(
name="detach-model-1",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="detach-model-2",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3",
)
endpoint = store.create_gateway_endpoint(
name="detach-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,
),
],
)
store.detach_model_from_endpoint(
endpoint_id=endpoint.endpoint_id,
model_definition_id=model_def1.model_definition_id,
)
updated_endpoint = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(updated_endpoint.model_mappings) == 1
model_def_id = updated_endpoint.model_mappings[0].model_definition_id
assert model_def_id == model_def2.model_definition_id
def test_detach_nonexistent_mapping_raises(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="no-map-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="no-map-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="no-map-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
with pytest.raises(MlflowException, match="not attached") as exc:
store.detach_model_from_endpoint(
endpoint_id=endpoint.endpoint_id,
model_definition_id="nonexistent-model",
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
# =============================================================================
# Binding Operations
# =============================================================================
def test_create_gateway_endpoint_binding(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="bind-key", secret_value={"api_key": "value"})
model_def = store.create_gateway_model_definition(
name="bind-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="bind-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
binding = store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="job-123",
created_by="binder",
)
assert isinstance(binding, GatewayEndpointBinding)
assert binding.endpoint_id == endpoint.endpoint_id
assert binding.resource_type == "scorer"
assert binding.resource_id == "job-123"
assert binding.created_by == "binder"
def test_delete_gateway_endpoint_binding(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="del-bind-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="del-bind-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="del-bind-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="job-456",
)
store.delete_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="job-456",
)
bindings = store.list_endpoint_bindings(endpoint_id=endpoint.endpoint_id)
assert len(bindings) == 0
def test_list_gateway_endpoint_bindings(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="list-bind-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="list-bind-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="list-bind-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="job-1",
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="job-2",
)
bindings = store.list_endpoint_bindings(endpoint_id=endpoint.endpoint_id)
assert len(bindings) == 2
filtered = store.list_endpoint_bindings(resource_type="scorer", resource_id="job-1")
assert len(filtered) == 1
assert filtered[0].resource_id == "job-1"
# =============================================================================
# Config Resolver Operations
# =============================================================================
def test_get_resource_gateway_endpoint_configs(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="resolver-key",
secret_value={"api_key": "sk-secret-value-123"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="resolver-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4-turbo",
)
endpoint = store.create_gateway_endpoint(
name="resolver-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="resolver-job-123",
)
configs = get_resource_endpoint_configs(
resource_type="scorer",
resource_id="resolver-job-123",
store=store,
)
assert len(configs) == 1
config = configs[0]
assert config.endpoint_id == endpoint.endpoint_id
assert config.endpoint_name == "resolver-endpoint"
assert len(config.models) == 1
model_config = config.models[0]
assert model_config.model_definition_id == model_def.model_definition_id
assert model_config.provider == "openai"
assert model_config.model_name == "gpt-4-turbo"
assert model_config.secret_value == {"api_key": "sk-secret-value-123"}
def test_get_resource_endpoint_configs_with_auth_config(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="auth-resolver-key",
secret_value={"api_key": "aws-secret"},
provider="bedrock",
auth_config={"region": "us-east-1", "profile": "default"},
)
model_def = store.create_gateway_model_definition(
name="auth-resolver-model",
secret_id=secret.secret_id,
provider="bedrock",
model_name="anthropic.claude-3",
)
endpoint = store.create_gateway_endpoint(
name="auth-resolver-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="auth-job",
)
configs = get_resource_endpoint_configs(
resource_type="scorer",
resource_id="auth-job",
store=store,
)
model_config = configs[0].models[0]
assert model_config.auth_config == {"region": "us-east-1", "profile": "default"}
def test_get_resource_endpoint_configs_with_dict_secret(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="aws-creds",
secret_value={
"aws_access_key_id": "AKIA1234567890",
"aws_secret_access_key": "secret-key-value",
},
provider="bedrock",
auth_config={"auth_mode": "access_keys", "aws_region_name": "us-west-2"},
)
model_def = store.create_gateway_model_definition(
name="aws-model",
secret_id=secret.secret_id,
provider="bedrock",
model_name="anthropic.claude-3",
)
endpoint = store.create_gateway_endpoint(
name="aws-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint.endpoint_id,
resource_type="scorer",
resource_id="aws-job",
)
configs = get_resource_endpoint_configs(
resource_type="scorer",
resource_id="aws-job",
store=store,
)
model_config = configs[0].models[0]
assert model_config.secret_value == {
"aws_access_key_id": "AKIA1234567890",
"aws_secret_access_key": "secret-key-value",
}
assert model_config.auth_config == {
"auth_mode": "access_keys",
"aws_region_name": "us-west-2",
}
def test_get_resource_endpoint_configs_no_bindings(store: SqlAlchemyStore):
configs = get_resource_endpoint_configs(
resource_type="scorer",
resource_id="nonexistent-resource",
store=store,
)
assert configs == []
def test_get_resource_endpoint_configs_multiple_endpoints(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="multi-key", secret_value={"api_key": "value"})
model_def1 = store.create_gateway_model_definition(
name="multi-model-1",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="multi-model-2",
secret_id=secret.secret_id,
provider="anthropic",
model_name="claude-3",
)
endpoint1 = store.create_gateway_endpoint(
name="multi-endpoint-1",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def1.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
endpoint2 = store.create_gateway_endpoint(
name="multi-endpoint-2",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def2.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.create_endpoint_binding(
endpoint_id=endpoint1.endpoint_id,
resource_type="scorer",
resource_id="multi-resource",
)
store.create_endpoint_binding(
endpoint_id=endpoint2.endpoint_id,
resource_type="scorer",
resource_id="multi-resource",
)
configs = get_resource_endpoint_configs(
resource_type="scorer",
resource_id="multi-resource",
store=store,
)
assert len(configs) == 2
endpoint_names = {c.endpoint_name for c in configs}
assert endpoint_names == {"multi-endpoint-1", "multi-endpoint-2"}
def test_get_gateway_endpoint_config(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="ep-config-key",
secret_value={"api_key": "sk-endpoint-secret-789"},
provider="openai",
)
model_def = store.create_gateway_model_definition(
name="ep-config-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4o",
)
endpoint = store.create_gateway_endpoint(
name="ep-config-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
config = get_endpoint_config(
endpoint_name=endpoint.name,
store=store,
)
assert config.endpoint_id == endpoint.endpoint_id
assert config.endpoint_name == "ep-config-endpoint"
assert len(config.models) == 1
model_config = config.models[0]
assert model_config.model_definition_id == model_def.model_definition_id
assert model_config.provider == "openai"
assert model_config.model_name == "gpt-4o"
assert model_config.secret_value == {"api_key": "sk-endpoint-secret-789"}
def test_get_gateway_endpoint_config_with_auth_config(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="ep-auth-key",
secret_value={"api_key": "bedrock-secret"},
provider="bedrock",
auth_config={"region": "eu-west-1", "project_id": "test-project"},
)
model_def = store.create_gateway_model_definition(
name="ep-auth-model",
secret_id=secret.secret_id,
provider="bedrock",
model_name="anthropic.claude-3-sonnet",
)
endpoint = store.create_gateway_endpoint(
name="ep-auth-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
config = get_endpoint_config(
endpoint_name=endpoint.name,
store=store,
)
model_config = config.models[0]
assert model_config.auth_config == {"region": "eu-west-1", "project_id": "test-project"}
def test_get_gateway_endpoint_config_multiple_models(store: SqlAlchemyStore):
secret1 = store.create_gateway_secret(
secret_name="ep-multi-key-1", secret_value={"api_key": "secret-1"}
)
secret2 = store.create_gateway_secret(
secret_name="ep-multi-key-2", secret_value={"api_key": "secret-2"}
)
model_def1 = store.create_gateway_model_definition(
name="ep-multi-model-1",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="ep-multi-model-2",
secret_id=secret2.secret_id,
provider="anthropic",
model_name="claude-3-opus",
)
endpoint = store.create_gateway_endpoint(
name="ep-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,
),
],
)
config = get_endpoint_config(
endpoint_name=endpoint.name,
store=store,
)
assert len(config.models) == 2
providers = {m.provider for m in config.models}
assert providers == {"openai", "anthropic"}
model_names = {m.model_name for m in config.models}
assert model_names == {"gpt-4", "claude-3-opus"}
def test_get_gateway_endpoint_config_nonexistent_endpoint_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found") as exc:
get_endpoint_config(
endpoint_name="nonexistent-endpoint",
store=store,
)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_get_gateway_endpoint_config_experiment_id_is_string(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="exp-id-test-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="exp-id-test-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="exp-id-test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
usage_tracking=True,
)
config = get_endpoint_config(
endpoint_name=endpoint.name,
store=store,
)
# Verify experiment_id is a string (not an integer from SQLAlchemy)
assert config.experiment_id is not None
assert isinstance(config.experiment_id, str)
# =============================================================================
# Serialization & Caching Tests
# =============================================================================
def test_gateway_model_config_roundtrip():
original = GatewayModelConfig(
model_definition_id="md-123",
provider="openai",
model_name="gpt-4",
secret_value={"api_key": "sk-test"},
auth_config={"region": "us-east-1"},
weight=0.7,
linkage_type=GatewayModelLinkageType.FALLBACK,
fallback_order=2,
)
serialized = json.loads(json.dumps(original.to_dict()))
restored = GatewayModelConfig.from_dict(serialized)
assert restored == original
assert isinstance(restored.linkage_type, GatewayModelLinkageType)
def test_gateway_endpoint_config_roundtrip_with_fallback():
original = GatewayEndpointConfig(
endpoint_id="e-abc",
endpoint_name="my-ep",
models=[
GatewayModelConfig(
model_definition_id="md-1",
provider="openai",
model_name="gpt-4",
secret_value={"api_key": "sk-1"},
linkage_type=GatewayModelLinkageType.PRIMARY,
),
GatewayModelConfig(
model_definition_id="md-2",
provider="anthropic",
model_name="claude-3",
secret_value={"api_key": "sk-2"},
linkage_type=GatewayModelLinkageType.FALLBACK,
fallback_order=0,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=3,
),
experiment_id="42",
)
serialized = json.loads(json.dumps(original.to_dict()))
restored = GatewayEndpointConfig.from_dict(serialized)
assert restored == original
assert isinstance(restored.routing_strategy, RoutingStrategy)
assert isinstance(restored.models[0].linkage_type, GatewayModelLinkageType)
assert isinstance(restored.fallback_config.strategy, FallbackStrategy)
def test_gateway_endpoint_config_roundtrip_no_fallback():
original = GatewayEndpointConfig(
endpoint_id="e-xyz",
endpoint_name="simple-ep",
models=[
GatewayModelConfig(
model_definition_id="md-1",
provider="openai",
model_name="gpt-4",
secret_value={"api_key": "sk-1"},
),
],
)
serialized = json.loads(json.dumps(original.to_dict()))
restored = GatewayEndpointConfig.from_dict(serialized)
assert restored == original
def test_endpoint_config_cache_hit_skips_db(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="cache-hit-key", secret_value={"api_key": "sk-cache"}
)
model_def = store.create_gateway_model_definition(
name="cache-hit-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="cache-hit-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# First call populates the cache
config1 = get_endpoint_config(endpoint_name=endpoint.name, store=store)
assert config1.endpoint_name == endpoint.name
# Second call should hit the cache and not open a new DB session
original_session_maker = store.ManagedSessionMaker
with mock.patch.object(
store, "ManagedSessionMaker", wraps=original_session_maker
) as mock_session:
config2 = get_endpoint_config(endpoint_name=endpoint.name, store=store)
mock_session.assert_not_called()
assert config2.endpoint_name == config1.endpoint_name
assert config2.models[0].secret_value == config1.models[0].secret_value
def test_endpoint_config_cache_invalidated_on_secret_update(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="cache-inv-key", secret_value={"api_key": "sk-old"}
)
model_def = store.create_gateway_model_definition(
name="cache-inv-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="cache-inv-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Populate cache
config1 = get_endpoint_config(endpoint_name=endpoint.name, store=store)
assert config1.models[0].secret_value == {"api_key": "sk-old"}
# Update secret — should invalidate cache
store.update_gateway_secret(
secret_id=secret.secret_id,
secret_value={"api_key": "sk-new"},
)
# Next call should fetch fresh data from DB
config2 = get_endpoint_config(endpoint_name=endpoint.name, store=store)
assert config2.models[0].secret_value == {"api_key": "sk-new"}
def test_endpoint_config_cache_invalidated_on_endpoint_delete(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="cache-del-key", secret_value={"api_key": "sk-del"}
)
model_def = store.create_gateway_model_definition(
name="cache-del-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="cache-del-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Populate cache
config = get_endpoint_config(endpoint_name=endpoint.name, store=store)
assert config.endpoint_id == endpoint.endpoint_id
# Delete endpoint — should invalidate cache
store.delete_gateway_endpoint(endpoint.endpoint_id)
# Next call should hit DB and raise since endpoint is gone
with pytest.raises(MlflowException, match="not found"):
get_endpoint_config(endpoint_name=endpoint.name, store=store)
def test_gateway_endpoint_config_roundtrip_fallback_with_none_fields():
original = GatewayEndpointConfig(
endpoint_id="e-none",
endpoint_name="none-fallback-ep",
models=[
GatewayModelConfig(
model_definition_id="md-1",
provider="openai",
model_name="gpt-4",
secret_value={"api_key": "sk-1"},
),
],
fallback_config=FallbackConfig(strategy=None, max_attempts=None),
)
serialized = json.loads(json.dumps(original.to_dict()))
restored = GatewayEndpointConfig.from_dict(serialized)
assert restored == original
def test_endpoint_config_cache_workspace_isolation(store: SqlAlchemyStore, workspaces_enabled):
if not workspaces_enabled:
pytest.skip("workspace isolation only relevant when workspaces enabled")
secret = store.create_gateway_secret(
secret_name="ws-iso-key", secret_value={"api_key": "sk-ws"}
)
model_def = store.create_gateway_model_definition(
name="ws-iso-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="ws-iso-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Populate cache in current workspace
config = get_endpoint_config(endpoint_name=endpoint.name, store=store)
assert config.endpoint_id == endpoint.endpoint_id
# Switch to a different workspace — same endpoint name should NOT be served from cache
with WorkspaceContext(f"other-workspace-{uuid.uuid4().hex}"):
with pytest.raises(MlflowException, match="not found"):
get_endpoint_config(endpoint_name=endpoint.name, store=store)
# =============================================================================
# Endpoint Tag Operations
# =============================================================================
def test_set_gateway_endpoint_tag(store: SqlAlchemyStore):
secret = store.create_gateway_secret(secret_name="tag-key", secret_value={"api_key": "value"})
model_def = store.create_gateway_model_definition(
name="tag-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="tag-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
tag = GatewayEndpointTag(key="env", value="production")
store.set_gateway_endpoint_tag(endpoint.endpoint_id, tag)
retrieved = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(retrieved.tags) == 1
assert retrieved.tags[0].key == "env"
assert retrieved.tags[0].value == "production"
def test_set_gateway_endpoint_tag_update_existing(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="tag-upd-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="tag-upd-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="tag-upd-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.set_gateway_endpoint_tag(endpoint.endpoint_id, GatewayEndpointTag(key="env", value="dev"))
store.set_gateway_endpoint_tag(
endpoint.endpoint_id, GatewayEndpointTag(key="env", value="production")
)
retrieved = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(retrieved.tags) == 1
assert retrieved.tags[0].key == "env"
assert retrieved.tags[0].value == "production"
def test_set_multiple_endpoint_tags(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="multi-tag-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="multi-tag-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="multi-tag-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.set_gateway_endpoint_tag(
endpoint.endpoint_id, GatewayEndpointTag(key="env", value="production")
)
store.set_gateway_endpoint_tag(endpoint.endpoint_id, GatewayEndpointTag(key="team", value="ml"))
store.set_gateway_endpoint_tag(
endpoint.endpoint_id, GatewayEndpointTag(key="version", value="v1")
)
retrieved = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(retrieved.tags) == 3
tag_dict = {t.key: t.value for t in retrieved.tags}
assert tag_dict == {"env": "production", "team": "ml", "version": "v1"}
def test_set_gateway_endpoint_tag_nonexistent_endpoint_raises(store: SqlAlchemyStore):
tag = GatewayEndpointTag(key="env", value="production")
with pytest.raises(MlflowException, match="not found") as exc:
store.set_gateway_endpoint_tag("nonexistent-endpoint", tag)
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_delete_gateway_endpoint_tag(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="del-tag-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="del-tag-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="del-tag-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.set_gateway_endpoint_tag(
endpoint.endpoint_id, GatewayEndpointTag(key="env", value="production")
)
store.set_gateway_endpoint_tag(endpoint.endpoint_id, GatewayEndpointTag(key="team", value="ml"))
store.delete_gateway_endpoint_tag(endpoint.endpoint_id, "env")
retrieved = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(retrieved.tags) == 1
assert retrieved.tags[0].key == "team"
def test_delete_gateway_endpoint_tag_nonexistent_endpoint_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found") as exc:
store.delete_gateway_endpoint_tag("nonexistent-endpoint", "env")
assert exc.value.error_code == ErrorCode.Name(RESOURCE_DOES_NOT_EXIST)
def test_delete_gateway_endpoint_tag_nonexistent_key_no_op(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="del-noop-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="del-noop-model", secret_id=secret.secret_id, provider="openai", model_name="gpt-4"
)
endpoint = store.create_gateway_endpoint(
name="del-noop-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
# Should not raise even if tag doesn't exist
store.delete_gateway_endpoint_tag(endpoint.endpoint_id, "nonexistent-key")
retrieved = store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
assert len(retrieved.tags) == 0
def test_endpoint_tags_deleted_with_endpoint(store: SqlAlchemyStore):
secret = store.create_gateway_secret(
secret_name="cascade-tag-key", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name="cascade-tag-model",
secret_id=secret.secret_id,
provider="openai",
model_name="gpt-4",
)
endpoint = store.create_gateway_endpoint(
name="cascade-tag-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=1.0,
),
],
)
store.set_gateway_endpoint_tag(
endpoint.endpoint_id, GatewayEndpointTag(key="env", value="production")
)
store.delete_gateway_endpoint(endpoint.endpoint_id)
with pytest.raises(MlflowException, match="not found"):
store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
# =============================================================================
# Scorer-Endpoint Integration Tests
# =============================================================================
def _create_gateway_endpoint(store: SqlAlchemyStore, name: str) -> GatewayEndpoint:
"""Helper to create a gateway endpoint for scorer tests."""
secret = store.create_gateway_secret(
secret_name=f"{name}-secret", secret_value={"api_key": "value"}
)
model_def = store.create_gateway_model_definition(
name=f"{name}-model", 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,
),
],
)
def test_register_scorer_resolves_endpoint_name_to_id(store: SqlAlchemyStore):
experiment_id = store.create_experiment(f"scorer-endpoint-test-{uuid.uuid4().hex}")
endpoint = _create_gateway_endpoint(store, "test-endpoint")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Rate the response",
}
})
scorer = store.register_scorer(experiment_id, "my-scorer", serialized_scorer)
# The returned scorer should have the endpoint name (resolved from ID)
stored_data = json.loads(scorer._serialized_scorer)
stored_model = stored_data["instructions_judge_pydantic_data"]["model"]
assert stored_model == f"gateway:/{endpoint.name}"
def test_register_scorer_with_nonexistent_endpoint_raises(store: SqlAlchemyStore):
experiment_id = store.create_experiment(f"scorer-nonexistent-endpoint-test-{uuid.uuid4().hex}")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": "gateway:/nonexistent-endpoint",
"instructions": "Rate the response",
}
})
with pytest.raises(MlflowException, match="not found"):
store.register_scorer(experiment_id, "my-scorer", serialized_scorer)
def test_get_scorer_resolves_endpoint_id_to_name(store: SqlAlchemyStore):
experiment_id = store.create_experiment(f"get-scorer-endpoint-test-{uuid.uuid4().hex}")
endpoint = _create_gateway_endpoint(store, "get-test-endpoint")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Rate the response",
}
})
store.register_scorer(experiment_id, "my-scorer", serialized_scorer)
# When retrieving, the endpoint ID should be resolved back to name
retrieved = store.get_scorer(experiment_id, "my-scorer")
retrieved_data = json.loads(retrieved._serialized_scorer)
retrieved_model = retrieved_data["instructions_judge_pydantic_data"]["model"]
assert retrieved_model == f"gateway:/{endpoint.name}"
def test_get_scorer_with_deleted_endpoint_sets_model_to_null(store: SqlAlchemyStore):
experiment_id = store.create_experiment(f"deleted-endpoint-scorer-test-{uuid.uuid4().hex}")
endpoint = _create_gateway_endpoint(store, "to-delete-endpoint")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Rate the response",
}
})
store.register_scorer(experiment_id, "my-scorer", serialized_scorer)
# Delete the endpoint
store.delete_gateway_endpoint(endpoint.endpoint_id)
# Retrieving should set model to null
retrieved = store.get_scorer(experiment_id, "my-scorer")
retrieved_data = json.loads(retrieved._serialized_scorer)
assert retrieved_data["instructions_judge_pydantic_data"]["model"] is None
def test_list_scorers_batch_resolves_endpoint_ids(store: SqlAlchemyStore):
experiment_id = store.create_experiment(f"list-scorers-endpoint-test-{uuid.uuid4().hex}")
endpoint1 = _create_gateway_endpoint(store, "list-endpoint-1")
endpoint2 = _create_gateway_endpoint(store, "list-endpoint-2")
# Create scorers with different endpoints
store.register_scorer(
experiment_id,
"scorer-1",
json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint1.name}",
"instructions": "Rate 1",
}
}),
)
store.register_scorer(
experiment_id,
"scorer-2",
json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint2.name}",
"instructions": "Rate 2",
}
}),
)
store.register_scorer(
experiment_id,
"scorer-3",
json.dumps({
"instructions_judge_pydantic_data": {
"model": "openai:/gpt-4",
"instructions": "Rate 3",
}
}),
)
scorers = store.list_scorers(experiment_id)
assert len(scorers) == 3
# Find each scorer and verify model resolution
scorer_map = {s.scorer_name: s for s in scorers}
data1 = json.loads(scorer_map["scorer-1"]._serialized_scorer)
assert data1["instructions_judge_pydantic_data"]["model"] == f"gateway:/{endpoint1.name}"
data2 = json.loads(scorer_map["scorer-2"]._serialized_scorer)
assert data2["instructions_judge_pydantic_data"]["model"] == f"gateway:/{endpoint2.name}"
data3 = json.loads(scorer_map["scorer-3"]._serialized_scorer)
assert data3["instructions_judge_pydantic_data"]["model"] == "openai:/gpt-4"
# =============================================================================
# Fallback Routing Tests
# =============================================================================
def test_create_gateway_endpoint_with_fallback_routing(store: SqlAlchemyStore):
# Create secrets and model definitions
secret1 = store.create_gateway_secret(
secret_name="fallback-key-1", secret_value={"api_key": "sk-model1"}
)
secret2 = store.create_gateway_secret(
secret_name="fallback-key-2", secret_value={"api_key": "sk-model2"}
)
model_def1 = store.create_gateway_model_definition(
name="fallback-model-1",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="fallback-model-2",
secret_id=secret2.secret_id,
provider="anthropic",
model_name="claude-3-5-sonnet-20241022",
)
# Create a third model for PRIMARY
secret3 = store.create_gateway_secret(
secret_name="primary-key", secret_value={"api_key": "sk-primary"}
)
model_def3 = store.create_gateway_model_definition(
name="primary-model",
secret_id=secret3.secret_id,
provider="openai",
model_name="gpt-4o",
)
# Create endpoint with fallback configuration
endpoint = store.create_gateway_endpoint(
name="fallback-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def3.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,
),
],
created_by="test-user",
routing_strategy=None, # Fallback is independent of routing strategy
fallback_config=FallbackConfig(
strategy=FallbackStrategy.SEQUENTIAL,
max_attempts=2,
),
)
# Verify endpoint was created with fallback config
assert isinstance(endpoint, GatewayEndpoint)
assert endpoint.endpoint_id.startswith("e-")
assert endpoint.name == "fallback-endpoint"
assert endpoint.routing_strategy is None # No routing strategy needed for fallback
assert endpoint.fallback_config is not None
assert isinstance(endpoint.fallback_config, FallbackConfig)
assert endpoint.fallback_config.strategy == FallbackStrategy.SEQUENTIAL
assert endpoint.fallback_config.max_attempts == 2
assert len(endpoint.model_mappings) == 3
fallback_ids = [
m.model_definition_id
for m in endpoint.model_mappings
if m.linkage_type == GatewayModelLinkageType.FALLBACK
]
assert model_def1.model_definition_id in fallback_ids
assert model_def2.model_definition_id in fallback_ids
# Verify linkage types and fallback_order
# PRIMARY models are in endpoint.model_mappings
assert len(endpoint.model_mappings) == 3
primary_mapping = next(
m for m in endpoint.model_mappings if m.linkage_type == GatewayModelLinkageType.PRIMARY
)
assert primary_mapping.model_definition_id == model_def3.model_definition_id
def test_create_gateway_endpoint_with_traffic_split(store: SqlAlchemyStore):
secret1 = store.create_gateway_secret(
secret_name="traffic-split-key-1", secret_value={"api_key": "sk-test1"}
)
secret2 = store.create_gateway_secret(
secret_name="traffic-split-key-2", secret_value={"api_key": "sk-test2"}
)
model_def1 = store.create_gateway_model_definition(
name="traffic-split-model-1",
secret_id=secret1.secret_id,
provider="openai",
model_name="gpt-4",
)
model_def2 = store.create_gateway_model_definition(
name="traffic-split-model-2",
secret_id=secret2.secret_id,
provider="openai",
model_name="gpt-3.5-turbo",
)
endpoint = store.create_gateway_endpoint(
name="traffic-split-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,
),
],
routing_strategy=RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT,
)
assert endpoint.routing_strategy == RoutingStrategy.REQUEST_BASED_TRAFFIC_SPLIT
assert len(endpoint.model_mappings) == 2
# All should be PRIMARY linkages for traffic split
for mapping in endpoint.model_mappings:
assert mapping.linkage_type == GatewayModelLinkageType.PRIMARY
# =============================================================================
# Budget Policy Operations
# =============================================================================
def test_create_budget_policy(store: SqlAlchemyStore):
policy = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=BudgetDurationUnit.MONTHS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
created_by="admin",
)
assert isinstance(policy, GatewayBudgetPolicy)
assert policy.budget_policy_id.startswith("bp-")
assert policy.budget_unit == BudgetUnit.USD
assert policy.budget_amount == 100.0
assert policy.duration.unit == BudgetDurationUnit.MONTHS
assert policy.duration.value == 1
assert policy.target_scope == BudgetTargetScope.GLOBAL
assert policy.budget_action == BudgetAction.ALERT
assert policy.created_by == "admin"
assert policy.created_at > 0
assert policy.last_updated_at > 0
def test_get_budget_policy_by_id(store: SqlAlchemyStore):
created = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=75.0,
duration=BudgetDuration(unit=BudgetDurationUnit.HOURS, value=24),
target_scope=BudgetTargetScope.WORKSPACE,
budget_action=BudgetAction.REJECT,
)
fetched = store.get_budget_policy(budget_policy_id=created.budget_policy_id)
assert fetched.budget_policy_id == created.budget_policy_id
assert fetched.budget_unit == BudgetUnit.USD
assert fetched.budget_amount == 75.0
assert fetched.duration.unit == BudgetDurationUnit.HOURS
assert fetched.duration.value == 24
assert fetched.target_scope == BudgetTargetScope.WORKSPACE
assert fetched.budget_action == BudgetAction.REJECT
def test_get_budget_policy_not_found_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="BudgetPolicy"):
store.get_budget_policy(budget_policy_id="bp-nonexistent")
def test_update_budget_policy(store: SqlAlchemyStore):
created = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=BudgetDurationUnit.MONTHS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
)
updated = store.update_budget_policy(
budget_policy_id=created.budget_policy_id,
budget_amount=200.0,
budget_action=BudgetAction.REJECT,
updated_by="editor",
)
assert updated.budget_amount == 200.0
assert updated.budget_action == BudgetAction.REJECT
assert updated.last_updated_by == "editor"
assert updated.last_updated_at >= created.last_updated_at
# Unchanged fields should remain
assert updated.duration.unit == BudgetDurationUnit.MONTHS
assert updated.duration.value == 1
assert updated.target_scope == BudgetTargetScope.GLOBAL
def test_update_budget_policy_not_found_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="BudgetPolicy"):
store.update_budget_policy(
budget_policy_id="bp-nonexistent",
budget_amount=999.0,
)
def test_delete_budget_policy(store: SqlAlchemyStore):
created = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=10.0,
duration=BudgetDuration(unit=BudgetDurationUnit.DAYS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
)
store.delete_budget_policy(created.budget_policy_id)
with pytest.raises(MlflowException, match="BudgetPolicy"):
store.get_budget_policy(budget_policy_id=created.budget_policy_id)
def test_delete_budget_policy_not_found_raises(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="BudgetPolicy"):
store.delete_budget_policy("bp-nonexistent")
def test_list_budget_policies(store: SqlAlchemyStore):
store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=BudgetDurationUnit.MONTHS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
)
store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=50.0,
duration=BudgetDuration(unit=BudgetDurationUnit.DAYS, value=7),
target_scope=BudgetTargetScope.WORKSPACE,
budget_action=BudgetAction.REJECT,
)
all_policies = store.list_budget_policies()
assert len(all_policies) == 2
assert all_policies.token is None
def test_list_budget_policies_empty(store: SqlAlchemyStore):
policies = store.list_budget_policies()
assert policies == []
assert policies.token is None
def test_list_budget_policies_pagination(store: SqlAlchemyStore):
for i in range(3):
store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0 + i,
duration=BudgetDuration(unit=BudgetDurationUnit.MONTHS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
)
page1 = store.list_budget_policies(max_results=2)
assert len(page1) == 2
assert page1.token is not None
page2 = store.list_budget_policies(max_results=2, page_token=page1.token)
assert len(page2) == 1
assert page2.token is None
all_ids = [p.budget_policy_id for p in page1] + [p.budget_policy_id for p in page2]
assert len(set(all_ids)) == 3
def test_list_budget_policies_invalid_max_results(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="max_results"):
store.list_budget_policies(max_results=0)
def test_create_budget_policy_all_duration_units(store: SqlAlchemyStore):
for du in BudgetDurationUnit:
policy = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=du, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
)
assert policy.duration.unit == du
def test_create_budget_policy_all_budget_actions(store: SqlAlchemyStore):
for action in BudgetAction:
policy = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=BudgetDurationUnit.MONTHS, value=1),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=action,
)
assert policy.budget_action == action
# =============================================================================
# Guardrail Tests
# =============================================================================
def _create_scorer(store: SqlAlchemyStore, endpoint_name: str | None = None):
"""Helper to create a scorer for guardrail tests."""
name_suffix = uuid.uuid4().hex[:8]
ep_name = endpoint_name or f"guardrail-ep-{name_suffix}"
endpoint = _create_gateway_endpoint(store, ep_name)
experiment_id = store.create_experiment(f"guardrail-scorer-exp-{name_suffix}")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Is this input safe?",
}
})
return store.register_scorer(experiment_id, f"safety-judge-{name_suffix}", serialized_scorer)
def _create_scorer_versions(store: SqlAlchemyStore):
"""Helper to create multiple versions of a scorer for guardrail tests."""
name_suffix = uuid.uuid4().hex[:8]
endpoint = _create_gateway_endpoint(store, f"guardrail-ep-{name_suffix}")
experiment_id = store.create_experiment(f"guardrail-scorer-exp-{name_suffix}")
scorer_name = f"safety-judge-{name_suffix}"
serialized_scorer_v1 = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Is this input safe?",
}
})
serialized_scorer_v2 = json.dumps({
"instructions_judge_pydantic_data": {
"model": f"gateway:/{endpoint.name}",
"instructions": "Is this input still safe?",
}
})
scorer_v1 = store.register_scorer(experiment_id, scorer_name, serialized_scorer_v1)
scorer_v2 = store.register_scorer(experiment_id, scorer_name, serialized_scorer_v2)
return scorer_v1, scorer_v2
def test_create_gateway_guardrail(store: SqlAlchemyStore):
scorer = _create_scorer(store)
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
created_by="test-user",
)
assert isinstance(guardrail, GatewayGuardrail)
assert guardrail.guardrail_id.startswith("gr-")
assert guardrail.name == "test-guardrail"
assert guardrail.scorer.scorer_id == scorer.scorer_id
assert guardrail.scorer.scorer_version == scorer.scorer_version
assert guardrail.stage == GuardrailStage.BEFORE
assert guardrail.action == GuardrailAction.VALIDATION
assert guardrail.created_by == "test-user"
assert guardrail.last_updated_by == "test-user"
assert guardrail.created_at > 0
assert guardrail.last_updated_at > 0
def test_create_gateway_guardrail_after_sanitization(store: SqlAlchemyStore):
scorer = _create_scorer(store)
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.AFTER,
action=GuardrailAction.SANITIZATION,
)
assert guardrail.stage == GuardrailStage.AFTER
assert guardrail.action == GuardrailAction.SANITIZATION
def test_create_gateway_guardrail_rejects_unknown_scorer(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="Scorer with ID 'missing-scorer' not found"):
store.create_gateway_guardrail(
name="test-guardrail",
scorer_id="missing-scorer",
scorer_version=1,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
def test_create_gateway_guardrail_rejects_unknown_scorer_version(store: SqlAlchemyStore):
scorer = _create_scorer(store)
with pytest.raises(
MlflowException,
match=rf"Scorer with ID '{scorer.scorer_id}' and version 999 not found",
):
store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=999,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
def test_get_gateway_guardrail(store: SqlAlchemyStore):
scorer = _create_scorer(store)
created = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
fetched = store.get_gateway_guardrail(created.guardrail_id)
assert fetched.guardrail_id == created.guardrail_id
assert fetched.scorer.scorer_id == scorer.scorer_id
assert fetched.scorer.scorer_version == scorer.scorer_version
assert fetched.stage == GuardrailStage.BEFORE
assert fetched.action == GuardrailAction.VALIDATION
def test_get_gateway_guardrail_not_found(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found"):
store.get_gateway_guardrail("gr-nonexistent")
def test_delete_gateway_guardrail(store: SqlAlchemyStore):
scorer = _create_scorer(store)
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.delete_gateway_guardrail(guardrail.guardrail_id)
with pytest.raises(MlflowException, match="not found"):
store.get_gateway_guardrail(guardrail.guardrail_id)
def test_delete_gateway_guardrail_not_found(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found"):
store.delete_gateway_guardrail("gr-nonexistent")
def test_list_gateway_guardrails(store: SqlAlchemyStore):
scorer = _create_scorer(store)
for stage in GuardrailStage:
store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=stage,
action=GuardrailAction.VALIDATION,
)
result = store.list_gateway_guardrails()
assert len(result) == 2
assert result.token is None
def test_list_gateway_guardrails_empty(store: SqlAlchemyStore):
result = store.list_gateway_guardrails()
assert result == []
assert result.token is None
def test_list_gateway_guardrails_pagination(store: SqlAlchemyStore):
scorer = _create_scorer(store)
for _ in range(3):
store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
page1 = store.list_gateway_guardrails(max_results=2)
assert len(page1) == 2
assert page1.token is not None
page2 = store.list_gateway_guardrails(max_results=2, page_token=page1.token)
assert len(page2) == 1
assert page2.token is None
all_ids = sorted([g.guardrail_id for g in page1] + [g.guardrail_id for g in page2])
assert len(all_ids) == 3
assert len(set(all_ids)) == 3
def test_add_guardrail_to_endpoint(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-endpoint-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
config = store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
execution_order=5,
created_by="test-user",
)
assert isinstance(config, GatewayGuardrailConfig)
assert config.endpoint_id == endpoint.endpoint_id
assert config.guardrail_id == guardrail.guardrail_id
assert config.execution_order == 5
assert config.created_by == "test-user"
def test_add_guardrail_to_endpoint_null_execution_order(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-null-order-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
config = store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
assert config.execution_order is None
def test_add_guardrail_to_endpoint_duplicate_raises(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-dup-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
with pytest.raises(MlflowException, match="already added"):
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
def test_update_endpoint_guardrail_config(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-update-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
execution_order=5,
)
updated = store.update_endpoint_guardrail_config(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
execution_order=10,
)
assert updated.execution_order == 10
assert updated.guardrail_id == guardrail.guardrail_id
def test_update_endpoint_guardrail_config_not_found(store: SqlAlchemyStore):
endpoint = _create_gateway_endpoint(store, f"gr-upd-nf-{uuid.uuid4().hex[:8]}")
with pytest.raises(MlflowException, match="not found"):
store.update_endpoint_guardrail_config(
endpoint_id=endpoint.endpoint_id,
guardrail_id="gr-nonexistent",
execution_order=1,
)
def test_add_guardrail_to_nonexistent_endpoint_raises(store: SqlAlchemyStore):
scorer = _create_scorer(store)
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
with pytest.raises(MlflowException, match="not found"):
store.add_guardrail_to_endpoint(
endpoint_id="e-nonexistent",
guardrail_id=guardrail.guardrail_id,
)
def test_add_nonexistent_guardrail_to_endpoint_raises(store: SqlAlchemyStore):
endpoint = _create_gateway_endpoint(store, f"gr-noguard-{uuid.uuid4().hex[:8]}")
with pytest.raises(MlflowException, match="not found"):
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id="gr-nonexistent",
)
def test_remove_guardrail_from_endpoint(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-remove-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
store.remove_guardrail_from_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
configs = store.list_endpoint_guardrail_configs(endpoint.endpoint_id)
assert configs == []
def test_remove_guardrail_from_endpoint_not_found(store: SqlAlchemyStore):
endpoint = _create_gateway_endpoint(store, f"gr-removenf-{uuid.uuid4().hex[:8]}")
with pytest.raises(MlflowException, match="not found"):
store.remove_guardrail_from_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id="gr-nonexistent",
)
def test_list_endpoint_guardrail_configs(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-list-{uuid.uuid4().hex[:8]}")
guardrails = [
store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
for _ in range(3)
]
configs = [
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=g.guardrail_id,
execution_order=(i + 1) * 10,
)
for i, g in enumerate(guardrails)
]
configs = store.list_endpoint_guardrail_configs(endpoint.endpoint_id)
assert len(configs) == 3
assert [c.execution_order for c in configs] == [10, 20, 30]
assert [c.guardrail_id for c in configs] == [g.guardrail_id for g in guardrails]
for config, guardrail in zip(configs, guardrails):
assert config.guardrail is not None
assert config.guardrail.guardrail_id == guardrail.guardrail_id
assert config.guardrail.stage == GuardrailStage.BEFORE
def test_list_endpoint_guardrail_configs_null_order_sorts_last(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-null-last-{uuid.uuid4().hex[:8]}")
guardrail_with_order = store.create_gateway_guardrail(
name="test-guardrail-with-order",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
guardrail_null_order = store.create_gateway_guardrail(
name="test-guardrail-null-order",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail_null_order.guardrail_id,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail_with_order.guardrail_id,
execution_order=5,
)
configs = store.list_endpoint_guardrail_configs(endpoint.endpoint_id)
assert len(configs) == 2
assert configs[0].execution_order == 5
assert configs[0].guardrail_id == guardrail_with_order.guardrail_id
assert configs[1].execution_order is None
assert configs[1].guardrail_id == guardrail_null_order.guardrail_id
def test_list_endpoint_guardrail_configs_empty(store: SqlAlchemyStore):
endpoint = _create_gateway_endpoint(store, f"gr-empty-{uuid.uuid4().hex[:8]}")
configs = store.list_endpoint_guardrail_configs(endpoint.endpoint_id)
assert configs == []
def test_list_endpoint_guardrail_configs_nonexistent_endpoint(store: SqlAlchemyStore):
with pytest.raises(MlflowException, match="not found"):
store.list_endpoint_guardrail_configs("e-nonexistent")
def test_delete_guardrail_cascades_to_configs(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-cascade-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
store.delete_gateway_guardrail(guardrail.guardrail_id)
configs = store.list_endpoint_guardrail_configs(endpoint.endpoint_id)
assert configs == []
def test_delete_endpoint_cascades_to_guardrail_configs(store: SqlAlchemyStore):
scorer = _create_scorer(store)
endpoint = _create_gateway_endpoint(store, f"gr-ep-cascade-{uuid.uuid4().hex[:8]}")
guardrail = store.create_gateway_guardrail(
name="test-guardrail",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
)
store.add_guardrail_to_endpoint(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
)
store.delete_gateway_endpoint(endpoint.endpoint_id)
# Guardrail itself should still exist
fetched = store.get_gateway_guardrail(guardrail.guardrail_id)
assert fetched.guardrail_id == guardrail.guardrail_id
# =============================================================================
# sum_gateway_trace_cost Tests
# =============================================================================
def _insert_trace_with_cost(
session,
experiment_id,
trace_id,
timestamp_ms,
span_costs,
is_gateway=True,
endpoint_id="ep-test",
):
trace = SqlTraceInfo(
request_id=trace_id,
experiment_id=experiment_id,
timestamp_ms=timestamp_ms,
execution_time_ms=100,
status="OK",
)
session.add(trace)
session.flush()
if is_gateway:
metadata = SqlTraceMetadata(
request_id=trace_id,
key=TraceMetadataKey.GATEWAY_ENDPOINT_ID,
value=endpoint_id,
)
session.add(metadata)
for span_id, cost in span_costs:
span = SqlSpan(
trace_id=trace_id,
experiment_id=experiment_id,
span_id=span_id,
status="OK",
start_time_unix_nano=timestamp_ms * 1_000_000,
end_time_unix_nano=(timestamp_ms + 100) * 1_000_000,
content="{}",
)
session.add(span)
session.flush()
metric = SqlSpanMetrics(
trace_id=trace_id,
span_id=span_id,
key=SpanMetricKey.TOTAL_COST,
value=cost,
)
session.add(metric)
session.flush()
def test_sum_gateway_trace_cost_basic(store: SqlAlchemyStore):
exp = store.create_experiment("cost-test-basic")
exp_id = int(exp)
with store.ManagedSessionMaker(read_only=False) as session:
_insert_trace_with_cost(session, exp_id, "t1", 1000, [("s1", 0.05), ("s2", 0.03)])
_insert_trace_with_cost(session, exp_id, "t2", 2000, [("s1", 0.10)])
total = store.sum_gateway_trace_cost(start_time_ms=0, end_time_ms=5000)
assert abs(total - 0.18) < 1e-9
def test_sum_gateway_trace_cost_excludes_non_gateway(store: SqlAlchemyStore):
exp = store.create_experiment("cost-test-non-gw")
exp_id = int(exp)
with store.ManagedSessionMaker(read_only=False) as session:
_insert_trace_with_cost(session, exp_id, "gw1", 1000, [("s1", 0.10)], is_gateway=True)
_insert_trace_with_cost(session, exp_id, "nongw1", 1000, [("s1", 0.50)], is_gateway=False)
total = store.sum_gateway_trace_cost(start_time_ms=0, end_time_ms=5000)
assert abs(total - 0.10) < 1e-9
def test_sum_gateway_trace_cost_time_window(store: SqlAlchemyStore):
exp = store.create_experiment("cost-test-window")
exp_id = int(exp)
with store.ManagedSessionMaker(read_only=False) as session:
_insert_trace_with_cost(session, exp_id, "early", 500, [("s1", 0.01)])
_insert_trace_with_cost(session, exp_id, "in-window", 1500, [("s1", 0.05)])
_insert_trace_with_cost(session, exp_id, "late", 3000, [("s1", 0.99)])
# Only in-window trace should be included
total = store.sum_gateway_trace_cost(start_time_ms=1000, end_time_ms=2000)
assert abs(total - 0.05) < 1e-9
def test_sum_gateway_trace_cost_workspace_filter(store: SqlAlchemyStore):
with store.ManagedSessionMaker(read_only=False) as session:
# Create two experiments in different workspaces
exp_ws_a = SqlExperiment(
name=f"cost-ws-a-{uuid.uuid4().hex}",
artifact_location="/tmp/a",
lifecycle_stage="active",
workspace="workspace-a",
)
exp_ws_b = SqlExperiment(
name=f"cost-ws-b-{uuid.uuid4().hex}",
artifact_location="/tmp/b",
lifecycle_stage="active",
workspace="workspace-b",
)
session.add_all([exp_ws_a, exp_ws_b])
session.flush()
_insert_trace_with_cost(session, exp_ws_a.experiment_id, "t-a", 1000, [("s1", 0.10)])
_insert_trace_with_cost(session, exp_ws_b.experiment_id, "t-b", 1000, [("s1", 0.25)])
# Filtering by workspace-a should only include 0.10
total_a = store.sum_gateway_trace_cost(
start_time_ms=0, end_time_ms=5000, workspace="workspace-a"
)
assert abs(total_a - 0.10) < 1e-9
# Filtering by workspace-b should only include 0.25
total_b = store.sum_gateway_trace_cost(
start_time_ms=0, end_time_ms=5000, workspace="workspace-b"
)
assert abs(total_b - 0.25) < 1e-9
# No workspace filter should include both
total_all = store.sum_gateway_trace_cost(start_time_ms=0, end_time_ms=5000)
assert abs(total_all - 0.35) < 1e-9
def test_sum_gateway_trace_cost_empty(store: SqlAlchemyStore):
total = store.sum_gateway_trace_cost(start_time_ms=0, end_time_ms=5000)
assert total == 0.0