mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
@@ -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 key(family: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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__ = [
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 key(family: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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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. 回退到旧格式
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ Claude SSE 流解析器
|
||||
解析 Claude Messages API 的 Server-Sent Events 流。
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ OpenAI SSE 流解析器
|
||||
解析 OpenAI Chat Completions API 的 Server-Sent Events 流。
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user