fix: 用量记录保留空 headers/body,格式转换支持同 normalizer 透传

- 用量记录和遥测写入中将 truthiness 检查改为 is not None,
  避免空 dict/list 被误丢弃
- FormatConversionRegistry 新增 _same_normalizer() 方法,
  共享 normalizer 的格式(如 claude:chat/claude:cli)视为同格式透传
- 前端请求详情抽屉按数据可用性智能选择默认数据源
- 视频任务 finalize 支持 headers/original_headers 等别名字段
This commit is contained in:
fawney19
2026-02-21 21:16:35 +08:00
parent b649a69a5e
commit 1e4d0006b9
8 changed files with 179 additions and 61 deletions

View File

@@ -338,6 +338,37 @@ async def test_queue_writer_include_headers_bodies(monkeypatch: Any) -> None:
assert "response_body" in event.data
@pytest.mark.asyncio
async def test_queue_writer_failure_preserves_empty_request_headers(monkeypatch: Any) -> None:
"""失败事件在传入空请求头时也应保留 request_headers 字段。"""
dummy = DummyRedis()
async def _get_redis_client(require_redis: bool = False) -> Any:
return dummy
monkeypatch.setattr("src.services.usage.telemetry_writer.get_redis_client", _get_redis_client)
writer = QueueTelemetryWriter(
request_id="req-empty-hdr",
user_id="user-1",
api_key_id="key-1",
log_level="full",
)
await writer.record_failure(
provider="test",
model="model",
status_code=500,
error_message="boom",
request_headers={},
request_body={"message": "x"},
)
event = UsageEvent.from_stream_fields(dummy.calls[0][1])
assert "request_headers" in event.data
assert event.data["request_headers"] == {}
assert event.data["request_body"] == {"message": "x"}
@pytest.mark.asyncio
async def test_event_to_record_body_deserialization(monkeypatch: Any) -> None:
"""测试 _event_to_record 正确反序列化 body 字符串为 dict"""

View File

@@ -189,3 +189,40 @@ async def test_video_finalize_strict_mode_missing_required_marks_failed(
kwargs = update_settled.call_args.kwargs
assert kwargs["total_cost_usd"] == 0.0
assert kwargs["status"] == "failed"
@pytest.mark.asyncio
async def test_video_finalize_fallback_reads_headers_alias(monkeypatch: pytest.MonkeyPatch) -> None:
usage_query = MagicMock()
usage_query.filter.return_value.first.return_value = None
user_query = MagicMock()
user_query.filter.return_value.first.return_value = SimpleNamespace(id="u1")
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = SimpleNamespace(id="ak1")
provider_query = MagicMock()
provider_query.filter.return_value.first.return_value = SimpleNamespace(
id="p1",
name="prov1",
)
db = MagicMock()
db.query.side_effect = [usage_query, user_query, api_key_query, provider_query]
task = _make_task(
status=VideoStatus.FAILED.value,
error_message="boom",
request_metadata={"headers": {"x-test-header": "1"}},
)
record_usage = AsyncMock(return_value=SimpleNamespace(id="usage-1"))
monkeypatch.setattr(
"src.services.usage.service.UsageService.record_usage_with_custom_cost",
record_usage,
)
svc = TaskService(db)
ok = await svc.finalize_video_task(task)
assert ok is True
kwargs = record_usage.call_args.kwargs
assert kwargs["request_headers"] == {"x-test-header": "1"}