refactor: 提取 HandlerAdapterBase 基类,新增 api_family/endpoint_kind 结构化维度

- 从 ChatAdapterBase 和 CliAdapterBase 提取公共逻辑到 HandlerAdapterBase,
  消除头部处理、异常处理、计费策略等重复代码
- Usage 记录链路全程透传 api_family / endpoint_kind / provider_api_family /
  provider_endpoint_kind 四个结构化字段,替代从 api_format 字符串解析
- 删除冗余的 ClaudeCliNormalizer、GeminiCliNormalizer、ClaudeCliResponseParser、
  GeminiCliResponseParser,改用 data_format_id 回退机制自动复用 Chat 版本
- build_endpoint_url 签名统一扩展 request_data / model_name 参数
- 新增 Alembic 迁移,含历史数据回填
This commit is contained in:
fawney19
2026-02-21 13:17:11 +08:00
parent 54988916e3
commit 0fe5346f4d
36 changed files with 967 additions and 1310 deletions

View File

@@ -52,6 +52,10 @@ class RequestMetadata:
provider: str = "unknown"
model: str = "unknown"
# 结构化格式维度(从 Adapter 层透传,优先于从 api_format 字符串解析)
api_family: str | None = None # 协议族: claude, openai, gemini
endpoint_kind: str | None = None # 端点类型: chat, cli, video
# Provider 追踪信息
provider_id: str | None = None
provider_endpoint_id: str | None = None
@@ -80,6 +84,8 @@ class RequestMetadata:
api_format=self.api_format,
provider=provider,
model=self.model,
api_family=self.api_family,
endpoint_kind=self.endpoint_kind,
provider_id=provider_id,
provider_endpoint_id=provider_endpoint_id,
provider_api_key_id=provider_api_key_id,
@@ -96,6 +102,8 @@ class RequestMetadata:
api_format=self.api_format,
provider=self.provider,
model=self.model,
api_family=self.api_family,
endpoint_kind=self.endpoint_kind,
provider_id=self.provider_id,
provider_endpoint_id=self.provider_endpoint_id,
provider_api_key_id=self.provider_api_key_id,
@@ -274,6 +282,8 @@ class RequestResult:
api_format=get_meta_value(existing_metadata, "api_format") or api_format,
provider=get_meta_value(existing_metadata, "provider", "unknown") or "unknown",
model=get_meta_value(existing_metadata, "model", model) or model,
api_family=get_meta_value(existing_metadata, "api_family"),
endpoint_kind=get_meta_value(existing_metadata, "endpoint_kind"),
provider_id=get_meta_value(existing_metadata, "provider_id"),
provider_endpoint_id=get_meta_value(existing_metadata, "provider_endpoint_id"),
provider_api_key_id=get_meta_value(existing_metadata, "provider_api_key_id"),

View File

@@ -135,6 +135,8 @@ class UsageBillingIntegrationMixin:
cache_read_input_tokens=params.cache_read_input_tokens,
request_type=params.request_type,
api_format=params.api_format,
api_family=params.api_family,
endpoint_kind=params.endpoint_kind,
endpoint_api_format=params.endpoint_api_format,
has_format_conversion=params.has_format_conversion,
is_stream=params.is_stream,

View File

@@ -10,6 +10,17 @@ from src.services.system.config import SystemConfigService
from src.services.usage._types import UsageCostInfo
from src.services.usage.error_classifier import classify_error
def _parse_format_dimensions(api_format: str | None) -> tuple[str | None, str | None]:
"""从 api_format (如 'claude:chat') 解析出 (api_family, endpoint_kind)"""
if not api_format:
return None, None
parts = api_format.lower().split(":", 1)
if len(parts) == 2:
return parts[0], parts[1]
return parts[0], None
# Metadata pruning configuration (ordered by priority - drop first to last)
METADATA_PRUNE_KEYS: tuple[str, ...] = (
"raw_response_ref",
@@ -45,7 +56,11 @@ def build_usage_params(
cache_read_input_tokens: int,
request_type: str,
api_format: str | None,
api_family: str | None = None,
endpoint_kind: str | None = None,
endpoint_api_format: str | None,
provider_api_family: str | None = None,
provider_endpoint_kind: str | None = None,
has_format_conversion: bool,
is_stream: bool,
response_time_ms: int | None,
@@ -160,6 +175,16 @@ def build_usage_params(
if status_code >= 400 or error_message or status in {"failed", "cancelled"}:
error_category = classify_error(status_code, error_message, status).value
# 从 api_format / endpoint_api_format 解析 api_family + endpoint_kind
# 优先使用透传值fallback 到字符串解析
parsed_family, parsed_kind = _parse_format_dimensions(api_format)
client_family = api_family or parsed_family
client_kind = endpoint_kind or parsed_kind
parsed_ep_family, parsed_ep_kind = _parse_format_dimensions(endpoint_api_format)
ep_family = provider_api_family or parsed_ep_family
ep_kind = provider_endpoint_kind or parsed_ep_kind
return {
"user_id": user.id if user else None,
"api_key_id": api_key.id if api_key else None,
@@ -196,7 +221,11 @@ def build_usage_params(
"price_per_request": request_price,
"request_type": request_type,
"api_format": api_format,
"api_family": client_family,
"endpoint_kind": client_kind,
"endpoint_api_format": endpoint_api_format,
"provider_api_family": ep_family,
"provider_endpoint_kind": ep_kind,
"has_format_conversion": has_format_conversion,
"is_stream": is_stream,
"status_code": status_code,
@@ -228,7 +257,11 @@ def update_existing_usage(
existing_usage.model = usage_params["model"]
existing_usage.request_type = usage_params["request_type"]
existing_usage.api_format = usage_params["api_format"]
existing_usage.api_family = usage_params.get("api_family")
existing_usage.endpoint_kind = usage_params.get("endpoint_kind")
existing_usage.endpoint_api_format = usage_params["endpoint_api_format"]
existing_usage.provider_api_family = usage_params.get("provider_api_family")
existing_usage.provider_endpoint_kind = usage_params.get("provider_endpoint_kind")
existing_usage.has_format_conversion = usage_params["has_format_conversion"]
existing_usage.is_stream = usage_params["is_stream"]
existing_usage.status = usage_params["status"]

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
from sqlalchemy.orm import Session
@@ -23,6 +23,8 @@ class UsageRecordParams:
cache_read_input_tokens: int
request_type: str
api_format: str | None
api_family: str | None # 协议族(从 Adapter 层透传)
endpoint_kind: str | None # 端点类型(从 Adapter 层透传)
endpoint_api_format: str | None # 端点原生 API 格式
has_format_conversion: bool # 是否发生了格式转换
is_stream: bool

View File

@@ -70,6 +70,8 @@ def _event_to_record(event: UsageEvent) -> dict[str, Any]:
"cache_read_input_tokens": data.get("cache_read_input_tokens") or 0,
"request_type": data.get("request_type") or "chat",
"api_format": data.get("api_format"),
"api_family": data.get("api_family"),
"endpoint_kind": data.get("endpoint_kind"),
"endpoint_api_format": data.get("endpoint_api_format"),
"has_format_conversion": data.get("has_format_conversion"),
"is_stream": data.get("is_stream", True),

View File

@@ -112,6 +112,8 @@ class UsageRecorder:
cache_read_input_tokens=usage.cache_read_input_tokens,
request_type="chat",
api_format=metadata.api_format,
api_family=metadata.api_family,
endpoint_kind=metadata.endpoint_kind,
is_stream=result.is_stream,
response_time_ms=result.response_time_ms,
status_code=200,
@@ -181,6 +183,8 @@ class UsageRecorder:
output_tokens=0,
request_type="chat",
api_format=metadata.api_format,
api_family=metadata.api_family,
endpoint_kind=metadata.endpoint_kind,
is_stream=result.is_stream,
response_time_ms=result.response_time_ms,
status_code=result.status_code,

View File

@@ -67,6 +67,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens: int = 0,
request_type: str = "chat",
api_format: str | None = None,
api_family: str | None = None,
endpoint_kind: str | None = None,
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
is_stream: bool = False,
@@ -116,6 +118,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens=cache_read_input_tokens,
request_type=request_type,
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,
@@ -189,6 +193,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens: int = 0,
request_type: str = "chat",
api_format: str | None = None,
api_family: str | None = None,
endpoint_kind: str | None = None,
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
is_stream: bool = False,
@@ -240,6 +246,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens=cache_read_input_tokens,
request_type=request_type,
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,
@@ -380,6 +388,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_creation_input_tokens: int = 0,
cache_read_input_tokens: int = 0,
api_format: str | None = None,
api_family: str | None = None,
endpoint_kind: str | None = None,
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
is_stream: bool = False,
@@ -443,6 +453,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens=cache_read_input_tokens,
request_type=request_type,
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,
@@ -744,6 +756,8 @@ class UsageRecordingMixin(UsageBillingIntegrationMixin):
cache_read_input_tokens=int(record.get("cache_read_input_tokens") or 0),
request_type=record.get("request_type") or "chat",
api_format=record.get("api_format"),
api_family=record.get("api_family"),
endpoint_kind=record.get("endpoint_kind"),
endpoint_api_format=record.get("endpoint_api_format"),
has_format_conversion=bool(record.get("has_format_conversion")),
is_stream=bool(record.get("is_stream", True)),

View File

@@ -42,6 +42,9 @@ class StreamUsageTracker:
provider_api_key_id: str | None = None,
# API 格式(用于选择正确的响应解析器)
api_format: str | None = None,
# 结构化格式维度(从 Adapter 层透传)
api_family: str | None = None,
endpoint_kind: str | None = None,
# 格式转换信息
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
@@ -85,6 +88,8 @@ class StreamUsageTracker:
# API 格式和响应解析器
self.api_format = api_format or "claude:chat"
self.api_family = api_family
self.endpoint_kind = endpoint_kind
self.endpoint_api_format = endpoint_api_format
self.has_format_conversion = has_format_conversion
self.response_parser = get_parser_for_format(self.api_format)
@@ -759,6 +764,8 @@ class StreamUsageTracker:
cache_read_input_tokens=self.cache_read_input_tokens,
request_type="chat",
api_format=self.api_format,
api_family=self.api_family,
endpoint_kind=self.endpoint_kind,
endpoint_api_format=self.endpoint_api_format,
has_format_conversion=self.has_format_conversion,
is_stream=True,
@@ -854,6 +861,9 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
provider_api_key_id: str | None = None,
# API 格式(用于选择正确的响应解析器)
api_format: str | None = None,
# 结构化格式维度(从 Adapter 层透传)
api_family: str | None = None,
endpoint_kind: str | None = None,
# 格式转换信息
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
@@ -873,6 +883,8 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
provider_endpoint_id,
provider_api_key_id,
api_format,
api_family,
endpoint_kind,
endpoint_api_format,
has_format_conversion,
)
@@ -1126,6 +1138,9 @@ def create_stream_tracker(
provider_api_key_id: str | None = None,
# API 格式(用于选择正确的响应解析器)
api_format: str | None = None,
# 结构化格式维度(从 Adapter 层透传)
api_family: str | None = None,
endpoint_kind: str | None = None,
# 格式转换信息
endpoint_api_format: str | None = None,
has_format_conversion: bool = False,
@@ -1171,6 +1186,8 @@ def create_stream_tracker(
provider_endpoint_id,
provider_api_key_id,
api_format,
api_family,
endpoint_kind,
endpoint_api_format,
has_format_conversion,
)
@@ -1190,6 +1207,8 @@ def create_stream_tracker(
provider_endpoint_id,
provider_api_key_id,
api_format,
api_family,
endpoint_kind,
endpoint_api_format,
has_format_conversion,
)

View File

@@ -81,6 +81,9 @@ class MessageTelemetry:
provider_endpoint_id: str | None = None,
provider_api_key_id: str | None = None,
api_format: str | None = None,
# 结构化格式维度(从 Adapter 层透传)
api_family: str | None = None,
endpoint_kind: str | None = None,
# 格式转换追踪
endpoint_api_format: str | None = None, # 端点原生 API 格式
has_format_conversion: bool = False, # 是否发生了格式转换
@@ -110,6 +113,8 @@ class MessageTelemetry:
cache_read_input_tokens=cache_read_tokens,
request_type="chat",
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,
@@ -167,6 +172,8 @@ class MessageTelemetry:
request_headers: dict[str, Any],
is_stream: bool,
api_format: str | None = None,
api_family: str | None = None,
endpoint_kind: str | None = None,
provider_request_headers: dict[str, Any] | None = None,
provider_request_body: Any | None = None,
# 预估 token 信息(来自 message_start 事件,用于中断请求的成本估算)
@@ -222,6 +229,8 @@ class MessageTelemetry:
cache_read_input_tokens=cache_read_tokens,
request_type="chat",
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,
@@ -259,6 +268,8 @@ class MessageTelemetry:
request_headers: dict[str, Any],
is_stream: bool,
api_format: str | None = None,
api_family: str | None = None,
endpoint_kind: str | None = None,
provider_request_headers: dict[str, Any] | None = None,
provider_request_body: Any | None = None,
input_tokens: int = 0,
@@ -299,6 +310,8 @@ class MessageTelemetry:
cache_read_input_tokens=cache_read_tokens,
request_type="chat",
api_format=api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
endpoint_api_format=endpoint_api_format,
has_format_conversion=has_format_conversion,
is_stream=is_stream,

View File

@@ -225,6 +225,10 @@ class QueueTelemetryWriter(TelemetryWriter):
data["request_type"] = request_type
if kwargs.get("api_format"):
data["api_format"] = kwargs["api_format"]
if kwargs.get("api_family"):
data["api_family"] = kwargs["api_family"]
if kwargs.get("endpoint_kind"):
data["endpoint_kind"] = kwargs["endpoint_kind"]
if kwargs.get("endpoint_api_format"):
data["endpoint_api_format"] = kwargs["endpoint_api_format"]
if kwargs.get("has_format_conversion"):