mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
334
_deprecated_py_src/api/handlers/claude/adapter.py
Normal file
334
_deprecated_py_src/api/handlers/claude/adapter.py
Normal file
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
Claude Chat Adapter - 基于 ChatAdapterBase 的 Claude Chat API 适配器
|
||||
|
||||
处理 /v1/messages 端点的 Claude Chat 格式请求。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
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 ApiFamily, get_header_value
|
||||
from src.core.logger import logger
|
||||
from src.models.claude import ClaudeMessagesRequest, ClaudeTokenCountRequest
|
||||
|
||||
|
||||
class ClaudeCapabilityDetector:
|
||||
"""Claude API 能力检测器"""
|
||||
|
||||
@staticmethod
|
||||
def detect_from_headers(
|
||||
headers: dict[str, str],
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""
|
||||
从 Claude 请求头和请求体检测能力需求
|
||||
|
||||
检测规则:
|
||||
- anthropic-beta: context-1m-xxx -> context_1m: True
|
||||
- 请求体中 cache_control.ttl = "1h" -> cache_1h: True
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
request_body: 请求体(用于检测 cache_control.ttl)
|
||||
"""
|
||||
requirements: dict[str, bool] = {}
|
||||
|
||||
# 使用统一的大小写不敏感获取
|
||||
beta_header = get_header_value(headers, "anthropic-beta")
|
||||
if beta_header and "context-1m" in beta_header.lower():
|
||||
requirements["context_1m"] = True
|
||||
|
||||
# 从请求体检测 cache_1h
|
||||
if request_body and _detect_cache_1h_in_body(request_body):
|
||||
requirements["cache_1h"] = True
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
def _has_cache_1h_ttl(block: dict[str, Any]) -> bool:
|
||||
"""检查单个内容块是否包含 cache_control.ttl = '1h'"""
|
||||
cache_control = block.get("cache_control")
|
||||
if isinstance(cache_control, dict):
|
||||
return cache_control.get("ttl") == "1h"
|
||||
return False
|
||||
|
||||
|
||||
def _detect_cache_1h_in_body(body: dict[str, Any]) -> bool:
|
||||
"""
|
||||
扫描 Claude 请求体,检测是否包含 cache_control.ttl = "1h"
|
||||
|
||||
检查位置:
|
||||
- system[].cache_control.ttl
|
||||
- messages[].content[].cache_control.ttl
|
||||
- tools[].cache_control.ttl
|
||||
"""
|
||||
# 检查 system(数组格式)
|
||||
system = body.get("system")
|
||||
if isinstance(system, list):
|
||||
for block in system:
|
||||
if isinstance(block, dict) and _has_cache_1h_ttl(block):
|
||||
return True
|
||||
|
||||
# 检查 messages
|
||||
messages = body.get("messages")
|
||||
if isinstance(messages, list):
|
||||
for msg in messages:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
content = msg.get("content")
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and _has_cache_1h_ttl(block):
|
||||
return True
|
||||
|
||||
# 检查 tools
|
||||
tools = body.get("tools")
|
||||
if isinstance(tools, list):
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict) and _has_cache_1h_ttl(tool):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
_TOKEN_COUNTER_PLUGIN: Any = None
|
||||
|
||||
|
||||
def _get_token_counter() -> Any:
|
||||
global _TOKEN_COUNTER_PLUGIN # noqa: PLW0603
|
||||
if _TOKEN_COUNTER_PLUGIN is None:
|
||||
from src.plugins.token.tiktoken_counter import TiktokenCounterPlugin
|
||||
|
||||
_TOKEN_COUNTER_PLUGIN = TiktokenCounterPlugin(name="tiktoken")
|
||||
return _TOKEN_COUNTER_PLUGIN
|
||||
|
||||
|
||||
async def _count_text_tokens_with_fallback(text: str, model: str) -> int:
|
||||
"""使用 tiktoken 插件计数,失败时回退到轻量估算。"""
|
||||
if not text:
|
||||
return 0
|
||||
try:
|
||||
plugin = _get_token_counter()
|
||||
if plugin.enabled:
|
||||
return await plugin.count_tokens(text, model)
|
||||
except Exception as exc:
|
||||
logger.debug("tiktoken token 计数失败,使用估算回退: {}", exc)
|
||||
# 与旧实现保持一致:按字符估算
|
||||
return max(1, len(text) // 4)
|
||||
|
||||
|
||||
async def _count_messages_tokens_with_fallback(messages: list[dict[str, Any]], model: str) -> int:
|
||||
"""按历史逻辑统计 messages token(每条消息固定开销 + 内容 token)。"""
|
||||
total = 0
|
||||
for message in messages:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
total += 4 # 角色与分隔符开销
|
||||
|
||||
content = message.get("content", "")
|
||||
if isinstance(content, str):
|
||||
total += await _count_text_tokens_with_fallback(content, model)
|
||||
elif isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
text = item.get("text")
|
||||
if isinstance(text, str):
|
||||
total += await _count_text_tokens_with_fallback(text, model)
|
||||
|
||||
return total
|
||||
|
||||
|
||||
@register_adapter
|
||||
class ClaudeChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
Claude Chat API 适配器
|
||||
|
||||
处理 Claude Chat 格式的请求(/v1/messages 端点,进行格式验证)。
|
||||
"""
|
||||
|
||||
FORMAT_ID = "claude:chat"
|
||||
API_FAMILY = ApiFamily.CLAUDE
|
||||
name = "claude.chat"
|
||||
|
||||
@property
|
||||
def HANDLER_CLASS(self) -> type[ChatHandlerBase]:
|
||||
"""延迟导入 Handler 类避免循环依赖"""
|
||||
from src.api.handlers.claude.handler import ClaudeChatHandler
|
||||
|
||||
return ClaudeChatHandler
|
||||
|
||||
def __init__(self, allowed_api_formats: list[str] | None = None):
|
||||
super().__init__(allowed_api_formats)
|
||||
logger.info(f"[{self.name}] 初始化Chat模式适配器 | API格式: {self.allowed_api_formats}")
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""检测 Claude 请求中隐含的能力需求"""
|
||||
return ClaudeCapabilityDetector.detect_from_headers(headers, request_body)
|
||||
|
||||
def _validate_request_body(
|
||||
self, original_request_body: dict, path_params: dict | None = None
|
||||
) -> None:
|
||||
"""验证请求体"""
|
||||
try:
|
||||
if not isinstance(original_request_body, dict):
|
||||
raise ValueError("Request body must be a JSON object")
|
||||
|
||||
required_fields = ["model", "messages", "max_tokens"]
|
||||
missing_fields = [f for f in required_fields if f not in original_request_body]
|
||||
if missing_fields:
|
||||
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")
|
||||
|
||||
request = ClaudeMessagesRequest.model_validate(
|
||||
original_request_body,
|
||||
strict=False,
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error(f"请求体基本验证失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.warning(f"Pydantic验证警告(将继续处理): {str(e)}")
|
||||
request = ClaudeMessagesRequest.model_construct(
|
||||
model=original_request_body.get("model"),
|
||||
max_tokens=original_request_body.get("max_tokens"),
|
||||
messages=original_request_body.get("messages", []),
|
||||
stream=original_request_body.get("stream", False),
|
||||
)
|
||||
return request
|
||||
|
||||
def _build_audit_metadata(self, _payload: dict[str, Any], request_obj: Any) -> dict[str, Any]:
|
||||
"""构建 Claude Chat 特定的审计元数据"""
|
||||
role_counts: dict[str, int] = {}
|
||||
for message in request_obj.messages:
|
||||
role_counts[message.role] = role_counts.get(message.role, 0) + 1
|
||||
|
||||
return {
|
||||
"action": "claude_messages",
|
||||
"model": request_obj.model,
|
||||
"stream": bool(request_obj.stream),
|
||||
"max_tokens": request_obj.max_tokens,
|
||||
"temperature": getattr(request_obj, "temperature", None),
|
||||
"top_p": getattr(request_obj, "top_p", None),
|
||||
"top_k": getattr(request_obj, "top_k", None),
|
||||
"messages_count": len(request_obj.messages),
|
||||
"message_roles": role_counts,
|
||||
"stop_sequences": len(request_obj.stop_sequences or []),
|
||||
"tools_count": len(request_obj.tools or []),
|
||||
"system_present": bool(request_obj.system),
|
||||
"metadata_present": bool(request_obj.metadata),
|
||||
"thinking_enabled": bool(request_obj.thinking),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def build_endpoint_url(
|
||||
cls,
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Claude API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
if base_url.endswith("/v1"):
|
||||
return f"{base_url}/messages"
|
||||
else:
|
||||
return f"{base_url}/v1/messages"
|
||||
|
||||
# build_request_body 使用基类实现,通过 format_conversion_registry 自动转换 OPENAI -> CLAUDE
|
||||
|
||||
|
||||
def build_claude_adapter(request: Request) -> Any:
|
||||
"""根据认证头构造 Chat 或 Claude Code 适配器。
|
||||
|
||||
- Authorization: Bearer (且无 x-api-key) -> CLI 模式
|
||||
- x-api-key -> Chat 模式
|
||||
"""
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
has_bearer = auth_header.lower().startswith("bearer ")
|
||||
has_api_key = bool(request.headers.get("x-api-key"))
|
||||
|
||||
if has_bearer and not has_api_key:
|
||||
from src.api.handlers.claude_cli.adapter import ClaudeCliAdapter
|
||||
|
||||
return ClaudeCliAdapter()
|
||||
return ClaudeChatAdapter()
|
||||
|
||||
|
||||
class ClaudeTokenCountAdapter(ApiAdapter):
|
||||
"""计算 Claude 请求 Token 数的轻量适配器。"""
|
||||
|
||||
name = "claude.token_count"
|
||||
mode = ApiMode.STANDARD
|
||||
eager_request_body = False
|
||||
|
||||
def extract_api_key(self, request: Request) -> str | None:
|
||||
"""从请求中提取 API 密钥 (x-api-key 或 Authorization: Bearer)"""
|
||||
from src.core.api_format import get_auth_handler
|
||||
from src.core.api_format.enums import AuthMethod
|
||||
|
||||
handler = get_auth_handler(AuthMethod.API_KEY)
|
||||
api_key = handler.extract_credentials(request)
|
||||
if api_key:
|
||||
return api_key
|
||||
|
||||
bearer_handler = get_auth_handler(AuthMethod.BEARER)
|
||||
return bearer_handler.extract_credentials(request)
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
payload = await context.ensure_json_body_async()
|
||||
|
||||
try:
|
||||
request = ClaudeTokenCountRequest.model_validate(payload, strict=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Token count payload invalid: {e}")
|
||||
raise HTTPException(status_code=400, detail="Invalid token count payload") from e
|
||||
|
||||
total_tokens = 0
|
||||
|
||||
if request.system:
|
||||
if isinstance(request.system, str):
|
||||
total_tokens += await _count_text_tokens_with_fallback(
|
||||
request.system, request.model
|
||||
)
|
||||
elif isinstance(request.system, list):
|
||||
for block in request.system:
|
||||
if hasattr(block, "text"):
|
||||
total_tokens += await _count_text_tokens_with_fallback(
|
||||
block.text, request.model
|
||||
)
|
||||
|
||||
messages_dict = [
|
||||
msg.model_dump() if hasattr(msg, "model_dump") else msg for msg in request.messages
|
||||
]
|
||||
total_tokens += await _count_messages_tokens_with_fallback(messages_dict, request.model)
|
||||
|
||||
context.add_audit_metadata(
|
||||
action="claude_token_count",
|
||||
model=request.model,
|
||||
messages_count=len(request.messages),
|
||||
system_present=bool(request.system),
|
||||
tools_count=len(request.tools or []),
|
||||
thinking_enabled=bool(request.thinking),
|
||||
input_tokens=total_tokens,
|
||||
)
|
||||
|
||||
return JSONResponse({"input_tokens": total_tokens})
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ClaudeChatAdapter",
|
||||
"ClaudeTokenCountAdapter",
|
||||
"build_claude_adapter",
|
||||
]
|
||||
Reference in New Issue
Block a user