refactor: 全局适配 ApiFamily/EndpointKind 结构化标识体系

将新的 (ApiFamily, EndpointKind) / `family:kind` 签名体系应用到整个代码库:
- API Handlers: 所有 adapter/handler 使用新的签名格式
- Services: provider, model, usage, cache, auth 等服务层适配
- Database: ProviderEndpoint 新增 api_family/endpoint_kind 字段
- Frontend: Provider 管理、Usage 表格等组件适配
- Tests: 更新所有相关测试用例
This commit is contained in:
fawney19
2026-02-01 17:28:00 +08:00
parent c246ccfc91
commit 7b66505634
219 changed files with 4732 additions and 2545 deletions

View File

@@ -27,9 +27,9 @@
from __future__ import annotations
import asyncio
import time
from collections.abc import Awaitable, Callable, Coroutine
from typing import (
TYPE_CHECKING,
Any,
@@ -38,18 +38,14 @@ from typing import (
runtime_checkable,
)
from collections.abc import Callable
from collections.abc import Awaitable, Coroutine
from fastapi import Request
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.orm import Session
from src.clients.redis_client import get_redis_client_sync
from src.core.api_format import APIFormat, resolve_api_format
from src.core.logger import logger
from src.services.orchestration.fallback_orchestrator import FallbackOrchestrator
from src.services.provider.format import normalize_api_format
from src.services.provider.format import normalize_endpoint_signature
from src.services.system.audit import audit_service
from src.services.usage.service import UsageService
@@ -238,7 +234,9 @@ class MessageTelemetry:
"""
provider_name = provider or "unknown"
if provider_name == "unknown":
logger.warning(f"[Telemetry] Recording failure with unknown provider (request_id={self.request_id})")
logger.warning(
f"[Telemetry] Recording failure with unknown provider (request_id={self.request_id})"
)
await UsageService.record_usage(
db=self.db,
@@ -393,8 +391,9 @@ class BaseMessageHandler:
self.client_ip = client_ip
self.user_agent = user_agent
self.start_time = start_time
self.allowed_api_formats = allowed_api_formats or [APIFormat.CLAUDE.value]
self.primary_api_format = normalize_api_format(self.allowed_api_formats[0])
# 新模式endpoint signature keyfamily:kind如 "claude:chat"
self.allowed_api_formats = allowed_api_formats or ["claude:chat"]
self.primary_api_format = normalize_endpoint_signature(self.allowed_api_formats[0])
self.adapter_detector = adapter_detector
redis_client = get_redis_client_sync()
@@ -446,13 +445,6 @@ class BaseMessageHandler:
"""可选的 Key 优先级解析钩子(默认不启用)。"""
return None
def get_api_format(self, provider_type: str | None = None) -> APIFormat:
"""根据 provider_type 解析 API 格式,未知类型默认 OPENAI"""
if provider_type:
result = resolve_api_format(provider_type, default=APIFormat.OPENAI)
return result or APIFormat.OPENAI
return self.primary_api_format
def build_provider_payload(
self,
original_body: dict[str, Any],
@@ -477,6 +469,7 @@ class BaseMessageHandler:
request_id: 请求 ID如果不传则使用 self.request_id
"""
import asyncio
from src.database.database import get_db
target_request_id = request_id or self.request_id
@@ -511,6 +504,7 @@ class BaseMessageHandler:
ctx: 流式上下文,包含 provider 相关信息
"""
import asyncio
from src.database.database import get_db
target_request_id = self.request_id
@@ -567,14 +561,23 @@ class BaseMessageHandler:
error: 异常对象
"""
from src.core.exceptions import (
ModelNotSupportedException,
ProviderException,
QuotaExceededException,
RateLimitException,
ModelNotSupportedException,
UpstreamClientException,
)
if isinstance(error, (ProviderException, QuotaExceededException, RateLimitException, ModelNotSupportedException, UpstreamClientException)):
if isinstance(
error,
(
ProviderException,
QuotaExceededException,
RateLimitException,
ModelNotSupportedException,
UpstreamClientException,
),
):
# 业务异常:简洁日志,不打印堆栈
logger.error(f"{message}: [{type(error).__name__}] {error}")
else:

View File

@@ -21,7 +21,7 @@ from __future__ import annotations
import time
import traceback
from abc import abstractmethod
from typing import Any
from typing import Any, ClassVar
import httpx
from fastapi import HTTPException, Request
@@ -32,12 +32,13 @@ from src.api.base.adapter import ApiAdapter, ApiMode
from src.api.base.context import ApiRequestContext
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import (
APIFormat,
build_adapter_base_headers,
build_adapter_headers,
get_adapter_protected_keys,
ApiFamily,
EndpointKind,
build_adapter_base_headers_for_endpoint,
build_adapter_headers_for_endpoint,
get_adapter_protected_keys_for_endpoint,
get_auth_handler,
get_default_auth_method,
get_default_auth_method_for_endpoint,
)
from src.core.exceptions import (
InvalidRequestException,
@@ -70,6 +71,10 @@ class ChatAdapterBase(ApiAdapter):
FORMAT_ID: str = "UNKNOWN"
HANDLER_CLASS: type[ChatHandlerBase]
# 新架构:结构化标识(逐步替代直接依赖 FORMAT_ID 的语义)
API_FAMILY: ClassVar[ApiFamily | None] = None
ENDPOINT_KIND: ClassVar[EndpointKind] = EndpointKind.CHAT
# 适配器配置
name: str = "chat.base"
mode = ApiMode.STANDARD
@@ -77,14 +82,6 @@ class ChatAdapterBase(ApiAdapter):
# 计费模板配置(子类可覆盖,如 "claude", "openai", "gemini"
BILLING_TEMPLATE: str = "claude"
@classmethod
def _get_api_format(cls) -> APIFormat:
"""获取 API 格式枚举,用于调用 headers.py 的统一函数"""
try:
return APIFormat[cls.FORMAT_ID]
except KeyError:
return APIFormat.OPENAI # 默认回退
# 子类可以配置的特殊方法用于check_endpoint
@classmethod
def build_endpoint_url(cls, base_url: str) -> str:
@@ -95,19 +92,19 @@ class ChatAdapterBase(ApiAdapter):
@classmethod
def build_base_headers(cls, api_key: str) -> dict[str, str]:
"""构建基础请求头,使用统一的 headers.py 实现"""
return build_adapter_base_headers(cls._get_api_format(), api_key)
return build_adapter_base_headers_for_endpoint(cls.FORMAT_ID, api_key)
@classmethod
def get_protected_header_keys(cls) -> tuple:
"""返回不应被extra_headers覆盖的头部key使用统一的 headers.py 实现"""
return get_adapter_protected_keys(cls._get_api_format())
return get_adapter_protected_keys_for_endpoint(cls.FORMAT_ID)
@classmethod
def build_headers_with_extra(
cls, api_key: str, extra_headers: dict[str, str] | None = None
) -> dict[str, str]:
"""构建完整请求头(包含 extra_headers使用统一的 headers.py 实现"""
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
return build_adapter_headers_for_endpoint(cls.FORMAT_ID, api_key, extra_headers)
@classmethod
def build_request_body(cls, request_data: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -125,7 +122,7 @@ class ChatAdapterBase(ApiAdapter):
def extract_api_key(self, request: Request) -> str | None:
"""从请求中提取 API 密钥,使用 AuthHandler 新流程"""
auth_method = get_default_auth_method(self._get_api_format())
auth_method = get_default_auth_method_for_endpoint(self.FORMAT_ID)
handler = get_auth_handler(auth_method)
return handler.extract_credentials(request)
@@ -742,7 +739,7 @@ def get_adapter_class(api_format: str) -> type[ChatAdapterBase] | None:
根据 API format 获取 Adapter 类
Args:
api_format: API 格式标识(如 "CLAUDE", "OPENAI", "GEMINI"
api_format: API 格式标识(如 "openai:chat", "claude:chat", "gemini:chat"
Returns:
对应的 Adapter 类,如果未找到返回 None

View File

@@ -413,17 +413,18 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
mapped_model: 映射后的模型名
fallback_model: 兜底模型名(无映射时使用)
"""
from src.core.api_format import APIFormat
from src.core.api_format.metadata import API_FORMAT_DEFINITIONS
from src.core.api_format.metadata import resolve_endpoint_definition
try:
target_format = APIFormat(provider_api_format.upper())
target_meta = API_FORMAT_DEFINITIONS.get(target_format)
if target_meta and target_meta.model_in_body:
request_body["model"] = mapped_model or fallback_model
except (ValueError, KeyError):
# 未知格式,默认设置 model 字段
target_meta = resolve_endpoint_definition(provider_api_format)
if target_meta is None:
# 未知格式,保守处理:默认设置 model
request_body["model"] = mapped_model or fallback_model
return
if target_meta.model_in_body:
request_body["model"] = mapped_model or fallback_model
else:
request_body.pop("model", None)
def _set_stream_after_conversion(
self,
@@ -444,26 +445,26 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
provider_api_format: Provider 侧 API 格式
is_stream: 是否为流式请求
"""
from src.core.api_format import APIFormat
from src.core.api_format.metadata import API_FORMAT_DEFINITIONS
from src.core.api_format.metadata import resolve_endpoint_definition
try:
client_format = APIFormat(client_api_format.upper())
provider_format = APIFormat(provider_api_format.upper())
client_meta = resolve_endpoint_definition(client_api_format)
provider_meta = resolve_endpoint_definition(provider_api_format)
client_meta = API_FORMAT_DEFINITIONS.get(client_format)
provider_meta = API_FORMAT_DEFINITIONS.get(provider_format)
# 默认stream_in_body=True如 OpenAI/Claude
client_uses_stream = client_meta.stream_in_body if client_meta else True
provider_uses_stream = provider_meta.stream_in_body if provider_meta else True
# 如果客户端格式不使用 stream 字段,但 Provider 格式需要
client_uses_stream = client_meta.stream_in_body if client_meta else True
provider_uses_stream = provider_meta.stream_in_body if provider_meta else True
# Provider 不使用 stream 字段(如 Gemini确保移除
if not provider_uses_stream:
request_body.pop("stream", None)
return
if not client_uses_stream and provider_uses_stream:
request_body["stream"] = is_stream
except (ValueError, KeyError):
# 未知格式,保守处理:如果请求体中没有 stream 字段则设置
if "stream" not in request_body:
request_body["stream"] = is_stream
# 如果客户端格式不使用 stream 字段,但 Provider 格式需要:补齐
if not client_uses_stream and provider_uses_stream:
request_body["stream"] = is_stream
elif "stream" not in request_body:
# 保守兜底:目标需要 stream 且当前缺失时写入
request_body["stream"] = is_stream
async def _get_mapped_model(
self,

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
import time
import traceback
from typing import Any
from typing import Any, ClassVar
import httpx
from fastapi import HTTPException, Request
@@ -30,12 +30,13 @@ from src.api.base.adapter import ApiAdapter, ApiMode
from src.api.base.context import ApiRequestContext
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
from src.core.api_format import (
APIFormat,
build_adapter_base_headers,
build_adapter_headers,
get_adapter_protected_keys,
ApiFamily,
EndpointKind,
build_adapter_base_headers_for_endpoint,
build_adapter_headers_for_endpoint,
get_adapter_protected_keys_for_endpoint,
get_auth_handler,
get_default_auth_method,
get_default_auth_method_for_endpoint,
)
from src.core.exceptions import (
InvalidRequestException,
@@ -68,6 +69,10 @@ class CliAdapterBase(ApiAdapter):
FORMAT_ID: str = "UNKNOWN"
HANDLER_CLASS: type[CliMessageHandlerBase]
# 新架构:结构化标识(逐步替代直接依赖 FORMAT_ID 的语义)
API_FAMILY: ClassVar[ApiFamily | None] = None
ENDPOINT_KIND: ClassVar[EndpointKind] = EndpointKind.CLI
# 适配器配置
name: str = "cli.base"
mode = ApiMode.PROXY
@@ -82,21 +87,13 @@ class CliAdapterBase(ApiAdapter):
# API 格式与头部处理 - 使用统一的 headers.py 函数
# =========================================================================
@classmethod
def _get_api_format(cls) -> APIFormat:
"""将 FORMAT_ID 转换为 APIFormat 枚举"""
try:
return APIFormat[cls.FORMAT_ID]
except KeyError:
return APIFormat.OPENAI
def extract_api_key(self, request: Request) -> str | None:
"""
从请求中提取 API 密钥
使用 AuthHandler 新流程,根据 API 格式选择认证方式。
"""
auth_method = get_default_auth_method(self._get_api_format())
auth_method = get_default_auth_method_for_endpoint(self.FORMAT_ID)
handler = get_auth_handler(auth_method)
return handler.extract_credentials(request)
@@ -107,7 +104,7 @@ class CliAdapterBase(ApiAdapter):
使用统一的头部处理函数。
"""
return build_adapter_base_headers(cls._get_api_format(), api_key)
return build_adapter_base_headers_for_endpoint(cls.FORMAT_ID, api_key)
@classmethod
def build_headers_with_extra(
@@ -118,7 +115,7 @@ class CliAdapterBase(ApiAdapter):
使用统一的头部处理函数,自动保护关键头部不被覆盖。
"""
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
return build_adapter_headers_for_endpoint(cls.FORMAT_ID, api_key, extra_headers)
@classmethod
def get_protected_header_keys(cls) -> tuple[str, ...]:
@@ -127,7 +124,7 @@ class CliAdapterBase(ApiAdapter):
使用统一的头部处理函数。
"""
return get_adapter_protected_keys(cls._get_api_format())
return get_adapter_protected_keys_for_endpoint(cls.FORMAT_ID)
async def handle(self, context: ApiRequestContext) -> Any:
"""处理 CLI API 请求"""
@@ -778,7 +775,15 @@ def _ensure_cli_adapters_loaded() -> None:
def get_cli_adapter_class(api_format: str) -> type[CliAdapterBase] | None:
"""根据 API format 获取 CLI Adapter 类"""
"""
根据 API format 获取 CLI Adapter 类
Args:
api_format: API 格式标识(如 "openai:cli", "claude:cli", "gemini:cli"
Returns:
对应的 CLI Adapter 类,如果未找到返回 None
"""
_ensure_cli_adapters_loaded()
return _CLI_ADAPTER_REGISTRY.get(api_format.upper()) if api_format else None

View File

@@ -16,21 +16,19 @@ import asyncio
import codecs
import json
import time
from collections.abc import AsyncGenerator, Callable
from typing import (
TYPE_CHECKING,
Any,
)
from collections.abc import Callable
from collections.abc import AsyncGenerator
import httpx
from fastapi import BackgroundTasks, Request
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.orm import Session
if TYPE_CHECKING:
from src.core.api_format import ApiFormatDefinition
from src.core.api_format import EndpointDefinition
from src.api.handlers.base.base_handler import (
BaseMessageHandler,
@@ -78,7 +76,6 @@ from src.services.provider.transport import build_provider_url
from src.utils.sse_parser import SSEEventParser
from src.utils.timeout import read_first_chunk_with_ttfb_timeout
# ==============================================================================
# SSE 行解析辅助函数
# ==============================================================================
@@ -163,7 +160,7 @@ def _format_converted_events_to_sse(
SSE 行列表(每个元素是完整的 SSE 事件,包含尾部空行)
"""
result: list[str] = []
needs_event_line = client_format.upper() in ("CLAUDE", "CLAUDE_CLI")
needs_event_line = str(client_format or "").strip().lower().startswith("claude:")
for evt in converted_events:
payload = json.dumps(evt, ensure_ascii=False)
@@ -357,16 +354,11 @@ class CliMessageHandlerBase(BaseMessageHandler):
return request_body
@staticmethod
def _get_format_metadata(format_id: str) -> ApiFormatDefinition | None:
"""获取格式元数据(解析失败返回 None"""
from src.core.api_format import APIFormat
from src.core.api_format.metadata import API_FORMAT_DEFINITIONS
def _get_format_metadata(format_id: str) -> "EndpointDefinition | None":
"""获取 endpoint 元数据(解析失败返回 None"""
from src.core.api_format.metadata import resolve_endpoint_definition
try:
fmt = APIFormat(format_id.upper())
return API_FORMAT_DEFINITIONS.get(fmt)
except (ValueError, KeyError):
return None
return resolve_endpoint_definition(format_id)
def _finalize_converted_request(
self,
@@ -447,7 +439,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
)
# 先计算 URL 模型(在清理 body 中的 model 字段之前)
url_model = self.get_model_for_url(converted_body, mapped_model) or mapped_model or fallback_model
url_model = (
self.get_model_for_url(converted_body, mapped_model) or mapped_model or fallback_model
)
# 统一设置并清理 model/stream 字段
self._finalize_converted_request(
@@ -704,7 +698,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
else str(ctx.client_api_format)
)
provider_api_format = str(ctx.provider_api_format or "")
needs_conversion = bool(getattr(candidate, "needs_conversion", False)) if candidate else False
needs_conversion = (
bool(getattr(candidate, "needs_conversion", False)) if candidate else False
)
ctx.needs_conversion = needs_conversion
# 跨格式:先做请求体转换(失败触发 failover
@@ -720,7 +716,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
else:
# 同格式:按原逻辑做轻量清理(子类可覆盖)
request_body = self.prepare_provider_request_body(request_body)
url_model = self.get_model_for_url(request_body, mapped_model) or mapped_model or ctx.model
url_model = (
self.get_model_for_url(request_body, mapped_model) or mapped_model or ctx.model
)
# 获取认证信息(处理 Service Account 等异步认证场景)
auth_info = await get_provider_auth(endpoint, key)
@@ -964,7 +962,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
# 格式转换或直接透传
if needs_conversion:
converted_lines, converted_events = self._convert_sse_line(ctx, line, events)
converted_lines, converted_events = self._convert_sse_line(
ctx, line, events
)
# 记录转换后的数据到 parsed_chunks
self._record_converted_chunks(ctx, converted_events)
for converted_line in converted_lines:
@@ -1015,8 +1015,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
else:
logger.debug("流式数据转发完成")
# 为 OpenAI 客户端补齐 [DONE] 标记(非 CLI 格式)
client_fmt = (ctx.client_api_format or "").upper()
if needs_conversion and client_fmt == "OPENAI":
client_fmt = (ctx.client_api_format or "").strip().lower()
if needs_conversion and client_fmt == "openai:chat":
yield b"data: [DONE]\n\n"
except GeneratorExit:
@@ -1306,7 +1306,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
# 格式转换或直接透传
if needs_conversion:
converted_lines, converted_events = self._convert_sse_line(ctx, line, events)
converted_lines, converted_events = self._convert_sse_line(
ctx, line, events
)
# 记录转换后的数据到 parsed_chunks
self._record_converted_chunks(ctx, converted_events)
for converted_line in converted_lines:
@@ -1383,7 +1385,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
# 格式转换或直接透传
if needs_conversion:
converted_lines, converted_events = self._convert_sse_line(ctx, line, events)
converted_lines, converted_events = self._convert_sse_line(
ctx, line, events
)
# 记录转换后的数据到 parsed_chunks
self._record_converted_chunks(ctx, converted_events)
for converted_line in converted_lines:
@@ -1437,8 +1441,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
else:
logger.debug("流式数据转发完成")
# 为 OpenAI 客户端补齐 [DONE] 标记(非 CLI 格式)
client_fmt = (ctx.client_api_format or "").upper()
if needs_conversion and client_fmt == "OPENAI":
client_fmt = (ctx.client_api_format or "").strip().lower()
if needs_conversion and client_fmt == "openai:chat":
yield b"data: [DONE]\n\n"
except GeneratorExit:
@@ -1693,19 +1697,17 @@ class CliMessageHandlerBase(BaseMessageHandler):
new_input = usage.get("input_tokens", 0) or 0
new_output = usage.get("output_tokens", 0) or 0
new_cached = usage.get("cache_read_tokens") or usage.get("cache_read_input_tokens") or 0
new_cache_creation = usage.get("cache_creation_tokens") or usage.get("cache_creation_input_tokens") or 0
new_cache_creation = (
usage.get("cache_creation_tokens") or usage.get("cache_creation_input_tokens") or 0
)
# 取最大值更新(与 _process_event_data 相同的策略)
if new_input > ctx.input_tokens:
ctx.input_tokens = new_input
logger.debug(
f"[{ctx.request_id}] 从转换后事件更新 input_tokens: {new_input}"
)
logger.debug(f"[{ctx.request_id}] 从转换后事件更新 input_tokens: {new_input}")
if new_output > ctx.output_tokens:
ctx.output_tokens = new_output
logger.debug(
f"[{ctx.request_id}] 从转换后事件更新 output_tokens: {new_output}"
)
logger.debug(f"[{ctx.request_id}] 从转换后事件更新 output_tokens: {new_output}")
if new_cached > ctx.cached_tokens:
ctx.cached_tokens = new_cached
if new_cache_creation > ctx.cache_creation_tokens:
@@ -1969,9 +1971,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
# Provider 响应元数据(如 Gemini 的 modelVersion
response_metadata=ctx.response_metadata if ctx.response_metadata else None,
)
logger.debug(
f"[{ctx.request_id}] Usage 记录完成: cost=${total_cost:.6f}"
)
logger.debug(f"[{ctx.request_id}] Usage 记录完成: cost=${total_cost:.6f}")
# 简洁的请求完成摘要(两行格式)
line1 = f"[OK] {self.request_id[:8]} | {ctx.model} | {ctx.provider_name}"
if ctx.first_byte_time_ms:
@@ -2186,7 +2186,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
else:
# 同格式:按原逻辑做轻量清理(子类可覆盖)
request_body = self.prepare_provider_request_body(request_body)
url_model = self.get_model_for_url(request_body, mapped_model) or mapped_model or model
url_model = (
self.get_model_for_url(request_body, mapped_model) or mapped_model or model
)
# 获取认证信息(处理 Service Account 等异步认证场景)
auth_info = await get_provider_auth(endpoint, key)
@@ -2532,7 +2534,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
- CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI格式相同只是认证不同可透传
- OPENAI 和 OPENAI_CLI格式不同Chat Completions vs Responses API需要转换
"""
from src.core.api_format.utils import get_base_format
from src.core.api_format.metadata import can_passthrough_endpoint
from src.core.api_format.signature import normalize_signature_key
if not ctx.provider_api_format or not ctx.client_api_format:
logger.debug(
@@ -2541,8 +2544,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
)
return False
provider_format = str(ctx.provider_api_format).upper()
client_format = str(ctx.client_api_format).upper()
provider_format = normalize_signature_key(str(ctx.provider_api_format))
client_format = normalize_signature_key(str(ctx.client_api_format))
# 1. 格式完全匹配 -> 不需要转换
if provider_format == client_format:
@@ -2552,26 +2555,18 @@ class CliMessageHandlerBase(BaseMessageHandler):
)
return False
# 2. 同族格式检查
provider_base = get_base_format(provider_format)
client_base = get_base_format(client_format)
if provider_base == client_base:
# OPENAI 和 OPENAI_CLI 的请求/响应格式不同,需要转换
# CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI 格式相同,可透传
result = provider_base == "OPENAI"
# 2. 根据 data_format_id 判断是否可透传(可透传则不需要转换)
if can_passthrough_endpoint(client_format, provider_format):
logger.debug(
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
f"provider={provider_format}(base={provider_base}), "
f"client={client_format}(base={client_base}) -> {result} (same family, OPENAI needs conversion)"
f"provider={provider_format}, client={client_format} -> False (passthroughable)"
)
return result
return False
# 3. 跨格式 -> 需要转换
# 3. 其他情况 -> 需要转换
logger.debug(
f"[{getattr(ctx, 'request_id', 'unknown')}] _needs_format_conversion: "
f"provider={provider_format}(base={provider_base}), "
f"client={client_format}(base={client_base}) -> True (cross-format)"
f"provider={provider_format}, client={client_format} -> True"
)
return True
@@ -2617,17 +2612,17 @@ class CliMessageHandlerBase(BaseMessageHandler):
if not line or line.strip() == "":
return ([line] if line else [], [])
client_format = (ctx.client_api_format or "").upper()
client_format = (ctx.client_api_format or "").strip().lower()
# [DONE] 标记处理:只有 OpenAI 客户端需要Claude 客户端不需要
if line == "data: [DONE]":
if client_format.startswith("OPENAI"):
if client_format.startswith("openai"):
return [line], []
else:
# Claude/Gemini 客户端不需要 [DONE] 标记
return [], []
provider_format = (ctx.provider_api_format or "").upper()
provider_format = (ctx.provider_api_format or "").strip().lower()
# 过滤上游控制行id/retry避免与目标格式混淆
if line.startswith(("id:", "retry:")):
@@ -2682,9 +2677,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
logger.warning(f"格式转换失败,透传原始数据: {e}")
return [line], []
def _parse_sse_line_to_json(
self, line: str, provider_format: str
) -> tuple[Any | None, str]:
def _parse_sse_line_to_json(self, line: str, provider_format: str) -> tuple[Any | None, str]:
"""
解析 SSE 行为 JSON 对象
@@ -2718,9 +2711,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
return None, "skip"
# Gemini JSON-array 格式
if provider_format == "GEMINI":
if provider_format.startswith("gemini"):
return _parse_gemini_json_array_line(line)
# 其他格式:无法识别,透传
return None, "passthrough"

View File

@@ -15,18 +15,23 @@
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from collections.abc import Iterable
import asyncio
import json
import time
import uuid
import json
import asyncio
from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any
import httpx
from src.core.api_format import (
CORE_REDACT_HEADERS,
merge_headers_with_protection,
redact_headers_for_log,
)
from src.core.logger import logger
from src.core.api_format import CORE_REDACT_HEADERS, merge_headers_with_protection, redact_headers_for_log
from src.utils.ssl_utils import get_ssl_context
@@ -88,7 +93,7 @@ async def run_endpoint_check(
provider_id=provider_id,
db=db,
user=user,
request_id=str(uuid.uuid4())[:8]
request_id=str(uuid.uuid4())[:8],
)
# 使用协调器执行检查
@@ -114,6 +119,7 @@ async def run_endpoint_check(
return response_data
async def _calculate_and_record_usage(
*,
db: Any,
@@ -146,9 +152,9 @@ async def _calculate_and_record_usage(
Returns:
Dict包含用量统计信息
"""
from src.services.usage.service import UsageService
from src.services.request.candidate import RequestCandidateService
from src.models.database import ApiKey, ProviderAPIKey
from src.services.request.candidate import RequestCandidateService
from src.services.usage.service import UsageService
# 获取Provider API Key对象不是用户API Key
provider_api_key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == api_key_id).first()
@@ -160,6 +166,7 @@ async def _calculate_and_record_usage(
provider_endpoint = None
if api_format and provider_api_key.provider_id:
from src.models.database import Provider
provider = db.query(Provider).filter(Provider.id == provider_api_key.provider_id).first()
if provider:
for ep in provider.endpoints:
@@ -172,7 +179,9 @@ async def _calculate_and_record_usage(
if user:
try:
user_api_key = db.query(ApiKey).filter(ApiKey.user_id == user.id).first()
logger.info(f"[endpoint_check] User API Key found: {user_api_key.id if user_api_key else None}")
logger.info(
f"[endpoint_check] User API Key found: {user_api_key.id if user_api_key else None}"
)
except Exception as e:
logger.warning(f"[endpoint_check] Failed to get user API Key: {e}")
user_api_key = None
@@ -181,7 +190,12 @@ async def _calculate_and_record_usage(
# 用量记录会关联到执行测试的用户但实际的API调用使用Provider的配置
# Token计数 - 优先使用直接传递的数据,否则使用原有逻辑
if input_tokens is None or output_tokens is None or cache_creation_input_tokens is None or cache_read_input_tokens is None:
if (
input_tokens is None
or output_tokens is None
or cache_creation_input_tokens is None
or cache_read_input_tokens is None
):
# 使用原有逻辑计算token
logger.info(f"[endpoint_check] Calculating tokens from response data")
@@ -191,7 +205,7 @@ async def _calculate_and_record_usage(
usage_info = response_data.get("usage", {})
if not api_format:
api_format = "OPENAI"
api_format = "openai:chat"
logger.info(f"[endpoint_check] Detected API format: {api_format}")
@@ -199,8 +213,9 @@ async def _calculate_and_record_usage(
logger.info(f"[endpoint_check] Found usage field in response: {usage_info}")
# 使用提取函数获取token数据
api_identifier = provider_name # 在这个旧函数中我们只能使用provider_name
extracted_input, extracted_output, extracted_cache_creation, extracted_cache_read = \
extracted_input, extracted_output, extracted_cache_creation, extracted_cache_read = (
_extract_tokens_from_response(api_identifier, response_data)
)
input_tokens = input_tokens or extracted_input
output_tokens = output_tokens or extracted_output
@@ -209,10 +224,13 @@ async def _calculate_and_record_usage(
else:
# 如果没有usage字段使用fallback
logger.warning(f"[endpoint_check] No usage field found in response, using fallback counting")
logger.warning(
f"[endpoint_check] No usage field found in response, using fallback counting"
)
try:
fallback_input, fallback_output, fallback_cache_creation, fallback_cache_read = \
fallback_input, fallback_output, fallback_cache_creation, fallback_cache_read = (
_fallback_token_counting(request_data, response_data)
)
input_tokens = input_tokens or fallback_input
output_tokens = output_tokens or fallback_output
@@ -226,16 +244,20 @@ async def _calculate_and_record_usage(
cache_creation_input_tokens = cache_creation_input_tokens or 0
cache_read_input_tokens = cache_read_input_tokens or 0
logger.info(f"[endpoint_check] Final token count | input={input_tokens}, output={output_tokens}, "
f"cache_creation={cache_creation_input_tokens}, cache_read={cache_read_input_tokens}")
logger.info(
f"[endpoint_check] Final token count | input={input_tokens}, output={output_tokens}, "
f"cache_creation={cache_creation_input_tokens}, cache_read={cache_read_input_tokens}"
)
try:
# 使用UsageService记录用量
# 测试请求会关联到执行测试的用户API Key但实际使用Provider API Key
logger.info(f"[endpoint_check] Recording usage | provider={provider_name}, model={model_name}, "
f"tokens=({input_tokens}+{output_tokens}), status={status_code}, "
f"user_api_key_id={user_api_key.id if user_api_key else None}, "
f"provider_endpoint_id={provider_endpoint.id if provider_endpoint else None}")
logger.info(
f"[endpoint_check] Recording usage | provider={provider_name}, model={model_name}, "
f"tokens=({input_tokens}+{output_tokens}), status={status_code}, "
f"user_api_key_id={user_api_key.id if user_api_key else None}, "
f"provider_endpoint_id={provider_endpoint.id if provider_endpoint else None}"
)
usage_record = await UsageService.record_usage_async(
db=db,
@@ -269,7 +291,9 @@ async def _calculate_and_record_usage(
# 检查费用计算是否成功
total_cost = float(usage_record.total_cost_usd) if usage_record.total_cost_usd else 0.0
actual_cost = float(usage_record.actual_total_cost_usd) if usage_record.actual_total_cost_usd else 0.0
actual_cost = (
float(usage_record.actual_total_cost_usd) if usage_record.actual_total_cost_usd else 0.0
)
cache_cost = float(usage_record.cache_cost_usd) if usage_record.cache_cost_usd else 0.0
# 如果费用为0但Token不为0可能是价格配置缺失使用默认价格
@@ -280,9 +304,11 @@ async def _calculate_and_record_usage(
total_cost = ((input_tokens + output_tokens) / 1_000_000) * fallback_price_per_1m
actual_cost = total_cost # 测试请求使用实际成本
logger.info(f"[endpoint_check] Usage recorded successfully | "
f"usage_id={usage_record.id}, total_cost=${total_cost:.6f}, "
f"actual_cost=${actual_cost:.6f}")
logger.info(
f"[endpoint_check] Usage recorded successfully | "
f"usage_id={usage_record.id}, total_cost=${total_cost:.6f}, "
f"actual_cost=${actual_cost:.6f}"
)
# 创建RequestCandidate记录用于监控追踪API
try:
@@ -323,7 +349,9 @@ async def _calculate_and_record_usage(
extra_data={"model_name": model_name, "api_format": api_format},
)
logger.info(f"[endpoint_check] RequestCandidate created | request_id=test_{request_id}, candidate_id={candidate.id}")
logger.info(
f"[endpoint_check] RequestCandidate created | request_id=test_{request_id}, candidate_id={candidate.id}"
)
except Exception as e:
logger.warning(f"[endpoint_check] Failed to create RequestCandidate: {e}")
# 不影响主要功能
@@ -359,7 +387,9 @@ async def _calculate_and_record_usage(
}
def _extract_tokens_from_response(api_identifier: str, response_data: dict[str, Any] | None) -> tuple[int, int, int, int]:
def _extract_tokens_from_response(
api_identifier: str, response_data: dict[str, Any] | None
) -> tuple[int, int, int, int]:
"""
从响应中提取Token计数信息
@@ -395,6 +425,7 @@ def _extract_tokens_from_response(api_identifier: str, response_data: dict[str,
# 尝试提取cache creation tokens
try:
from src.api.handlers.base.utils import extract_cache_creation_tokens
cache_creation_input_tokens = extract_cache_creation_tokens(usage_info)
except Exception as e:
logger.warning(f"[endpoint_check] Failed to extract cache creation tokens: {e}")
@@ -402,14 +433,18 @@ def _extract_tokens_from_response(api_identifier: str, response_data: dict[str,
elif "openai" in api_identifier_lower:
# OpenAI格式
input_tokens = usage_info.get("prompt_tokens", 0) or usage_info.get("input_tokens", 0)
output_tokens = usage_info.get("completion_tokens", 0) or usage_info.get("output_tokens", 0)
output_tokens = usage_info.get("completion_tokens", 0) or usage_info.get(
"output_tokens", 0
)
cache_creation_input_tokens = 0
cache_read_input_tokens = 0
elif "gemini" in api_identifier_lower or "google" in api_identifier_lower:
# Gemini格式 - 使用与OpenAI类似的字段名
input_tokens = usage_info.get("prompt_tokens", 0) or usage_info.get("input_tokens", 0)
output_tokens = usage_info.get("completion_tokens", 0) or usage_info.get("output_tokens", 0)
output_tokens = usage_info.get("completion_tokens", 0) or usage_info.get(
"output_tokens", 0
)
cache_creation_input_tokens = 0
cache_read_input_tokens = 0
@@ -421,31 +456,38 @@ def _extract_tokens_from_response(api_identifier: str, response_data: dict[str,
cache_read_input_tokens = usage_info.get("cache_read_input_tokens", 0)
try:
from src.api.handlers.base.utils import extract_cache_creation_tokens
cache_creation_input_tokens = extract_cache_creation_tokens(usage_info)
except Exception as e:
logger.warning(f"[endpoint_check] Failed to extract cache creation tokens: {e}")
else:
# 默认情况:尝试通用提取
logger.warning(f"[endpoint_check] Unknown API identifier: {api_identifier}, using generic token extraction")
logger.warning(
f"[endpoint_check] Unknown API identifier: {api_identifier}, using generic token extraction"
)
input_tokens = usage_info.get("input_tokens", 0) or usage_info.get("prompt_tokens", 0)
output_tokens = usage_info.get("output_tokens", 0) or usage_info.get("completion_tokens", 0)
output_tokens = usage_info.get("output_tokens", 0) or usage_info.get(
"completion_tokens", 0
)
except Exception as e:
logger.warning(f"[endpoint_check] Error extracting tokens from response: {e}")
return 0, 0, 0, 0
logger.info(f"[endpoint_check] Tokens extracted from response | "
f"api_identifier={api_identifier}, "
f"input={input_tokens}, output={output_tokens}, "
f"cache_creation={cache_creation_input_tokens}, cache_read={cache_read_input_tokens}")
logger.info(
f"[endpoint_check] Tokens extracted from response | "
f"api_identifier={api_identifier}, "
f"input={input_tokens}, output={output_tokens}, "
f"cache_creation={cache_creation_input_tokens}, cache_read={cache_read_input_tokens}"
)
return input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens
def _fallback_token_counting(request_data: dict[str, Any], response_data: dict[str, Any] | None) -> tuple[int, int, int, int]:
def _fallback_token_counting(
request_data: dict[str, Any], response_data: dict[str, Any] | None
) -> tuple[int, int, int, int]:
"""
回退的Token计数方法简单估算
@@ -496,16 +538,21 @@ def _fallback_token_counting(request_data: dict[str, Any], response_data: dict[s
output_text += part["text"]
output_tokens = max(1, len(output_text.split()) // 4)
logger.info(f"[endpoint_check] Fallback token count | input={input_tokens}, output={output_tokens}")
logger.info(
f"[endpoint_check] Fallback token count | input={input_tokens}, output={output_tokens}"
)
return input_tokens, output_tokens, 0, 0
# =========================================================================
# 重构后的架构类 - 分离关注点
# =========================================================================
@dataclass
class EndpointCheckRequest:
"""端点检查请求数据类"""
url: str
headers: dict[str, str]
json_body: dict[str, Any]
@@ -523,6 +570,7 @@ class EndpointCheckRequest:
@dataclass
class EndpointCheckResult:
"""端点检查结果数据类"""
status_code: int
headers: dict[str, str]
response_time_ms: int
@@ -547,9 +595,7 @@ class HttpRequestExecutor:
# 使用httpx进行异步请求
async with httpx.AsyncClient(timeout=self.timeout, verify=get_ssl_context()) as client:
response = await client.post(
url=request.url,
json=request.json_body,
headers=request.headers
url=request.url, json=request.json_body, headers=request.headers
)
end_time = time.time()
@@ -559,7 +605,9 @@ class HttpRequestExecutor:
if response.status_code == 200:
try:
response_data = response.json()
logger.debug(f"[{request.api_format}] check_endpoint | response | json={_truncate_repr(response_data)}")
logger.debug(
f"[{request.api_format}] check_endpoint | response | json={_truncate_repr(response_data)}"
)
except Exception:
response_data = None
logger.debug(f"[{request.api_format}] check_endpoint | response | invalid json")
@@ -569,18 +617,20 @@ class HttpRequestExecutor:
headers=dict(response.headers),
response_time_ms=response_time_ms,
request_id=request_id,
response_data=response_data
response_data=response_data,
)
else:
# 对于非200状态码使用错误处理器
error_body = response.text[:500] if response.text else "(empty)"
logger.debug(f"[{request.api_format}] check_endpoint | response | error={error_body}")
logger.debug(
f"[{request.api_format}] check_endpoint | response | error={error_body}"
)
# 创建HTTPStatusError让错误处理器处理
http_error = httpx.HTTPStatusError(
message=f"HTTP {response.status_code}: {error_body}",
request=None, # 我们不需要完整的request对象
response=response
response=response,
)
return await ErrorHandler.handle_error(http_error, request)
@@ -594,7 +644,9 @@ class UsageCalculator:
"""用量计算器 - 专门负责Token计数和费用计算"""
@staticmethod
def calculate_tokens(request: EndpointCheckRequest, result: EndpointCheckResult) -> tuple[int, int, int, int]:
def calculate_tokens(
request: EndpointCheckRequest, result: EndpointCheckResult
) -> tuple[int, int, int, int]:
"""
计算Token数量
@@ -612,7 +664,9 @@ class UsageCalculator:
return _extract_tokens_from_response(api_identifier, result.response_data)
@staticmethod
def _fallback_token_counting(request_data: dict[str, Any], response_data: dict[str, Any] | None) -> tuple[int, int, int, int]:
def _fallback_token_counting(
request_data: dict[str, Any], response_data: dict[str, Any] | None
) -> tuple[int, int, int, int]:
"""回退的Token计数方法简单估算"""
# 估算输入Token
messages = request_data.get("messages", request_data.get("contents", []))
@@ -658,6 +712,7 @@ class UsageCalculator:
return input_tokens, output_tokens, 0, 0
class AsyncBatchUsageRecorder:
"""异步用量记录器 - 批处理数据库操作"""
@@ -711,7 +766,9 @@ class AsyncBatchUsageRecorder:
# 目前保持简单的逐条插入,但减少了锁的竞争
for record in records_to_flush:
# 调用原有的用量记录逻辑(简化版)
logger.debug(f"[AsyncBatchUsageRecorder] Flushing usage record: {record.get('request_id', 'unknown')}")
logger.debug(
f"[AsyncBatchUsageRecorder] Flushing usage record: {record.get('request_id', 'unknown')}"
)
logger.info(f"[AsyncBatchUsageRecorder] Flushed {len(records_to_flush)} usage records")
except Exception as e:
@@ -741,6 +798,7 @@ class AsyncBatchUsageRecorder:
# 全局批处理器实例(单例)
_global_batch_recorder: AsyncBatchUsageRecorder | None = None
def get_batch_recorder() -> AsyncBatchUsageRecorder:
"""获取全局批处理器实例"""
global _global_batch_recorder
@@ -753,32 +811,48 @@ def get_batch_recorder() -> AsyncBatchUsageRecorder:
# 统一错误处理机制
# =========================================================================
class EndpointCheckError(Exception):
"""端点检查错误基类"""
def __init__(self, message: str, error_type: str, status_code: int = 500, details: dict[str, Any] | None = None):
def __init__(
self,
message: str,
error_type: str,
status_code: int = 500,
details: dict[str, Any] | None = None,
):
super().__init__(message)
self.message = message
self.error_type = error_type
self.status_code = status_code
self.details = details or {}
class NetworkError(EndpointCheckError):
"""网络请求错误"""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(message, "network_error", 0, details)
class AuthenticationError(EndpointCheckError):
"""认证错误"""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(message, "authentication_error", 401, details)
class RateLimitError(EndpointCheckError):
"""速率限制错误"""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(message, "rate_limit_error", 429, details)
class UpstreamError(EndpointCheckError):
"""上游服务错误"""
def __init__(self, message: str, status_code: int, details: dict[str, Any] | None = None):
super().__init__(message, "upstream_error", status_code, details)
@@ -803,7 +877,9 @@ class ErrorHandler:
return ErrorHandler._handle_unknown_error(error, request)
@staticmethod
def _handle_network_error(error: httpx.RequestError, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_network_error(
error: httpx.RequestError, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理网络错误"""
error_message = f"Network error: {str(error)}"
logger.warning(f"[{request.api_format}] Network error: {error}")
@@ -827,12 +903,14 @@ class ErrorHandler:
response_data={
"error_type": error_type,
"original_error": str(error),
"retryable": True
}
"retryable": True,
},
)
@staticmethod
def _handle_timeout_error(error: httpx.TimeoutException, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_timeout_error(
error: httpx.TimeoutException, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理超时错误"""
logger.warning(f"[{request.api_format}] Request timeout: {error}")
return EndpointCheckResult(
@@ -845,14 +923,18 @@ class ErrorHandler:
"error_type": "timeout",
"original_error": str(error),
"retryable": True,
"timeout_seconds": request.timeout
}
"timeout_seconds": request.timeout,
},
)
@staticmethod
def _handle_http_status_error(error: httpx.HTTPStatusError, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_http_status_error(
error: httpx.HTTPStatusError, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理HTTP状态错误"""
logger.warning(f"[{request.api_format}] HTTP error: {error.response.status_code} - {error.response.text[:200]}")
logger.warning(
f"[{request.api_format}] HTTP error: {error.response.status_code} - {error.response.text[:200]}"
)
# 根据状态码分类错误
status_code = error.response.status_code
@@ -887,14 +969,18 @@ class ErrorHandler:
"error_type": error_type,
"http_status": status_code,
"response_body": error.response.text[:500] if error.response.text else "",
"retryable": retryable
}
"retryable": retryable,
},
)
@staticmethod
def _handle_business_error(error: EndpointCheckError, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_business_error(
error: EndpointCheckError, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理业务逻辑错误"""
logger.warning(f"[{request.api_format}] Business error: {error.error_type} - {error.message}")
logger.warning(
f"[{request.api_format}] Business error: {error.error_type} - {error.message}"
)
return EndpointCheckResult(
status_code=error.status_code,
headers={},
@@ -904,12 +990,14 @@ class ErrorHandler:
response_data={
"error_type": error.error_type,
"details": error.details,
"retryable": error.status_code >= 500 or error.status_code == 429
}
"retryable": error.status_code >= 500 or error.status_code == 429,
},
)
@staticmethod
def _handle_validation_error(error: ValueError, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_validation_error(
error: ValueError, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理验证错误"""
logger.warning(f"[{request.api_format}] Validation error: {error}")
return EndpointCheckResult(
@@ -921,15 +1009,18 @@ class ErrorHandler:
response_data={
"error_type": "validation_error",
"original_error": str(error),
"retryable": False
}
"retryable": False,
},
)
@staticmethod
def _handle_unknown_error(error: Exception, request: EndpointCheckRequest) -> EndpointCheckResult:
def _handle_unknown_error(
error: Exception, request: EndpointCheckRequest
) -> EndpointCheckResult:
"""处理未知错误"""
logger.error(f"[{request.api_format}] Unknown error: {type(error).__name__}: {error}")
import traceback
logger.error(f"[{request.api_format}] Traceback: {traceback.format_exc()}")
return EndpointCheckResult(
@@ -941,8 +1032,8 @@ class ErrorHandler:
response_data={
"error_type": "internal_error",
"original_error": str(error),
"retryable": False
}
"retryable": False,
},
)
@@ -950,9 +1041,11 @@ class ErrorHandler:
# 配置化支持
# =========================================================================
@dataclass
class EndpointCheckConfig:
"""端点检查配置"""
# 性能配置
timeout: float = 30.0
max_retries: int = 3
@@ -986,20 +1079,31 @@ class EndpointCheckConfig:
import os
return cls(
timeout=float(os.getenv('ENDPOINT_CHECK_TIMEOUT', '30.0')),
max_retries=int(os.getenv('ENDPOINT_CHECK_MAX_RETRIES', '3')),
retry_delay=float(os.getenv('ENDPOINT_CHECK_RETRY_DELAY', '1.0')),
api_format_cache_size=int(os.getenv('ENDPOINT_CHECK_CACHE_SIZE', '512')),
enable_batch_recording=os.getenv('ENDPOINT_CHECK_BATCH_RECORDING', 'true').lower() == 'true',
batch_size=int(os.getenv('ENDPOINT_CHECK_BATCH_SIZE', '10')),
batch_flush_interval=float(os.getenv('ENDPOINT_CHECK_BATCH_INTERVAL', '2.0')),
enable_detailed_logging=os.getenv('ENDPOINT_CHECK_DETAILED_LOGGING', 'false').lower() == 'true',
enable_structured_logging=os.getenv('ENDPOINT_CHECK_STRUCTURED_LOGGING', 'true').lower() == 'true',
enable_usage_calculation=os.getenv('ENDPOINT_CHECK_USAGE_CALCULATION', 'true').lower() == 'true',
enable_fallback_token_counting=os.getenv('ENDPOINT_CHECK_FALLBACK_COUNTING', 'true').lower() == 'true',
enable_error_classification=os.getenv('ENDPOINT_CHECK_ERROR_CLASSIFICATION', 'true').lower() == 'true',
retry_on_server_errors=os.getenv('ENDPOINT_CHECK_RETRY_SERVER_ERRORS', 'true').lower() == 'true',
retry_on_timeouts=os.getenv('ENDPOINT_CHECK_RETRY_TIMEOUTS', 'true').lower() == 'true',
timeout=float(os.getenv("ENDPOINT_CHECK_TIMEOUT", "30.0")),
max_retries=int(os.getenv("ENDPOINT_CHECK_MAX_RETRIES", "3")),
retry_delay=float(os.getenv("ENDPOINT_CHECK_RETRY_DELAY", "1.0")),
api_format_cache_size=int(os.getenv("ENDPOINT_CHECK_CACHE_SIZE", "512")),
enable_batch_recording=os.getenv("ENDPOINT_CHECK_BATCH_RECORDING", "true").lower()
== "true",
batch_size=int(os.getenv("ENDPOINT_CHECK_BATCH_SIZE", "10")),
batch_flush_interval=float(os.getenv("ENDPOINT_CHECK_BATCH_INTERVAL", "2.0")),
enable_detailed_logging=os.getenv("ENDPOINT_CHECK_DETAILED_LOGGING", "false").lower()
== "true",
enable_structured_logging=os.getenv("ENDPOINT_CHECK_STRUCTURED_LOGGING", "true").lower()
== "true",
enable_usage_calculation=os.getenv("ENDPOINT_CHECK_USAGE_CALCULATION", "true").lower()
== "true",
enable_fallback_token_counting=os.getenv(
"ENDPOINT_CHECK_FALLBACK_COUNTING", "true"
).lower()
== "true",
enable_error_classification=os.getenv(
"ENDPOINT_CHECK_ERROR_CLASSIFICATION", "true"
).lower()
== "true",
retry_on_server_errors=os.getenv("ENDPOINT_CHECK_RETRY_SERVER_ERRORS", "true").lower()
== "true",
retry_on_timeouts=os.getenv("ENDPOINT_CHECK_RETRY_TIMEOUTS", "true").lower() == "true",
)
@classmethod
@@ -1016,8 +1120,7 @@ class ConfigurableEndpointChecker:
self.executor = HttpRequestExecutor(timeout=self.config.timeout)
self.usage_calculator = UsageCalculator()
self.orchestrator = EndpointCheckOrchestrator(
executor=self.executor,
usage_calculator=self.usage_calculator
executor=self.executor, usage_calculator=self.usage_calculator
)
# 应用配置到缓存大小
@@ -1027,7 +1130,9 @@ class ConfigurableEndpointChecker:
"""应用缓存配置"""
# 简化缓存配置 - 移除了有问题的缓存实现
# 未来如果需要缓存,可以重新设计缓存策略
logger.info(f"[ConfigurableEndpointChecker] Cache config applied: api_format_cache_size={self.config.api_format_cache_size}")
logger.info(
f"[ConfigurableEndpointChecker] Cache config applied: api_format_cache_size={self.config.api_format_cache_size}"
)
pass
async def check_endpoint(self, request: EndpointCheckRequest) -> EndpointCheckResult:
@@ -1063,19 +1168,24 @@ class ConfigurableEndpointChecker:
# 根据配置和错误类型判断是否重试
if error_type == "timeout" and self.config.retry_on_timeouts:
return True
elif error_type in ["server_error", "network_error", "connection_failed"] and self.config.retry_on_server_errors:
elif (
error_type in ["server_error", "network_error", "connection_failed"]
and self.config.retry_on_server_errors
):
return retryable
return False
async def _retry_check(self, request: EndpointCheckRequest, last_result: EndpointCheckResult) -> EndpointCheckResult:
async def _retry_check(
self, request: EndpointCheckRequest, last_result: EndpointCheckResult
) -> EndpointCheckResult:
"""重试端点检查"""
for attempt in range(self.config.max_retries):
if self.config.enable_structured_logging:
self._log_structured_retry(request, attempt + 1, last_result)
# 等待重试延迟
await asyncio.sleep(self.config.retry_delay * (2 ** attempt)) # 指数退避
await asyncio.sleep(self.config.retry_delay * (2**attempt)) # 指数退避
# 执行重试
result = await self.orchestrator.execute_check(request)
@@ -1105,11 +1215,13 @@ class ConfigurableEndpointChecker:
"max_retries": self.config.max_retries,
"enable_batch_recording": self.config.enable_batch_recording,
"enable_usage_calculation": self.config.enable_usage_calculation,
}
},
}
logger.info(f"[{request.api_format}] {json.dumps(log_entry)}")
def _log_structured_result(self, request: EndpointCheckRequest, result: EndpointCheckResult) -> None:
def _log_structured_result(
self, request: EndpointCheckRequest, result: EndpointCheckResult
) -> None:
"""记录结构化结果日志"""
log_entry = {
"event": "endpoint_check_complete",
@@ -1129,7 +1241,9 @@ class ConfigurableEndpointChecker:
logger.info(f"[{request.api_format}] {json.dumps(log_entry)}")
def _log_structured_retry(self, request: EndpointCheckRequest, attempt: int, last_result: EndpointCheckResult) -> None:
def _log_structured_retry(
self, request: EndpointCheckRequest, attempt: int, last_result: EndpointCheckResult
) -> None:
"""记录重试日志"""
log_entry = {
"event": "endpoint_check_retry",
@@ -1172,28 +1286,36 @@ class ConfigurableEndpointChecker:
# 全局配置检查器实例
_global_configured_checker: ConfigurableEndpointChecker | None = None
def get_configured_checker(config: EndpointCheckConfig | None = None) -> ConfigurableEndpointChecker:
def get_configured_checker(
config: EndpointCheckConfig | None = None,
) -> ConfigurableEndpointChecker:
"""获取全局配置检查器实例"""
global _global_configured_checker
if _global_configured_checker is None or config is not None:
_global_configured_checker = ConfigurableEndpointChecker(config or EndpointCheckConfig.from_env())
_global_configured_checker = ConfigurableEndpointChecker(
config or EndpointCheckConfig.from_env()
)
return _global_configured_checker
class EndpointCheckOrchestrator:
"""端点检查协调器 - 协调整个流程"""
def __init__(self, executor: HttpRequestExecutor | None = None,
usage_calculator: UsageCalculator | None = None):
def __init__(
self,
executor: HttpRequestExecutor | None = None,
usage_calculator: UsageCalculator | None = None,
):
self.executor = executor or HttpRequestExecutor()
self.usage_calculator = usage_calculator or UsageCalculator()
async def execute_check(self, request: EndpointCheckRequest) -> EndpointCheckResult:
"""执行端点检查的完整流程"""
logger.info(f"[{request.api_format}] Starting endpoint check | "
f"provider={request.provider_name}, model={request.model_name}")
logger.info(
f"[{request.api_format}] Starting endpoint check | "
f"provider={request.provider_name}, model={request.model_name}"
)
# 1. 执行HTTP请求
result = await self.executor.execute(request)
@@ -1201,8 +1323,12 @@ class EndpointCheckOrchestrator:
# 2. 计算用量
if request.db and request.user: # 只在有数据库连接和用户信息时才计算用量
try:
input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens = \
self.usage_calculator.calculate_tokens(request, result)
(
input_tokens,
output_tokens,
cache_creation_input_tokens,
cache_read_input_tokens,
) = self.usage_calculator.calculate_tokens(request, result)
# 检测API格式
api_format = request.api_format
@@ -1229,10 +1355,15 @@ class EndpointCheckOrchestrator:
api_format=api_format,
)
logger.info(f"[{request.api_format}] Usage calculated successfully: {result.usage_data}")
logger.info(
f"[{request.api_format}] Usage calculated successfully: {result.usage_data}"
)
except Exception as e:
logger.error(f"[{request.api_format}] Failed to calculate usage: {e}")
import traceback
logger.error(f"[{request.api_format}] Usage calculation traceback: {traceback.format_exc()}")
logger.error(
f"[{request.api_format}] Usage calculation traceback: {traceback.format_exc()}"
)
return result

View File

@@ -134,8 +134,8 @@ class OpenAIResponseParser(ResponseParser):
from src.api.handlers.openai.stream_parser import OpenAIStreamParser
self._parser = OpenAIStreamParser()
self.name = "OPENAI"
self.api_format = "OPENAI"
self.name = "openai:chat"
self.api_format = "openai:chat"
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
if not line or not line.strip():
@@ -245,8 +245,8 @@ class OpenAICliResponseParser(OpenAIResponseParser):
def __init__(self) -> None:
super().__init__()
self.name = "OPENAI_CLI"
self.api_format = "OPENAI_CLI"
self.name = "openai:cli"
self.api_format = "openai:cli"
class ClaudeResponseParser(ResponseParser):
@@ -256,8 +256,8 @@ class ClaudeResponseParser(ResponseParser):
from src.api.handlers.claude.stream_parser import ClaudeStreamParser
self._parser = ClaudeStreamParser()
self.name = "CLAUDE"
self.api_format = "CLAUDE"
self.name = "claude:chat"
self.api_format = "claude:chat"
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
if not line or not line.strip():
@@ -392,8 +392,8 @@ class ClaudeCliResponseParser(ClaudeResponseParser):
def __init__(self) -> None:
super().__init__()
self.name = "CLAUDE_CLI"
self.api_format = "CLAUDE_CLI"
self.name = "claude:cli"
self.api_format = "claude:cli"
class GeminiResponseParser(ResponseParser):
@@ -403,8 +403,8 @@ class GeminiResponseParser(ResponseParser):
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
self._parser = GeminiStreamParser()
self.name = "GEMINI"
self.api_format = "GEMINI"
self.name = "gemini:chat"
self.api_format = "gemini:chat"
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
"""
@@ -557,18 +557,18 @@ class GeminiCliResponseParser(GeminiResponseParser):
def __init__(self) -> None:
super().__init__()
self.name = "GEMINI_CLI"
self.api_format = "GEMINI_CLI"
self.name = "gemini:cli"
self.api_format = "gemini:cli"
# 解析器注册表
_PARSERS: dict[str, type[ResponseParser]] = {
"CLAUDE": ClaudeResponseParser,
"CLAUDE_CLI": ClaudeCliResponseParser,
"OPENAI": OpenAIResponseParser,
"OPENAI_CLI": OpenAICliResponseParser,
"GEMINI": GeminiResponseParser,
"GEMINI_CLI": GeminiCliResponseParser,
"claude:chat": ClaudeResponseParser,
"claude:cli": ClaudeCliResponseParser,
"openai:chat": OpenAIResponseParser,
"openai:cli": OpenAICliResponseParser,
"gemini:chat": GeminiResponseParser,
"gemini:cli": GeminiCliResponseParser,
}
@@ -577,7 +577,7 @@ def get_parser_for_format(format_id: str) -> ResponseParser:
根据格式 ID 获取 ResponseParser
Args:
format_id: 格式 ID"CLAUDE", "OPENAI", "CLAUDE_CLI", "OPENAI_CLI"
format_id: endpoint signature"claude:chat", "openai:cli"
Returns:
ResponseParser 实例
@@ -585,10 +585,12 @@ def get_parser_for_format(format_id: str) -> ResponseParser:
Raises:
KeyError: 格式不存在
"""
format_id = format_id.upper()
if format_id not in _PARSERS:
raise KeyError(f"Unknown format: {format_id}")
return _PARSERS[format_id]()
from src.core.api_format.signature import normalize_signature_key
normalized = normalize_signature_key(format_id)
if normalized not in _PARSERS:
raise KeyError(f"Unknown format: {normalized}")
return _PARSERS[normalized]()
__all__ = [

View File

@@ -18,7 +18,12 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from src.core.api_format import UPSTREAM_DROP_HEADERS, HeaderBuilder
from src.core.api_format import (
UPSTREAM_DROP_HEADERS,
HeaderBuilder,
get_auth_config_for_endpoint,
make_signature_key,
)
from src.core.crypto import crypto_service
if TYPE_CHECKING:
@@ -93,7 +98,7 @@ def build_test_request_body(
使用格式转换注册表将 OpenAI 格式的测试请求转换为目标格式。
Args:
format_id: 目标 API 格式 ID"CLAUDE", "GEMINI", "OPENAI_CLI"
format_id: 目标 endpoint signature"claude:chat", "gemini:chat", "openai:cli"
request_data: 可选的请求数据,会与默认测试请求合并
Returns:
@@ -110,11 +115,15 @@ def build_test_request_body(
# 获取测试请求数据OpenAI 格式)
source_data = get_test_request_data(request_data)
# CLI 格式使用基础格式进行转换(CLAUDE_CLI -> CLAUDE
# CLI 格式使用基础格式进行转换(claude:cli -> claude:chat
target_format = get_base_format(format_id) or format_id
# 使用注册表进行格式转换 (OPENAI -> 目标基础格式)
return format_conversion_registry.convert_request(source_data, "OPENAI", target_format)
# 使用注册表进行格式转换 (openai:chat -> 目标基础格式)
return format_conversion_registry.convert_request(
source_data,
make_signature_key("openai", "chat"),
target_format,
)
# ==============================================================================
@@ -237,8 +246,6 @@ class PassthroughRequestBuilder(RequestBuilder):
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value)
用于 Service Account 等异步获取 token 的场景
"""
from src.core.api_format import get_auth_config, resolve_api_format
# 1. 根据 API 格式自动设置认证头
if pre_computed_auth:
# 使用预先计算的认证信息Service Account 等场景)
@@ -246,11 +253,23 @@ class PassthroughRequestBuilder(RequestBuilder):
else:
# 标准 API Key 认证
decrypted_key = crypto_service.decrypt(key.api_key)
api_format = getattr(endpoint, "api_format", None)
resolved_format = resolve_api_format(api_format)
auth_header, auth_type = (
get_auth_config(resolved_format) if resolved_format else ("Authorization", "bearer")
)
raw_family = getattr(endpoint, "api_family", None)
raw_kind = getattr(endpoint, "endpoint_kind", None)
endpoint_sig: str | None = None
if (
isinstance(raw_family, str)
and isinstance(raw_kind, str)
and raw_family
and raw_kind
):
endpoint_sig = make_signature_key(raw_family, raw_kind)
else:
# 兜底:允许 endpoint.api_format 已经是 signature key 的情况
raw_format = getattr(endpoint, "api_format", None)
if isinstance(raw_format, str) and ":" in raw_format:
endpoint_sig = raw_format
auth_header, auth_type = get_auth_config_for_endpoint(endpoint_sig or "openai:chat")
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
# 认证头始终受保护,防止 header_rules 覆盖
protected_keys = {auth_header.lower(), "content-type"}

View File

@@ -260,8 +260,7 @@ class StreamContext:
# 第一行:基本信息 + 首字时间
line1 = (
f"[{status}] {request_id[:8]} | {self.model} | "
f"{self.provider_name or 'unknown'}"
f"[{status}] {request_id[:8]} | {self.model} | " f"{self.provider_name or 'unknown'}"
)
if self.first_byte_time_ms is not None:
line1 += f" | TTFB: {self.first_byte_time_ms}ms"

View File

@@ -14,12 +14,10 @@ from __future__ import annotations
import asyncio
import codecs
import json
from collections.abc import AsyncGenerator, Callable
from dataclasses import dataclass
from typing import Any
from collections.abc import Callable
from collections.abc import AsyncGenerator
import httpx
from src.api.handlers.base.content_extractors import (
@@ -310,8 +308,9 @@ class StreamProcessor:
# 预读阶段格式转换试验:首字节前可 failover
# 如果需要跨格式转换,对首个有效数据块做试转换
if ctx.needs_conversion and isinstance(data, dict):
client_format = (ctx.client_api_format or "").upper()
provider_format = (ctx.provider_api_format or "").upper()
# 新模式endpoint signature keyfamily:kind这里仅用于转换器选择不做 legacy 兼容
client_format = (ctx.client_api_format or "").strip().lower()
provider_format = (ctx.provider_api_format or "").strip().lower()
if client_format and provider_format:
try:
# 试转换:传 state=None不保留状态
@@ -414,14 +413,15 @@ class StreamProcessor:
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
# ctx.api_format 可能是 APIFormat 枚举,需要取 value
_api_format_str = (
ctx.api_format.value
if hasattr(ctx.api_format, "value")
else str(ctx.api_format or "")
_api_format_str = str(ctx.api_format or "")
client_format = (ctx.client_api_format or _api_format_str).strip().lower()
provider_format = (ctx.provider_api_format or _api_format_str).strip().lower()
client_family = (
client_format.split(":", 1)[0] if ":" in client_format else client_format
)
provider_family = (
provider_format.split(":", 1)[0] if ":" in provider_format else provider_format
)
client_format = (ctx.client_api_format or _api_format_str).upper()
provider_format = (ctx.provider_api_format or _api_format_str).upper()
# 使用 handler 层预计算的 needs_conversion由 candidate 决定)
needs_conversion = ctx.needs_conversion
@@ -446,7 +446,7 @@ class StreamProcessor:
streaming_started = True
def _build_stream_error_payload(message: str) -> dict:
if client_format.startswith("OPENAI"):
if client_family == "openai":
return {
"error": {
"message": message,
@@ -502,7 +502,7 @@ class StreamProcessor:
and normalized_line[5:].strip() == "[DONE]"
):
skip_next_blank_line = True
if client_format.startswith("OPENAI"):
if client_family == "openai":
openai_done_sent = True
return [b"data: [DONE]\n\n"]
return []
@@ -510,7 +510,7 @@ class StreamProcessor:
# 默认只处理 SSE 的 data 行;但 Gemini 上游可能返回 JSON-array/chunks无 data 前缀)
is_data_line = normalized_line.startswith("data:")
if not is_data_line:
if provider_format != "GEMINI":
if provider_family != "gemini":
return []
data_content = normalized_line.strip()
else:
@@ -554,9 +554,7 @@ class StreamProcessor:
error_bytes = (
f"data: {json.dumps(payload, ensure_ascii=False)}\n\n".encode()
)
done_bytes = (
b"data: [DONE]\n\n" if client_format.startswith("OPENAI") else b""
)
done_bytes = b"data: [DONE]\n\n" if client_family == "openai" else b""
if done_bytes:
openai_done_sent = True
return [error_bytes, done_bytes]
@@ -572,9 +570,7 @@ class StreamProcessor:
# 统一使用 SSE 格式输出Gemini streamGenerateContent 也使用 SSE
# 参考: https://ai.google.dev/api/generate-content
out.append(
f"data: {json.dumps(evt, ensure_ascii=False)}\n\n".encode()
)
out.append(f"data: {json.dumps(evt, ensure_ascii=False)}\n\n".encode())
return out
# 统一处理 prefetched + iterator
@@ -669,7 +665,7 @@ class StreamProcessor:
return
# Provider 流结束后,为 OpenAI 客户端补齐 [DONE](许多上游不发送该哨兵)
if client_format.startswith("OPENAI") and not openai_done_sent:
if client_family == "openai" and not openai_done_sent:
_mark_stream_started()
yield b"data: [DONE]\n\n"
@@ -944,9 +940,7 @@ class StreamProcessor:
self._extractors[format_name] = extractor
return self._extractors.get(format_name)
def _detect_format_and_extract(
self, data: dict
) -> tuple[str | None, ContentExtractor | None]:
def _detect_format_and_extract(self, data: dict) -> tuple[str | None, ContentExtractor | None]:
"""
检测数据格式并提取内容
@@ -1042,9 +1036,7 @@ class _LightweightSmoother:
self._extractors[format_name] = extractor
return self._extractors.get(format_name)
def _detect_format_and_extract(
self, data: dict
) -> tuple[str | None, ContentExtractor | None]:
def _detect_format_and_extract(self, data: dict) -> tuple[str | None, ContentExtractor | None]:
for format_name in get_extractor_formats():
extractor = self._get_extractor(format_name)
if extractor:
@@ -1062,9 +1054,7 @@ class _LightweightSmoother:
return [content]
return [content[i : i + self.chunk_size] for i in range(0, text_length, self.chunk_size)]
async def smooth(
self, stream_generator: AsyncGenerator[bytes]
) -> AsyncGenerator[bytes]:
async def smooth(self, stream_generator: AsyncGenerator[bytes]) -> AsyncGenerator[bytes]:
buffer = b""
is_first_content = True

View File

@@ -20,7 +20,11 @@ from src.config.settings import config
from src.core.logger import logger
from src.database import get_db
from src.models.database import ApiKey, User
from src.services.usage.telemetry_writer import DbTelemetryWriter, QueueTelemetryWriter, TelemetryWriter
from src.services.usage.telemetry_writer import (
DbTelemetryWriter,
QueueTelemetryWriter,
TelemetryWriter,
)
class StreamTelemetryRecorder:
@@ -96,13 +100,20 @@ class StreamTelemetryRecorder:
return
actual_request_body = ctx.provider_request_body or original_request_body
response_body = None
if not isinstance(writer, QueueTelemetryWriter) or config.usage_queue_include_bodies:
if (
not isinstance(writer, QueueTelemetryWriter)
or config.usage_queue_include_bodies
):
response_body = ctx.build_response_body(response_time_ms)
try:
await self._dispatch_record(
writer, ctx, original_headers, actual_request_body,
response_body, response_time_ms,
writer,
ctx,
original_headers,
actual_request_body,
response_body,
response_time_ms,
)
except Exception as writer_error:
if not isinstance(writer, QueueTelemetryWriter):
@@ -122,8 +133,12 @@ class StreamTelemetryRecorder:
if response_body is None:
response_body = ctx.build_response_body(response_time_ms)
await self._dispatch_record(
db_writer, ctx, original_headers, actual_request_body,
response_body, response_time_ms,
db_writer,
ctx,
original_headers,
actual_request_body,
response_body,
response_time_ms,
)
# 更新候选记录状态
@@ -152,11 +167,13 @@ class StreamTelemetryRecorder:
"""记录成功的请求"""
# 流式成功时,返回给客户端的是提供商响应头 + SSE 必需头
client_response_headers = filter_proxy_response_headers(ctx.response_headers)
client_response_headers.update({
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"content-type": "text/event-stream",
})
client_response_headers.update(
{
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"content-type": "text/event-stream",
}
)
await writer.record_success(
provider=ctx.provider_name or "unknown",
@@ -200,7 +217,9 @@ class StreamTelemetryRecorder:
) -> None:
"""记录失败的请求"""
# 失败时返回给客户端的是 JSON 错误响应,如果没有设置则使用默认值
client_response_headers = ctx.client_response_headers or {"content-type": "application/json"}
client_response_headers = ctx.client_response_headers or {
"content-type": "application/json"
}
await writer.record_failure(
provider=ctx.provider_name or "unknown",
@@ -242,7 +261,9 @@ class StreamTelemetryRecorder:
response_time_ms: int,
) -> None:
"""记录客户端取消的请求"""
client_response_headers = ctx.client_response_headers or {"content-type": "application/json"}
client_response_headers = ctx.client_response_headers or {
"content-type": "application/json"
}
await writer.record_cancelled(
provider=ctx.provider_name or "unknown",
@@ -319,7 +340,9 @@ class StreamTelemetryRecorder:
)
else:
# 请求链路追踪使用 upstream_response原始响应回退到 error_message友好消息
trace_error_message = ctx.upstream_response or ctx.error_message or f"HTTP {ctx.status_code}"
trace_error_message = (
ctx.upstream_response or ctx.error_message or f"HTTP {ctx.status_code}"
)
RequestCandidateService.mark_candidate_failed(
db=db,
candidate_id=ctx.attempt_id,
@@ -408,18 +431,30 @@ class StreamTelemetryRecorder:
"""根据上下文状态分发到对应的记录方法"""
if ctx.is_success():
await self._record_success(
writer, ctx, original_headers, actual_request_body,
response_body, response_time_ms,
writer,
ctx,
original_headers,
actual_request_body,
response_body,
response_time_ms,
)
elif ctx.is_client_disconnected():
await self._record_cancelled(
writer, ctx, original_headers, actual_request_body,
response_body, response_time_ms,
writer,
ctx,
original_headers,
actual_request_body,
response_body,
response_time_ms,
)
else:
await self._record_failure(
writer, ctx, original_headers, actual_request_body,
response_body, response_time_ms,
writer,
ctx,
original_headers,
actual_request_body,
response_body,
response_time_ms,
)
def _get_status_from_ctx(self, ctx: StreamContext) -> str:
@@ -440,7 +475,5 @@ class StreamTelemetryRecorder:
)
return None
bg_telemetry = MessageTelemetry(
bg_db, user, api_key_obj, self.request_id, self.client_ip
)
bg_telemetry = MessageTelemetry(bg_db, user, api_key_obj, self.request_id, self.client_ip)
return DbTelemetryWriter(bg_telemetry)

View File

@@ -4,12 +4,11 @@ Handler 基础工具函数
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
from src.core.exceptions import EmbeddedErrorException, ProviderNotAvailableException
from src.core.api_format import filter_response_headers
from src.core.exceptions import EmbeddedErrorException, ProviderNotAvailableException
from src.core.logger import logger
if TYPE_CHECKING:
@@ -72,15 +71,12 @@ def extract_cache_creation_tokens(usage: dict[str, Any]) -> int:
cache_1h = int(cache_creation.get("ephemeral_1h_input_tokens", 0))
total = cache_5m + cache_1h
logger.debug(
f"Using nested cache_creation: 5m={cache_5m}, 1h={cache_1h}, total={total}"
)
logger.debug(f"Using nested cache_creation: 5m={cache_5m}, 1h={cache_1h}, total={total}")
return total
# 2. 检查扁平新格式
has_flat_format = (
"claude_cache_creation_5_m_tokens" in usage
or "claude_cache_creation_1_h_tokens" in usage
"claude_cache_creation_5_m_tokens" in usage or "claude_cache_creation_1_h_tokens" in usage
)
if has_flat_format:
@@ -88,9 +84,7 @@ def extract_cache_creation_tokens(usage: dict[str, Any]) -> int:
cache_1h = int(usage.get("claude_cache_creation_1_h_tokens", 0))
total = cache_5m + cache_1h
logger.debug(
f"Using flat new format: 5m={cache_5m}, 1h={cache_1h}, total={total}"
)
logger.debug(f"Using flat new format: 5m={cache_5m}, 1h={cache_1h}, total={total}")
return total
# 3. 回退到旧格式

View File

@@ -16,7 +16,7 @@ from src.api.base.adapter import ApiAdapter, ApiMode
from src.api.base.context import ApiRequestContext
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import get_header_value
from src.core.api_format import ApiFamily, get_header_value
from src.core.logger import logger
from src.core.optimization_utils import TokenCounter
from src.models.claude import ClaudeMessagesRequest, ClaudeTokenCountRequest
@@ -58,7 +58,8 @@ class ClaudeChatAdapter(ChatAdapterBase):
处理 Claude Chat 格式的请求(/v1/messages 端点,进行格式验证)。
"""
FORMAT_ID = "CLAUDE"
FORMAT_ID = "claude:chat"
API_FAMILY = ApiFamily.CLAUDE
BILLING_TEMPLATE = "claude" # 使用 Claude 计费模板
name = "claude.chat"
@@ -70,7 +71,7 @@ class ClaudeChatAdapter(ChatAdapterBase):
return ClaudeChatHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["CLAUDE"])
super().__init__(allowed_api_formats)
logger.info(f"[{self.name}] 初始化Chat模式适配器 | API格式: {self.allowed_api_formats}")
def detect_capability_requirements(

View File

@@ -9,6 +9,7 @@ from typing import Any
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.api.handlers.base.utils import extract_cache_creation_tokens
from src.core.api_format import ApiFamily, EndpointKind
class ClaudeChatHandler(ChatHandlerBase):
@@ -21,7 +22,9 @@ class ClaudeChatHandler(ChatHandlerBase):
- 请求格式ClaudeMessagesRequest
"""
FORMAT_ID = "CLAUDE"
FORMAT_ID = "claude:chat"
API_FAMILY = ApiFamily.CLAUDE
ENDPOINT_KIND = EndpointKind.CHAT
def extract_model_from_request(
self,

View File

@@ -4,7 +4,6 @@ Claude SSE 流解析器
解析 Claude Messages API 的 Server-Sent Events 流。
"""
import json
from typing import Any

View File

@@ -14,6 +14,7 @@ from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
from src.api.handlers.claude.adapter import ClaudeCapabilityDetector, ClaudeChatAdapter
from src.config.settings import config
from src.core.api_format import ApiFamily
@register_cli_adapter
@@ -24,7 +25,8 @@ class ClaudeCliAdapter(CliAdapterBase):
处理 Claude CLI 格式的请求(/v1/messages 端点,使用 Bearer 认证)。
"""
FORMAT_ID = "CLAUDE_CLI"
FORMAT_ID = "claude:cli"
API_FAMILY = ApiFamily.CLAUDE
BILLING_TEMPLATE = "claude" # 使用 Claude 计费模板
name = "claude.cli"
@@ -36,7 +38,7 @@ class ClaudeCliAdapter(CliAdapterBase):
return ClaudeCliMessageHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["CLAUDE_CLI"])
super().__init__(allowed_api_formats)
def detect_capability_requirements(
self,
@@ -113,16 +115,16 @@ class ClaudeCliAdapter(CliAdapterBase):
cli_headers = {"User-Agent": config.internal_user_agent_claude_cli}
if extra_headers:
cli_headers.update(extra_headers)
models, error = await ClaudeChatAdapter.fetch_models(
client, base_url, api_key, cli_headers
)
models, error = await ClaudeChatAdapter.fetch_models(client, base_url, api_key, cli_headers)
# 更新 api_format 为 CLI 格式
for m in models:
m["api_format"] = cls.FORMAT_ID
return models, error
@classmethod
def build_endpoint_url(cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None) -> str:
def build_endpoint_url(
cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None
) -> str:
"""构建Claude CLI API端点URL"""
base_url = base_url.rstrip("/")
if base_url.endswith("/v1"):

View File

@@ -11,6 +11,7 @@ from src.api.handlers.base.cli_handler_base import (
StreamContext,
)
from src.api.handlers.base.utils import extract_cache_creation_tokens
from src.core.api_format import ApiFamily, EndpointKind
class ClaudeCliMessageHandler(CliMessageHandlerBase):
@@ -29,7 +30,9 @@ class ClaudeCliMessageHandler(CliMessageHandlerBase):
模型字段:请求体顶级 model 字段
"""
FORMAT_ID = "CLAUDE_CLI"
FORMAT_ID = "claude:cli"
API_FAMILY = ApiFamily.CLAUDE
ENDPOINT_KIND = EndpointKind.CLI
def extract_model_from_request(
self,
@@ -197,4 +200,3 @@ class ClaudeCliMessageHandler(CliMessageHandlerBase):
# 记录模型名称
if ctx.model:
ctx.response_metadata["model"] = ctx.model

View File

@@ -14,7 +14,7 @@ from fastapi.responses import JSONResponse
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import get_auth_handler
from src.core.api_format import ApiFamily, get_auth_handler
from src.core.api_format.enums import AuthMethod
from src.core.logger import logger
from src.models.gemini import GeminiRequest
@@ -31,7 +31,8 @@ class GeminiChatAdapter(ChatAdapterBase):
端点: /v1beta/models/{model}:generateContent
"""
FORMAT_ID = "GEMINI"
FORMAT_ID = "gemini:chat"
API_FAMILY = ApiFamily.GEMINI
BILLING_TEMPLATE = "gemini" # 使用 Gemini 计费模板
name = "gemini.chat"
@@ -43,7 +44,7 @@ class GeminiChatAdapter(ChatAdapterBase):
return GeminiChatHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["GEMINI"])
super().__init__(allowed_api_formats)
logger.info(
f"[{self.name}] 初始化 Gemini Chat 适配器 | API格式: {self.allowed_api_formats}"
)

View File

@@ -6,10 +6,12 @@ Gemini Chat Handler
from __future__ import annotations
from starlette.requests import Request
from typing import Any
from starlette.requests import Request
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import ApiFamily, EndpointKind
class GeminiChatHandler(ChatHandlerBase):
@@ -23,7 +25,9 @@ class GeminiChatHandler(ChatHandlerBase):
- 响应格式: JSON 数组流(非 SSE
"""
FORMAT_ID = "GEMINI"
FORMAT_ID = "gemini:chat"
API_FAMILY = ApiFamily.GEMINI
ENDPOINT_KIND = EndpointKind.CHAT
async def _resolve_preferred_key_ids(
self,

View File

@@ -15,7 +15,7 @@ from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
from src.api.handlers.gemini.adapter import GeminiChatAdapter
from src.config.settings import config
from src.core.api_format import get_auth_handler
from src.core.api_format import ApiFamily, get_auth_handler
from src.core.api_format.enums import AuthMethod
@@ -27,7 +27,8 @@ class GeminiCliAdapter(CliAdapterBase):
处理 Gemini CLI 格式的请求(透传模式,最小验证)。
"""
FORMAT_ID = "GEMINI_CLI"
FORMAT_ID = "gemini:cli"
API_FAMILY = ApiFamily.GEMINI
BILLING_TEMPLATE = "gemini" # 使用 Gemini 计费模板
name = "gemini.cli"
@@ -39,7 +40,7 @@ class GeminiCliAdapter(CliAdapterBase):
return GeminiCliMessageHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["GEMINI_CLI"])
super().__init__(allowed_api_formats)
def extract_api_key(self, request: Request) -> str | None:
"""

View File

@@ -10,6 +10,7 @@ from src.api.handlers.base.cli_handler_base import (
CliMessageHandlerBase,
StreamContext,
)
from src.core.api_format import ApiFamily, EndpointKind
class GeminiCliMessageHandler(CliMessageHandlerBase):
@@ -30,7 +31,9 @@ class GeminiCliMessageHandler(CliMessageHandlerBase):
- 请求体中的 model 字段用于内部路由,不发送给 API
"""
FORMAT_ID = "GEMINI_CLI"
FORMAT_ID = "gemini:cli"
API_FAMILY = ApiFamily.GEMINI
ENDPOINT_KIND = EndpointKind.CLI
def extract_model_from_request(
self,

View File

@@ -13,6 +13,7 @@ from fastapi.responses import JSONResponse
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import ApiFamily
from src.core.logger import logger
from src.models.openai import OpenAIRequest
@@ -25,7 +26,8 @@ class OpenAIChatAdapter(ChatAdapterBase):
处理 OpenAI Chat 格式的请求(/v1/chat/completions 端点)。
"""
FORMAT_ID = "OPENAI"
FORMAT_ID = "openai:chat"
API_FAMILY = ApiFamily.OPENAI
BILLING_TEMPLATE = "openai" # 使用 OpenAI 计费模板
name = "openai.chat"
@@ -37,9 +39,11 @@ class OpenAIChatAdapter(ChatAdapterBase):
return OpenAIChatHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["OPENAI"])
super().__init__(allowed_api_formats)
def _validate_request_body(self, original_request_body: dict, path_params: dict | None = None) -> None:
def _validate_request_body(
self, original_request_body: dict, path_params: dict | None = None
) -> None:
"""验证请求体"""
if not isinstance(original_request_body, dict):
return self._error_response(

View File

@@ -7,10 +7,12 @@ OpenAI Chat Handler - 基于通用 Chat Handler 基类的简化实现
from __future__ import annotations
from starlette.requests import Request
from typing import Any
from starlette.requests import Request
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import ApiFamily, EndpointKind
class OpenAIChatHandler(ChatHandlerBase):
@@ -23,7 +25,9 @@ class OpenAIChatHandler(ChatHandlerBase):
- 请求格式OpenAIRequest
"""
FORMAT_ID = "OPENAI"
FORMAT_ID = "openai:chat"
API_FAMILY = ApiFamily.OPENAI
ENDPOINT_KIND = EndpointKind.CHAT
def extract_model_from_request(
self,

View File

@@ -4,7 +4,6 @@ OpenAI SSE 流解析器
解析 OpenAI Chat Completions API 的 Server-Sent Events 流。
"""
import json
from typing import Any

View File

@@ -14,6 +14,7 @@ from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
from src.api.handlers.openai.adapter import OpenAIChatAdapter
from src.config.settings import config
from src.core.api_format import ApiFamily
@register_cli_adapter
@@ -24,7 +25,8 @@ class OpenAICliAdapter(CliAdapterBase):
处理 /v1/responses 端点的请求。
"""
FORMAT_ID = "OPENAI_CLI"
FORMAT_ID = "openai:cli"
API_FAMILY = ApiFamily.OPENAI
BILLING_TEMPLATE = "openai" # 使用 OpenAI 计费模板
name = "openai.cli"
@@ -36,7 +38,7 @@ class OpenAICliAdapter(CliAdapterBase):
return OpenAICliMessageHandler
def __init__(self, allowed_api_formats: list[str] | None = None):
super().__init__(allowed_api_formats or ["OPENAI_CLI"])
super().__init__(allowed_api_formats)
# =========================================================================
# 模型列表查询
@@ -55,16 +57,16 @@ class OpenAICliAdapter(CliAdapterBase):
cli_headers = {"User-Agent": config.internal_user_agent_openai_cli}
if extra_headers:
cli_headers.update(extra_headers)
models, error = await OpenAIChatAdapter.fetch_models(
client, base_url, api_key, cli_headers
)
models, error = await OpenAIChatAdapter.fetch_models(client, base_url, api_key, cli_headers)
# 更新 api_format 为 CLI 格式
for m in models:
m["api_format"] = cls.FORMAT_ID
return models, error
@classmethod
def build_endpoint_url(cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None) -> str:
def build_endpoint_url(
cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None
) -> str:
"""构建OpenAI CLI API端点URL"""
base_url = base_url.rstrip("/")
if base_url.endswith("/v1"):

View File

@@ -11,6 +11,7 @@ from src.api.handlers.base.cli_handler_base import (
CliMessageHandlerBase,
StreamContext,
)
from src.core.api_format import ApiFamily, EndpointKind
class OpenAICliMessageHandler(CliMessageHandlerBase):
@@ -28,7 +29,9 @@ class OpenAICliMessageHandler(CliMessageHandlerBase):
模型字段:请求体顶级 model 字段
"""
FORMAT_ID = "OPENAI_CLI"
FORMAT_ID = "openai:cli"
API_FAMILY = ApiFamily.OPENAI
ENDPOINT_KIND = EndpointKind.CLI
def extract_model_from_request(
self,
@@ -203,9 +206,10 @@ class OpenAICliMessageHandler(CliMessageHandlerBase):
if "object" in ctx.final_response:
ctx.response_metadata["object"] = ctx.final_response["object"]
if "system_fingerprint" in ctx.final_response:
ctx.response_metadata["system_fingerprint"] = ctx.final_response["system_fingerprint"]
ctx.response_metadata["system_fingerprint"] = ctx.final_response[
"system_fingerprint"
]
# 如果没有从响应中获取到 model使用上下文中的
if "model" not in ctx.response_metadata and ctx.model:
ctx.response_metadata["model"] = ctx.model