mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
- 新增 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>
193 lines
6.3 KiB
Python
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,
|
|
}
|
|
]
|