Files
Aether/tests/api/test_user_me_usage_routes.py
fawney19 bdfe4adc98 feat(usage): 修复缓存命中率计算并新增用户端 API 格式统计
- 新增 input_context_expr() 按 api_format 区分 input_tokens 语义
  (OpenAI/Gemini input_tokens 已含 cache_read,Claude 需额外加上)
- 缓存命中率统一改为基于归一化后的 total_input_context 计算
- 用户 /me/usage 接口新增 summary_by_api_format 后端聚合字段
- 前端 API 格式统计改用后端聚合数据,移除前端逐条记录手动统计
- 提取 formatHitRate 到 utils/format.ts 消除三处重复定义
- 移除 PoolManager 中未使用的 select_key 方法

Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
2026-03-14 00:33:19 +08:00

193 lines
6.3 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from src.api.user_me.routes import GetUsageAdapter
from src.core.enums import UserRole
@pytest.mark.asyncio
async def test_get_usage_adapter_uses_coarse_summary_grouping(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
query = MagicMock()
count_query = MagicMock()
count_query.scalar.return_value = 0
query.outerjoin.return_value = query
query.filter.return_value = query
query.group_by.return_value = query
query.with_entities.return_value = count_query
query.options.return_value = query
query.order_by.return_value = query
query.offset.return_value = query
query.limit.return_value = query
query.all.return_value = []
db.query.return_value = query
summary_getter = MagicMock(
return_value=[
{
"provider": "provider-a",
"model": "gpt-4o",
"requests": 2,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"total_input_context": 15,
"total_cost_usd": 1.5,
"actual_total_cost_usd": 1.2,
"success_count": 2,
"success_response_time_sum_ms": 1000.0,
"success_response_time_count": 2,
},
{
"provider": "pending",
"model": "gpt-4o",
"requests": 99,
"input_tokens": 999,
"output_tokens": 999,
"total_tokens": 1998,
"cache_read_tokens": 50,
"total_input_context": 1049,
"total_cost_usd": 9.9,
"actual_total_cost_usd": 9.9,
"success_count": 0,
"success_response_time_sum_ms": 0.0,
"success_response_time_count": 0,
},
]
)
monkeypatch.setattr("src.api.user_me.routes.UsageService.get_usage_summary", summary_getter)
monkeypatch.setattr("src.api.user_me.routes.WalletService.get_wallet", lambda *_a, **_k: None)
monkeypatch.setattr(
"src.api.user_me.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"limit_mode": "finite"},
)
adapter = GetUsageAdapter(time_range=None, limit=20, offset=0)
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="user-1", role=UserRole.USER),
request=SimpleNamespace(state=SimpleNamespace()),
)
result = await adapter.handle(context)
assert result["total_requests"] == 2
assert result["total_tokens"] == 15
assert result["summary_by_model"] == [
{
"model": "gpt-4o",
"requests": 2,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"cache_hit_rate": 33.33,
"total_cost_usd": 1.5,
}
]
assert result["summary_by_api_format"] == []
assert "total_actual_cost" not in result
assert result["summary_by_provider"] == [
{
"provider": "provider-a",
"requests": 2,
"total_tokens": 15,
"cache_read_tokens": 5,
"cache_hit_rate": 33.33,
"total_cost_usd": 1.5,
"success_rate": 100.0,
"avg_response_time_ms": 500.0,
}
]
assert summary_getter.call_args.kwargs["group_by"] is None
@pytest.mark.asyncio
async def test_get_usage_adapter_provider_success_rate_uses_success_count(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
query = MagicMock()
count_query = MagicMock()
count_query.scalar.return_value = 0
query.outerjoin.return_value = query
query.filter.return_value = query
query.group_by.return_value = query
query.with_entities.return_value = count_query
query.options.return_value = query
query.order_by.return_value = query
query.offset.return_value = query
query.limit.return_value = query
query.all.return_value = []
db.query.return_value = query
summary_getter = MagicMock(
return_value=[
{
"provider": "provider-a",
"model": "gpt-4o",
"requests": 3,
"input_tokens": 30,
"output_tokens": 15,
"total_tokens": 45,
"cache_read_tokens": 15,
"total_input_context": 45,
"total_cost_usd": 4.5,
"actual_total_cost_usd": 4.5,
"success_count": 2,
"success_response_time_sum_ms": 600.0,
"success_response_time_count": 2,
},
{
"provider": "provider-a",
"model": "gpt-4.1",
"requests": 1,
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": 5,
"total_input_context": 15,
"total_cost_usd": 1.5,
"actual_total_cost_usd": 1.5,
"success_count": 0,
"success_response_time_sum_ms": 0.0,
"success_response_time_count": 0,
},
]
)
monkeypatch.setattr("src.api.user_me.routes.UsageService.get_usage_summary", summary_getter)
monkeypatch.setattr("src.api.user_me.routes.WalletService.get_wallet", lambda *_a, **_k: None)
monkeypatch.setattr(
"src.api.user_me.routes.WalletService.serialize_wallet_summary",
lambda _wallet: {"limit_mode": "finite"},
)
adapter = GetUsageAdapter(time_range=None, limit=20, offset=0)
context = SimpleNamespace(
db=db,
user=SimpleNamespace(id="user-1", role=UserRole.USER),
request=SimpleNamespace(state=SimpleNamespace()),
)
result = await adapter.handle(context)
assert result["summary_by_provider"] == [
{
"provider": "provider-a",
"requests": 4,
"total_tokens": 60,
"cache_read_tokens": 20,
"cache_hit_rate": 33.33,
"total_cost_usd": 6.0,
"success_rate": 50.0,
"avg_response_time_ms": 300.0,
}
]