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:
fawney19
2026-03-12 17:11:14 +08:00
parent 8d69f72e2a
commit e9678ea899
11 changed files with 553 additions and 145 deletions

View File

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