mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
fix(admin): 优先级脏检查、base_url 校验、usage detail 延迟加载及导入数据验证
- 前端优先级管理: 保存时对比原始快照,仅提交实际变更的 provider/key 优先级, 并限制并发请求数(SAVE_CONCURRENCY=6),避免无效 API 调用 - handler_adapter_base: _normalize_test_base_url 改为 _validate_test_base_url, 移除对 dict 类型 base_url 的兼容,严格要求字符串输入 - provider_query: 新增 _require_test_endpoint_base_url,在测试链路提前校验 endpoint.base_url 类型和非空 - system.py: 导入 endpoint 时通过 ProviderEndpointCreate 模型校验数据, 拒绝非法 base_url 类型(如 dict) - usage detail: 使用 defer() 延迟加载 body 列,通过 SQL CASE 表达式在 数据库端计算 has_*_body 标记,减少不必要的大字段传输 - provider routes: 新建 provider 时 priority=0 边界处理,clamp 并 shift
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api.admin.usage.routes import AdminUsageRecordsAdapter
|
||||
from src.api.admin.usage.routes import AdminUsageDetailAdapter, AdminUsageRecordsAdapter
|
||||
|
||||
|
||||
class _FakeQuery:
|
||||
@@ -16,9 +16,11 @@ class _FakeQuery:
|
||||
*,
|
||||
scalar_result: int | None = None,
|
||||
all_result: list[Any] | None = None,
|
||||
first_result: Any = None,
|
||||
) -> None:
|
||||
self.scalar_result = scalar_result
|
||||
self.all_result = all_result or []
|
||||
self.first_result = first_result
|
||||
self.options_args: tuple[Any, ...] = ()
|
||||
|
||||
def outerjoin(self, *args: Any, **kwargs: Any) -> _FakeQuery:
|
||||
@@ -49,6 +51,9 @@ class _FakeQuery:
|
||||
def all(self) -> list[Any]:
|
||||
return self.all_result
|
||||
|
||||
def first(self) -> Any:
|
||||
return self.first_result
|
||||
|
||||
|
||||
class _FakeDb:
|
||||
def __init__(self, queries: list[_FakeQuery]) -> None:
|
||||
@@ -141,3 +146,130 @@ async def test_admin_usage_records_returns_model_version_without_request_metadat
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_usage_detail_defers_large_body_columns_when_bodies_excluded(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def _fake_get_tiered_pricing_info(
|
||||
self: AdminUsageDetailAdapter,
|
||||
db: Any,
|
||||
usage_record: Any,
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
AdminUsageDetailAdapter,
|
||||
"_get_tiered_pricing_info",
|
||||
_fake_get_tiered_pricing_info,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
AdminUsageDetailAdapter,
|
||||
"_extract_video_billing_info",
|
||||
lambda self, usage_record: None,
|
||||
)
|
||||
|
||||
class _UsageRecord:
|
||||
id = "usage-1"
|
||||
request_id = "req-1"
|
||||
user_id = "user-1"
|
||||
api_key_id = "key-1"
|
||||
provider_name = "openai"
|
||||
api_format = "openai:cli"
|
||||
model = "gpt-5.4"
|
||||
target_model = None
|
||||
input_tokens = 10
|
||||
output_tokens = 20
|
||||
total_tokens = 30
|
||||
cache_creation_input_tokens = 0
|
||||
cache_read_input_tokens = 0
|
||||
cache_creation_input_tokens_5m = 0
|
||||
cache_creation_input_tokens_1h = 0
|
||||
input_cost_usd = Decimal("0.001")
|
||||
output_cost_usd = Decimal("0.002")
|
||||
total_cost_usd = Decimal("0.003")
|
||||
cache_creation_cost_usd = Decimal("0")
|
||||
cache_read_cost_usd = Decimal("0")
|
||||
request_cost_usd = Decimal("0")
|
||||
input_price_per_1m = Decimal("0.1")
|
||||
output_price_per_1m = Decimal("0.2")
|
||||
cache_creation_price_per_1m = None
|
||||
cache_read_price_per_1m = None
|
||||
price_per_request = None
|
||||
request_type = "chat"
|
||||
is_stream = True
|
||||
status_code = 200
|
||||
error_message = None
|
||||
status = "completed"
|
||||
response_time_ms = 1200
|
||||
first_byte_time_ms = 200
|
||||
created_at = datetime(2026, 3, 12, 7, 0, tzinfo=timezone.utc)
|
||||
request_headers = {"x-test": "1"}
|
||||
provider_request_headers = {"authorization": "***"}
|
||||
response_headers = {"content-type": "text/event-stream"}
|
||||
client_response_headers = {"content-type": "text/event-stream"}
|
||||
request_metadata = {"trace_id": "trace-1"}
|
||||
|
||||
def get_request_body(self) -> Any:
|
||||
raise AssertionError("request body should not be loaded")
|
||||
|
||||
def get_provider_request_body(self) -> Any:
|
||||
raise AssertionError("provider request body should not be loaded")
|
||||
|
||||
def get_response_body(self) -> Any:
|
||||
raise AssertionError("response body should not be loaded")
|
||||
|
||||
def get_client_response_body(self) -> Any:
|
||||
raise AssertionError("client response body should not be loaded")
|
||||
|
||||
class _ApiKeyRecord:
|
||||
id = "key-1"
|
||||
name = "Primary"
|
||||
|
||||
def get_display_key(self) -> str:
|
||||
return "sk-test"
|
||||
|
||||
usage_query = _FakeQuery(
|
||||
first_result=(_UsageRecord(), True, True, True, True),
|
||||
)
|
||||
user_query = _FakeQuery(
|
||||
first_result=SimpleNamespace(id="user-1", username="tester", email="u@example.com"),
|
||||
)
|
||||
api_key_query = _FakeQuery(first_result=_ApiKeyRecord())
|
||||
db = _FakeDb([usage_query, user_query, api_key_query])
|
||||
context = SimpleNamespace(
|
||||
db=db,
|
||||
user=SimpleNamespace(id="admin-1"),
|
||||
add_audit_metadata=lambda **_: None,
|
||||
)
|
||||
|
||||
adapter = AdminUsageDetailAdapter(usage_id="usage-1", include_bodies=False)
|
||||
result = await adapter.handle(context) # type: ignore[arg-type]
|
||||
|
||||
assert result["request_body"] is None
|
||||
assert result["provider_request_body"] is None
|
||||
assert result["response_body"] is None
|
||||
assert result["client_response_body"] is None
|
||||
assert result["has_request_body"] is True
|
||||
assert result["has_provider_request_body"] is True
|
||||
assert result["has_response_body"] is True
|
||||
assert result["has_client_response_body"] is True
|
||||
|
||||
deferred_paths = {
|
||||
str(context.path)
|
||||
for option in usage_query.options_args
|
||||
for context in getattr(option, "context", ())
|
||||
}
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body]" in deferred_paths
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body]" in deferred_paths
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body]" in deferred_paths
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body]" in deferred_paths
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.request_body_compressed]" in deferred_paths
|
||||
assert (
|
||||
"ORM Path[Mapper[Usage(usage)] -> Usage.provider_request_body_compressed]" in deferred_paths
|
||||
)
|
||||
assert "ORM Path[Mapper[Usage(usage)] -> Usage.response_body_compressed]" in deferred_paths
|
||||
assert (
|
||||
"ORM Path[Mapper[Usage(usage)] -> Usage.client_response_body_compressed]" in deferred_paths
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user