mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
perf(admin-usage): 精简管理员 Usage 列表的 request_metadata 下发
- 从 request_metadata JSON 中直接查询 model_version - 管理员 Usage 列表不再返回完整 request_metadata - 前端表格、类型和 mock 改为使用顶层 model_version - 新增轻量响应回归测试
This commit is contained in:
@@ -809,7 +809,7 @@ function getApiFormatTooltip(record: UsageRecord): string {
|
||||
return record.api_format
|
||||
}
|
||||
|
||||
// 获取实际使用的模型(优先 target_model,其次 model_version)
|
||||
// 获取实际使用的模型(优先 target_model,其次列表接口下发的 model_version)
|
||||
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
||||
function getActualModel(record: UsageRecord): string | null {
|
||||
// 优先显示模型映射
|
||||
@@ -817,8 +817,8 @@ function getActualModel(record: UsageRecord): string | null {
|
||||
return record.target_model
|
||||
}
|
||||
// 其次显示 Provider 返回的实际版本(如 Gemini 的 modelVersion)
|
||||
if (record.request_metadata?.model_version && record.request_metadata.model_version !== record.model) {
|
||||
return record.request_metadata.model_version
|
||||
if (record.model_version && record.model_version !== record.model) {
|
||||
return record.model_version
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface UsageRecord {
|
||||
rate_multiplier?: number
|
||||
model: string
|
||||
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
||||
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
|
||||
api_format?: string
|
||||
endpoint_api_format?: string // 端点原生格式
|
||||
has_format_conversion?: boolean // 是否发生了格式转换
|
||||
@@ -90,10 +91,6 @@ export interface UsageRecord {
|
||||
created_at: string
|
||||
has_fallback?: boolean
|
||||
has_retry?: boolean
|
||||
request_metadata?: {
|
||||
model_version?: string // Provider 返回的实际模型版本(如 Gemini 的 modelVersion)
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
// 日期范围参数
|
||||
|
||||
@@ -392,7 +392,7 @@ function generateMockUsageRecords(count: number = 100) {
|
||||
status,
|
||||
created_at: createdAt.toISOString(),
|
||||
has_fallback: Math.random() > 0.9,
|
||||
request_metadata: model.provider === 'google' ? { model_version: 'gemini-3-pro-preview-2025-01' } : undefined
|
||||
model_version: model.provider === 'google' ? 'gemini-3-pro-preview-2025-01' : undefined
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -224,10 +224,11 @@ async def get_usage_records(
|
||||
|
||||
**返回字段**:
|
||||
- `records`: 使用记录列表,包含 id, user_id, user_email, username, api_key, provider, model, target_model,
|
||||
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, total_tokens,
|
||||
cost, actual_cost, rate_multiplier, response_time_ms, first_byte_time_ms, created_at, is_stream,
|
||||
input_price_per_1m, output_price_per_1m, cache_creation_price_per_1m, cache_read_price_per_1m,
|
||||
status_code, error_message, status, has_fallback, has_retry, has_rectified, api_format, api_key_name, request_metadata
|
||||
model_version, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
||||
total_tokens, cost, actual_cost, rate_multiplier, response_time_ms, first_byte_time_ms, created_at,
|
||||
is_stream, input_price_per_1m, output_price_per_1m, cache_creation_price_per_1m,
|
||||
cache_read_price_per_1m, status_code, error_message, status, has_fallback, has_retry,
|
||||
has_rectified, api_format, api_key_name
|
||||
- `total`: 符合条件的总记录数
|
||||
- `limit`: 当前分页限制
|
||||
- `offset`: 当前分页偏移量
|
||||
@@ -885,8 +886,12 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
count_query = count_query.outerjoin(ApiKey, Usage.api_key_id == ApiKey.id)
|
||||
|
||||
# -- 构建数据查询(完整 JOIN) --
|
||||
usage_model_version = Usage.request_metadata["model_version"].as_string().label(
|
||||
"model_version"
|
||||
)
|
||||
|
||||
query = (
|
||||
db.query(Usage, User, ProviderEndpoint, ProviderAPIKey, ApiKey)
|
||||
db.query(Usage, User, ProviderEndpoint, ProviderAPIKey, ApiKey, usage_model_version)
|
||||
.outerjoin(User, Usage.user_id == User.id)
|
||||
.outerjoin(ProviderEndpoint, Usage.provider_endpoint_id == ProviderEndpoint.id)
|
||||
.outerjoin(ProviderAPIKey, Usage.provider_api_key_id == ProviderAPIKey.id)
|
||||
@@ -1001,7 +1006,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
# Perf: count query uses fewer JOINs than the data query
|
||||
total = int(count_query.scalar() or 0)
|
||||
|
||||
# Perf: do not load large request/response columns for list view
|
||||
# Perf: do not load large request/response columns or full request_metadata for list view
|
||||
query = query.options(
|
||||
load_only(
|
||||
Usage.id,
|
||||
@@ -1032,7 +1037,6 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
Usage.api_format,
|
||||
Usage.endpoint_api_format,
|
||||
Usage.has_format_conversion,
|
||||
Usage.request_metadata,
|
||||
Usage.input_price_per_1m,
|
||||
Usage.output_price_per_1m,
|
||||
Usage.cache_creation_price_per_1m,
|
||||
@@ -1047,7 +1051,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
query.order_by(Usage.created_at.desc()).offset(self.offset).limit(self.limit).all()
|
||||
)
|
||||
|
||||
request_ids = [usage.request_id for usage, _, _, _, _ in records if usage.request_id]
|
||||
request_ids = [usage.request_id for usage, _, _, _, _, _ in records if usage.request_id]
|
||||
fallback_map = {}
|
||||
retry_map = {}
|
||||
rectified_map = {}
|
||||
@@ -1110,7 +1114,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
|
||||
# 构建 provider_id -> Provider 名称的映射,避免 N+1 查询
|
||||
provider_ids = list(
|
||||
{usage.provider_id for usage, _, _, _, _ in records if usage.provider_id}
|
||||
{usage.provider_id for usage, _, _, _, _, _ in records if usage.provider_id}
|
||||
)
|
||||
provider_map = {}
|
||||
if provider_ids:
|
||||
@@ -1121,7 +1125,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
|
||||
data = []
|
||||
api_key_display_cache: dict[str, str] = {}
|
||||
for usage, user, endpoint, provider_api_key, user_api_key in records:
|
||||
for usage, user, endpoint, provider_api_key, user_api_key, model_version in records:
|
||||
actual_cost = (
|
||||
float(usage.actual_total_cost_usd)
|
||||
if usage.actual_total_cost_usd is not None
|
||||
@@ -1198,7 +1202,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
||||
"endpoint_api_format": endpoint_api_format,
|
||||
"has_format_conversion": bool(has_format_conversion),
|
||||
"api_key_name": provider_api_key.name if provider_api_key else None,
|
||||
"request_metadata": usage.request_metadata, # Provider 响应元数据
|
||||
"model_version": model_version, # Provider 返回的实际模型版本(轻量字段)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
143
tests/api/test_admin_usage_routes.py
Normal file
143
tests/api/test_admin_usage_routes.py
Normal file
@@ -0,0 +1,143 @@
|
||||
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 AdminUsageRecordsAdapter
|
||||
|
||||
|
||||
class _FakeQuery:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scalar_result: int | None = None,
|
||||
all_result: list[Any] | None = None,
|
||||
) -> None:
|
||||
self.scalar_result = scalar_result
|
||||
self.all_result = all_result or []
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user