mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
340 lines
11 KiB
Python
340 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from src.api.admin.usage.routes import (
|
|
AdminUsageDetailAdapter,
|
|
AdminUsageRecordsAdapter,
|
|
_resolve_replay_model_name,
|
|
)
|
|
|
|
|
|
class _FakeQuery:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
scalar_result: int | None = None,
|
|
all_result: list[Any] | None = None,
|
|
first_result: Any = None,
|
|
) -> None:
|
|
self.scalar_result = scalar_result
|
|
self.all_result = all_result or []
|
|
self.first_result = first_result
|
|
self.options_args: tuple[Any, ...] = ()
|
|
|
|
def outerjoin(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def join(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def filter(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def options(self, *args: Any) -> _FakeQuery:
|
|
self.options_args = args
|
|
return self
|
|
|
|
def order_by(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def offset(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def limit(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
|
return self
|
|
|
|
def scalar(self) -> int | None:
|
|
return self.scalar_result
|
|
|
|
def all(self) -> list[Any]:
|
|
return self.all_result
|
|
|
|
def first(self) -> Any:
|
|
return self.first_result
|
|
|
|
|
|
class _FakeDb:
|
|
def __init__(self, queries: list[_FakeQuery]) -> None:
|
|
self._queries = queries
|
|
self.query_calls: list[tuple[Any, ...]] = []
|
|
|
|
def query(self, *args: Any) -> _FakeQuery:
|
|
self.query_calls.append(args)
|
|
return self._queries[len(self.query_calls) - 1]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_usage_records_returns_model_version_without_request_metadata(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr("src.utils.cache_decorator.get_redis_client_sync", lambda: None)
|
|
|
|
usage = SimpleNamespace(
|
|
id="usage-1",
|
|
request_id=None,
|
|
user_id="user-1",
|
|
api_key_id=None,
|
|
provider_name="google",
|
|
provider_id=None,
|
|
provider_endpoint_id=None,
|
|
provider_api_key_id=None,
|
|
model="gemini-2.5-pro",
|
|
target_model=None,
|
|
input_tokens=120,
|
|
output_tokens=80,
|
|
cache_creation_input_tokens=0,
|
|
cache_read_input_tokens=0,
|
|
total_tokens=200,
|
|
total_cost_usd=Decimal("1.25"),
|
|
actual_total_cost_usd=Decimal("1.25"),
|
|
rate_multiplier=Decimal("1.0"),
|
|
response_time_ms=850,
|
|
first_byte_time_ms=230,
|
|
created_at=datetime(2026, 3, 9, 8, 30, tzinfo=timezone.utc),
|
|
is_stream=False,
|
|
status_code=200,
|
|
error_message=None,
|
|
status="completed",
|
|
api_format="gemini:chat",
|
|
endpoint_api_format=None,
|
|
has_format_conversion=False,
|
|
input_price_per_1m=Decimal("0.10"),
|
|
output_price_per_1m=Decimal("0.30"),
|
|
cache_creation_price_per_1m=None,
|
|
cache_read_price_per_1m=None,
|
|
)
|
|
user = SimpleNamespace(id="user-1", email="user@example.com", username="tester")
|
|
|
|
count_query = _FakeQuery(scalar_result=1)
|
|
data_query = _FakeQuery(
|
|
all_result=[
|
|
(usage, user, None, None, None, "gemini-2.5-pro-001"),
|
|
]
|
|
)
|
|
db = _FakeDb([count_query, data_query])
|
|
context = SimpleNamespace(
|
|
db=db,
|
|
user=SimpleNamespace(id="admin-1"),
|
|
add_audit_metadata=lambda **_: None,
|
|
)
|
|
|
|
adapter = AdminUsageRecordsAdapter(
|
|
time_range=None,
|
|
search=None,
|
|
user_id=None,
|
|
username=None,
|
|
model=None,
|
|
provider=None,
|
|
api_format=None,
|
|
status=None,
|
|
limit=100,
|
|
offset=0,
|
|
)
|
|
|
|
result = await adapter.handle(context)
|
|
|
|
assert len(db.query_calls) == 2
|
|
assert len(db.query_calls[1]) == 6
|
|
assert getattr(db.query_calls[1][-1], "name", None) == "model_version"
|
|
|
|
record = result["records"][0]
|
|
assert record["model_version"] == "gemini-2.5-pro-001"
|
|
assert "request_metadata" not in record
|
|
|
|
usage_load_only = data_query.options_args[0]
|
|
usage_paths = {str(option.path) for option in usage_load_only.context}
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_metadata]" not in usage_paths
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_usage_detail_defers_large_body_columns_when_bodies_excluded(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
async def _fake_get_tiered_pricing_info(
|
|
self: AdminUsageDetailAdapter,
|
|
db: Any,
|
|
usage_record: Any,
|
|
) -> None:
|
|
return None
|
|
|
|
monkeypatch.setattr(
|
|
AdminUsageDetailAdapter,
|
|
"_get_tiered_pricing_info",
|
|
_fake_get_tiered_pricing_info,
|
|
)
|
|
monkeypatch.setattr(
|
|
AdminUsageDetailAdapter,
|
|
"_extract_video_billing_info",
|
|
lambda self, usage_record: None,
|
|
)
|
|
|
|
class _UsageRecord:
|
|
id = "usage-1"
|
|
request_id = "req-1"
|
|
user_id = "user-1"
|
|
api_key_id = "key-1"
|
|
provider_name = "openai"
|
|
api_format = "openai:cli"
|
|
model = "gpt-5.4"
|
|
target_model = None
|
|
input_tokens = 10
|
|
output_tokens = 20
|
|
total_tokens = 30
|
|
cache_creation_input_tokens = 0
|
|
cache_read_input_tokens = 0
|
|
cache_creation_input_tokens_5m = 0
|
|
cache_creation_input_tokens_1h = 0
|
|
input_cost_usd = Decimal("0.001")
|
|
output_cost_usd = Decimal("0.002")
|
|
total_cost_usd = Decimal("0.003")
|
|
cache_creation_cost_usd = Decimal("0")
|
|
cache_read_cost_usd = Decimal("0")
|
|
request_cost_usd = Decimal("0")
|
|
input_price_per_1m = Decimal("0.1")
|
|
output_price_per_1m = Decimal("0.2")
|
|
cache_creation_price_per_1m = None
|
|
cache_read_price_per_1m = None
|
|
price_per_request = None
|
|
request_type = "chat"
|
|
is_stream = True
|
|
status_code = 200
|
|
error_message = None
|
|
status = "completed"
|
|
response_time_ms = 1200
|
|
first_byte_time_ms = 200
|
|
created_at = datetime(2026, 3, 12, 7, 0, tzinfo=timezone.utc)
|
|
request_headers = {"x-test": "1"}
|
|
provider_request_headers = {"authorization": "***"}
|
|
response_headers = {"content-type": "text/event-stream"}
|
|
client_response_headers = {"content-type": "text/event-stream"}
|
|
request_metadata = {"trace_id": "trace-1"}
|
|
|
|
def get_request_body(self) -> Any:
|
|
raise AssertionError("request body should not be loaded")
|
|
|
|
def get_provider_request_body(self) -> Any:
|
|
raise AssertionError("provider request body should not be loaded")
|
|
|
|
def get_response_body(self) -> Any:
|
|
raise AssertionError("response body should not be loaded")
|
|
|
|
def get_client_response_body(self) -> Any:
|
|
raise AssertionError("client response body should not be loaded")
|
|
|
|
class _ApiKeyRecord:
|
|
id = "key-1"
|
|
name = "Primary"
|
|
|
|
def get_display_key(self) -> str:
|
|
return "sk-test"
|
|
|
|
usage_query = _FakeQuery(
|
|
first_result=(_UsageRecord(), True, True, True, True),
|
|
)
|
|
user_query = _FakeQuery(
|
|
first_result=SimpleNamespace(id="user-1", username="tester", email="u@example.com"),
|
|
)
|
|
api_key_query = _FakeQuery(first_result=_ApiKeyRecord())
|
|
db = _FakeDb([usage_query, user_query, api_key_query])
|
|
context = SimpleNamespace(
|
|
db=db,
|
|
user=SimpleNamespace(id="admin-1"),
|
|
add_audit_metadata=lambda **_: None,
|
|
)
|
|
|
|
adapter = AdminUsageDetailAdapter(usage_id="usage-1", include_bodies=False)
|
|
result = await adapter.handle(context) # type: ignore[arg-type]
|
|
|
|
assert result["request_body"] is None
|
|
assert result["provider_request_body"] is None
|
|
assert result["response_body"] is None
|
|
assert result["client_response_body"] is None
|
|
assert result["has_request_body"] is True
|
|
assert result["has_provider_request_body"] is True
|
|
assert result["has_response_body"] is True
|
|
assert result["has_client_response_body"] is True
|
|
|
|
deferred_paths = {
|
|
str(context.path)
|
|
for option in usage_query.options_args
|
|
for context in getattr(option, "context", ())
|
|
}
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body]" in deferred_paths
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body]" in deferred_paths
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body]" in deferred_paths
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body]" in deferred_paths
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body_compressed]" in deferred_paths
|
|
assert (
|
|
"ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body_compressed]" in deferred_paths
|
|
)
|
|
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body_compressed]" in deferred_paths
|
|
assert (
|
|
"ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body_compressed]" in deferred_paths
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_replay_model_name_falls_back_to_source_model_when_mapping_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class _FakeMapper:
|
|
def __init__(self, db: Any) -> None:
|
|
self.db = db
|
|
|
|
async def get_mapping(self, source_model: str, provider_id: str) -> None:
|
|
return None
|
|
|
|
monkeypatch.setattr("src.services.model.mapper.ModelMapperMiddleware", _FakeMapper)
|
|
|
|
resolved_model, mapping_source = await _resolve_replay_model_name(
|
|
SimpleNamespace(),
|
|
source_model="gpt-4o-mini",
|
|
target_provider=SimpleNamespace(id="provider-2", name="OpenAI Compatible"),
|
|
target_endpoint=SimpleNamespace(id="endpoint-2", api_format="openai:responses"),
|
|
target_api_key=None,
|
|
)
|
|
|
|
assert resolved_model == "gpt-4o-mini"
|
|
assert mapping_source == "none"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_replay_model_name_reruns_mapping_for_same_endpoint_replay(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class _FakeModel:
|
|
def select_provider_model_name(
|
|
self, affinity_key: str | None = None, api_format: str | None = None
|
|
) -> str:
|
|
assert affinity_key == "key-2"
|
|
assert api_format == "openai:responses"
|
|
return "provider-model-for-key-2"
|
|
|
|
class _FakeMapper:
|
|
def __init__(self, db: Any) -> None:
|
|
self.db = db
|
|
|
|
async def get_mapping(self, source_model: str, provider_id: str) -> Any:
|
|
assert source_model == "gpt-4o-mini"
|
|
assert provider_id == "provider-2"
|
|
return SimpleNamespace(model=_FakeModel())
|
|
|
|
monkeypatch.setattr("src.services.model.mapper.ModelMapperMiddleware", _FakeMapper)
|
|
|
|
resolved_model, mapping_source = await _resolve_replay_model_name(
|
|
SimpleNamespace(),
|
|
source_model="gpt-4o-mini",
|
|
target_provider=SimpleNamespace(id="provider-2", name="OpenAI Compatible"),
|
|
target_endpoint=SimpleNamespace(id="endpoint-2", api_format="openai:responses"),
|
|
target_api_key=SimpleNamespace(id="key-2"),
|
|
)
|
|
|
|
assert resolved_model == "provider-model-for-key-2"
|
|
assert mapping_source == "model_mapping"
|