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
|
return record.api_format
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实际使用的模型(优先 target_model,其次 model_version)
|
// 获取实际使用的模型(优先 target_model,其次列表接口下发的 model_version)
|
||||||
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
|
||||||
function getActualModel(record: UsageRecord): string | null {
|
function getActualModel(record: UsageRecord): string | null {
|
||||||
// 优先显示模型映射
|
// 优先显示模型映射
|
||||||
@@ -817,8 +817,8 @@ function getActualModel(record: UsageRecord): string | null {
|
|||||||
return record.target_model
|
return record.target_model
|
||||||
}
|
}
|
||||||
// 其次显示 Provider 返回的实际版本(如 Gemini 的 modelVersion)
|
// 其次显示 Provider 返回的实际版本(如 Gemini 的 modelVersion)
|
||||||
if (record.request_metadata?.model_version && record.request_metadata.model_version !== record.model) {
|
if (record.model_version && record.model_version !== record.model) {
|
||||||
return record.request_metadata.model_version
|
return record.model_version
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface UsageRecord {
|
|||||||
rate_multiplier?: number
|
rate_multiplier?: number
|
||||||
model: string
|
model: string
|
||||||
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
|
||||||
|
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
|
||||||
api_format?: string
|
api_format?: string
|
||||||
endpoint_api_format?: string // 端点原生格式
|
endpoint_api_format?: string // 端点原生格式
|
||||||
has_format_conversion?: boolean // 是否发生了格式转换
|
has_format_conversion?: boolean // 是否发生了格式转换
|
||||||
@@ -90,10 +91,6 @@ export interface UsageRecord {
|
|||||||
created_at: string
|
created_at: string
|
||||||
has_fallback?: boolean
|
has_fallback?: boolean
|
||||||
has_retry?: 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,
|
status,
|
||||||
created_at: createdAt.toISOString(),
|
created_at: createdAt.toISOString(),
|
||||||
has_fallback: Math.random() > 0.9,
|
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,
|
- `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,
|
model_version, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens,
|
||||||
cost, actual_cost, rate_multiplier, response_time_ms, first_byte_time_ms, created_at, is_stream,
|
total_tokens, cost, actual_cost, rate_multiplier, response_time_ms, first_byte_time_ms, created_at,
|
||||||
input_price_per_1m, output_price_per_1m, cache_creation_price_per_1m, cache_read_price_per_1m,
|
is_stream, input_price_per_1m, output_price_per_1m, cache_creation_price_per_1m,
|
||||||
status_code, error_message, status, has_fallback, has_retry, has_rectified, api_format, api_key_name, request_metadata
|
cache_read_price_per_1m, status_code, error_message, status, has_fallback, has_retry,
|
||||||
|
has_rectified, api_format, api_key_name
|
||||||
- `total`: 符合条件的总记录数
|
- `total`: 符合条件的总记录数
|
||||||
- `limit`: 当前分页限制
|
- `limit`: 当前分页限制
|
||||||
- `offset`: 当前分页偏移量
|
- `offset`: 当前分页偏移量
|
||||||
@@ -885,8 +886,12 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
count_query = count_query.outerjoin(ApiKey, Usage.api_key_id == ApiKey.id)
|
count_query = count_query.outerjoin(ApiKey, Usage.api_key_id == ApiKey.id)
|
||||||
|
|
||||||
# -- 构建数据查询(完整 JOIN) --
|
# -- 构建数据查询(完整 JOIN) --
|
||||||
|
usage_model_version = Usage.request_metadata["model_version"].as_string().label(
|
||||||
|
"model_version"
|
||||||
|
)
|
||||||
|
|
||||||
query = (
|
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(User, Usage.user_id == User.id)
|
||||||
.outerjoin(ProviderEndpoint, Usage.provider_endpoint_id == ProviderEndpoint.id)
|
.outerjoin(ProviderEndpoint, Usage.provider_endpoint_id == ProviderEndpoint.id)
|
||||||
.outerjoin(ProviderAPIKey, Usage.provider_api_key_id == ProviderAPIKey.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
|
# Perf: count query uses fewer JOINs than the data query
|
||||||
total = int(count_query.scalar() or 0)
|
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(
|
query = query.options(
|
||||||
load_only(
|
load_only(
|
||||||
Usage.id,
|
Usage.id,
|
||||||
@@ -1032,7 +1037,6 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
Usage.api_format,
|
Usage.api_format,
|
||||||
Usage.endpoint_api_format,
|
Usage.endpoint_api_format,
|
||||||
Usage.has_format_conversion,
|
Usage.has_format_conversion,
|
||||||
Usage.request_metadata,
|
|
||||||
Usage.input_price_per_1m,
|
Usage.input_price_per_1m,
|
||||||
Usage.output_price_per_1m,
|
Usage.output_price_per_1m,
|
||||||
Usage.cache_creation_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()
|
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 = {}
|
fallback_map = {}
|
||||||
retry_map = {}
|
retry_map = {}
|
||||||
rectified_map = {}
|
rectified_map = {}
|
||||||
@@ -1110,7 +1114,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# 构建 provider_id -> Provider 名称的映射,避免 N+1 查询
|
# 构建 provider_id -> Provider 名称的映射,避免 N+1 查询
|
||||||
provider_ids = list(
|
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 = {}
|
provider_map = {}
|
||||||
if provider_ids:
|
if provider_ids:
|
||||||
@@ -1121,7 +1125,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
api_key_display_cache: dict[str, str] = {}
|
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 = (
|
actual_cost = (
|
||||||
float(usage.actual_total_cost_usd)
|
float(usage.actual_total_cost_usd)
|
||||||
if usage.actual_total_cost_usd is not None
|
if usage.actual_total_cost_usd is not None
|
||||||
@@ -1198,7 +1202,7 @@ class AdminUsageRecordsAdapter(AdminApiAdapter):
|
|||||||
"endpoint_api_format": endpoint_api_format,
|
"endpoint_api_format": endpoint_api_format,
|
||||||
"has_format_conversion": bool(has_format_conversion),
|
"has_format_conversion": bool(has_format_conversion),
|
||||||
"api_key_name": provider_api_key.name if provider_api_key else None,
|
"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