refactor: 统一 API 格式显示函数,补全 Usage/Trace 的 provider 链路信息

- 提取 formatApiFormat 为共享工具函数,替换各组件中分散的 API_FORMAT_LABELS 调用
- Trace API 返回密钥认证类型(key_auth_type)和 OAuth 套餐信息(key_oauth_plan_type)
- 请求时间线展示密钥认证方式标签(OAuth/Vertex AI/Kiro 等)
- Usage 记录补充 provider_id/endpoint_id/key_id,避免 curl 复现时缺失 provider 信息
- curl 复现兜底从 RequestCandidate 表查找 provider 信息
- Headers Diff 面板左右独立滚动并同步垂直滚动位置
This commit is contained in:
fawney19
2026-02-19 14:51:08 +08:00
parent 0d2cafaec3
commit 7a81e56553
23 changed files with 355 additions and 206 deletions

View File

@@ -4,6 +4,7 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import datetime
from typing import Any
@@ -38,7 +39,9 @@ class CandidateResponse(BaseModel):
endpoint_name: str | None = None # 端点显示名称api_format
key_id: str | None = None
key_name: str | None = None # 密钥名称
key_preview: str | None = None # 密钥脱敏预览(如 sk-***abc
key_preview: str | None = None # 密钥脱敏预览(如 sk-***abcOAuth 类型不返回
key_auth_type: str | None = None # 密钥认证类型api_key, oauth, vertex_ai 等)
key_oauth_plan_type: str | None = None # OAuth 账号套餐类型free/plus/team/enterprise
key_capabilities: dict | None = None # Key 支持的能力
required_capabilities: dict | None = None # 请求实际需要的能力标签
status: str # 'pending', 'success', 'failed', 'skipped'
@@ -208,13 +211,15 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
# 批量加载 provider 信息,避免 N+1 查询
provider_ids = {c.provider_id for c in candidates if c.provider_id}
provider_map = {}
provider_website_map = {}
provider_map: dict[str, str] = {}
provider_website_map: dict[str, str | None] = {}
provider_type_map: dict[str, str] = {}
if provider_ids:
providers = db.query(Provider).filter(Provider.id.in_(provider_ids)).all()
for p in providers:
provider_map[p.id] = p.name
provider_website_map[p.id] = p.website
provider_type_map[p.id] = getattr(p, "provider_type", "custom") or "custom"
# 批量加载 endpoint 信息
endpoint_ids = {c.endpoint_id for c in candidates if c.endpoint_id}
@@ -227,15 +232,64 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
# 批量加载 key 信息
key_ids = {c.key_id for c in candidates if c.key_id}
key_map = {}
key_preview_map = {}
key_capabilities_map = {}
key_map: dict[str, str] = {}
key_preview_map: dict[str, str] = {}
key_capabilities_map: dict[str, dict | None] = {}
key_auth_type_map: dict[str, str] = {}
key_oauth_plan_map: dict[str, str | None] = {}
# 建立 key_id -> provider_id 的映射(用于获取 provider_type
key_provider_map: dict[str, str | None] = {
c.key_id: c.provider_id for c in candidates if c.key_id
}
if key_ids:
keys = db.query(ProviderAPIKey).filter(ProviderAPIKey.id.in_(key_ids)).all()
for k in keys:
key_map[k.id] = k.name
key_capabilities_map[k.id] = k.capabilities
# 生成脱敏预览:先解密再脱敏
is_oauth = k.auth_type == "oauth"
if is_oauth:
# OAuth: auth_type 使用具体的 provider_type如 kiro/codex/antigravity
pid = key_provider_map.get(k.id)
key_auth_type_map[k.id] = (
provider_type_map.get(pid, "oauth") if pid else "oauth"
)
# 提取 plan_type不同 provider 存储位置不同)
oauth_plan_type = None
# 1. Codex: auth_config.plan_type
# 2. Antigravity: auth_config.tier
if k.auth_config:
try:
decrypted_config = crypto_service.decrypt(k.auth_config)
auth_config = json.loads(decrypted_config)
oauth_plan_type = auth_config.get("plan_type")
if not oauth_plan_type:
ag_tier = auth_config.get("tier")
if ag_tier and isinstance(ag_tier, str):
oauth_plan_type = ag_tier.lower()
except Exception:
pass
# 3. Kiro: upstream_metadata.kiro.subscription_title
# subscription_title 通常为 "KIRO FREE" / "KIRO PRO+" 等,
# 去掉 provider 名称前缀,只保留等级部分
if not oauth_plan_type:
um = getattr(k, "upstream_metadata", None) or {}
kiro_meta = um.get("kiro") if isinstance(um, dict) else None
if isinstance(kiro_meta, dict):
sub_title = kiro_meta.get("subscription_title")
if sub_title and isinstance(sub_title, str):
# "KIRO FREE" -> "Free", "KIRO PRO+" -> "Pro+"
ptype = provider_type_map.get(pid, "") if pid else ""
if ptype and sub_title.upper().startswith(ptype.upper()):
sub_title = sub_title[len(ptype) :].strip()
oauth_plan_type = sub_title
key_oauth_plan_map[k.id] = oauth_plan_type
continue
else:
key_auth_type_map[k.id] = k.auth_type or "api_key"
# 非 OAuth生成脱敏预览
try:
decrypted_key = crypto_service.decrypt(k.api_key)
if len(decrypted_key) > 8:
@@ -272,6 +326,10 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
)
key_name = key_map.get(candidate.key_id) if candidate.key_id else None
key_preview = key_preview_map.get(candidate.key_id) if candidate.key_id else None
key_auth_type = key_auth_type_map.get(candidate.key_id) if candidate.key_id else None
key_oauth_plan_type = (
key_oauth_plan_map.get(candidate.key_id) if candidate.key_id else None
)
key_capabilities = (
key_capabilities_map.get(candidate.key_id) if candidate.key_id else None
)
@@ -290,6 +348,8 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
key_id=candidate.key_id,
key_name=key_name,
key_preview=key_preview,
key_auth_type=key_auth_type,
key_oauth_plan_type=key_oauth_plan_type,
key_capabilities=key_capabilities,
required_capabilities=candidate.required_capabilities,
status=candidate.status,

View File

@@ -1559,20 +1559,32 @@ class AdminUsageCurlAdapter(AdminApiAdapter):
usage_record = _find_usage_record(db, self.usage_id)
# 获取端点和密钥
endpoint_id = usage_record.provider_endpoint_id
key_id = usage_record.provider_api_key_id
# 兜底Usage 记录缺少 provider 信息时,从 RequestCandidate 表查找
if not endpoint_id or not key_id:
from src.models.database import RequestCandidate as RC
candidate = (
db.query(RC)
.filter(
RC.request_id == usage_record.request_id,
RC.status.in_(["success", "failed", "streaming"]),
)
.order_by(RC.candidate_index.desc(), RC.retry_index.desc())
.first()
)
if candidate:
endpoint_id = endpoint_id or candidate.endpoint_id
key_id = key_id or candidate.key_id
endpoint = None
if usage_record.provider_endpoint_id:
endpoint = (
db.query(ProviderEndpoint)
.filter(ProviderEndpoint.id == usage_record.provider_endpoint_id)
.first()
)
if endpoint_id:
endpoint = db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
provider_key = None
if usage_record.provider_api_key_id:
provider_key = (
db.query(ProviderAPIKey)
.filter(ProviderAPIKey.id == usage_record.provider_api_key_id)
.first()
)
if key_id:
provider_key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
# 重建请求 URL
url: str | None = None

View File

@@ -255,6 +255,9 @@ class ChatSyncExecutor:
request_body=actual_request_body,
error_message=str(e),
is_stream=False,
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
request_metadata=request_metadata or None,
)
client_format = (ctx.client_api_format_for_error or "").upper()
@@ -289,6 +292,9 @@ class ChatSyncExecutor:
provider_request_headers=ctx.provider_request_headers,
response_headers=ctx.response_headers,
client_response_headers={"content-type": "application/json"},
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
# 格式转换追踪
endpoint_api_format=ctx.provider_api_format_for_error or None,
has_format_conversion=is_format_converted(
@@ -347,6 +353,9 @@ class ChatSyncExecutor:
response_headers=error_response_headers,
# 非流式失败返回给客户端的是 JSON 错误响应
client_response_headers={"content-type": "application/json"},
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
# 格式转换追踪
endpoint_api_format=ctx.provider_api_format_for_error or None,
has_format_conversion=is_format_converted(
@@ -377,6 +386,9 @@ class ChatSyncExecutor:
ctx = self._ctx
ctx.provider_name = str(provider.name)
ctx.provider_id = str(provider.id)
ctx.endpoint_id = str(endpoint.id)
ctx.key_id = str(key.id)
provider_api_format = str(endpoint.api_format or api_format)
client_api_format = api_format.value if hasattr(api_format, "value") else str(api_format)
@@ -710,6 +722,9 @@ class ChatSyncExecutor:
provider_request_headers=ctx.provider_request_headers,
response_headers=ctx.response_headers,
client_response_headers=client_response_headers,
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
# 格式转换追踪
endpoint_api_format=ctx.provider_api_format or None,
has_format_conversion=ctx.has_format_conversion,

View File

@@ -269,6 +269,9 @@ class StreamTelemetryRecorder:
response_body=response_body,
response_headers=ctx.response_headers,
client_response_headers=client_response_headers,
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
target_model=ctx.mapped_model,
request_type="chat",
metadata=metadata,
@@ -318,6 +321,9 @@ class StreamTelemetryRecorder:
response_body=response_body,
response_headers=ctx.response_headers,
client_response_headers=client_response_headers,
provider_id=ctx.provider_id,
provider_endpoint_id=ctx.endpoint_id,
provider_api_key_id=ctx.key_id,
target_model=ctx.mapped_model,
request_type="chat",
metadata=metadata,

View File

@@ -256,10 +256,13 @@ def update_existing_usage(
existing_usage.actual_total_cost_usd = usage_params["actual_total_cost_usd"]
existing_usage.rate_multiplier = usage_params["rate_multiplier"]
# 更新 Provider 侧追踪信息
existing_usage.provider_id = usage_params["provider_id"]
existing_usage.provider_endpoint_id = usage_params["provider_endpoint_id"]
existing_usage.provider_api_key_id = usage_params["provider_api_key_id"]
# 更新 Provider 侧追踪信息(仅在有新值时更新,避免覆盖已有数据)
if usage_params.get("provider_id"):
existing_usage.provider_id = usage_params["provider_id"]
if usage_params.get("provider_endpoint_id"):
existing_usage.provider_endpoint_id = usage_params["provider_endpoint_id"]
if usage_params.get("provider_api_key_id"):
existing_usage.provider_api_key_id = usage_params["provider_api_key_id"]
# 更新元数据(如 billing_snapshot/dimensions 等)
if usage_params.get("request_metadata") is not None:

View File

@@ -172,6 +172,10 @@ class MessageTelemetry:
response_body: dict[str, Any] | None = None,
response_headers: dict[str, Any] | None = None,
client_response_headers: dict[str, Any] | None = None,
# Provider 侧追踪信息(用于 curl 复现等场景)
provider_id: str | None = None,
provider_endpoint_id: str | None = None,
provider_api_key_id: str | None = None,
# 格式转换追踪
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
@@ -183,9 +187,6 @@ class MessageTelemetry:
"""
记录失败请求
注意Provider 链路信息provider_id, endpoint_id, key_id不在此处记录
因为 RequestCandidate 表已经记录了完整的请求链路追踪信息。
Args:
input_tokens: 预估输入 tokens来自 message_start用于中断请求的成本估算
output_tokens: 预估输出 tokens来自已收到的内容
@@ -228,6 +229,10 @@ class MessageTelemetry:
client_response_headers=client_response_headers,
response_body=response_body or {"error": error_message},
request_id=self.request_id,
# Provider 侧追踪信息
provider_id=provider_id,
provider_endpoint_id=provider_endpoint_id,
provider_api_key_id=provider_api_key_id,
# 模型映射信息
target_model=target_model,
# 请求元数据
@@ -254,6 +259,10 @@ class MessageTelemetry:
response_body: dict[str, Any] | None = None,
response_headers: dict[str, Any] | None = None,
client_response_headers: dict[str, Any] | None = None,
# Provider 侧追踪信息
provider_id: str | None = None,
provider_endpoint_id: str | None = None,
provider_api_key_id: str | None = None,
# 格式转换追踪
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
@@ -294,6 +303,10 @@ class MessageTelemetry:
client_response_headers=client_response_headers,
response_body=response_body or {},
request_id=self.request_id,
# Provider 侧追踪信息
provider_id=provider_id,
provider_endpoint_id=provider_endpoint_id,
provider_api_key_id=provider_api_key_id,
target_model=target_model,
metadata=request_metadata,
)