perf(admin-usage): 精简管理员 Usage 列表的 request_metadata 下发

- 从 request_metadata JSON 中直接查询 model_version
- 管理员 Usage 列表不再返回完整 request_metadata
- 前端表格、类型和 mock 改为使用顶层 model_version
- 新增轻量响应回归测试
This commit is contained in:
AAEE86
2026-03-10 00:02:49 +08:00
parent 57c7cca556
commit 596227659a
5 changed files with 163 additions and 19 deletions

View File

@@ -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
} }

View File

@@ -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
}
} }
// 日期范围参数 // 日期范围参数

View File

@@ -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
}) })
} }

View File

@@ -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 返回的实际模型版本(轻量字段)
} }
) )

View 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