mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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-***abc),OAuth 类型不返回
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user