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:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,26 @@
"""Claude handler package (lazy exports)."""
from __future__ import annotations
from importlib import import_module
from typing import Any
_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
"ClaudeChatAdapter": (".adapter", "ClaudeChatAdapter"),
"ClaudeTokenCountAdapter": (".adapter", "ClaudeTokenCountAdapter"),
"build_claude_adapter": (".adapter", "build_claude_adapter"),
"ClaudeChatHandler": (".handler", "ClaudeChatHandler"),
}
__all__ = list(_LAZY_EXPORTS.keys())
def __getattr__(name: str) -> Any:
if name not in _LAZY_EXPORTS:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module_name, attr_name = _LAZY_EXPORTS[name]
module = import_module(module_name, __name__)
value = getattr(module, attr_name)
globals()[name] = value
return value

View 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",
]

View File

@@ -0,0 +1,128 @@
"""
Claude Chat Handler - 基于通用 Chat Handler 基类的简化实现
继承 ChatHandlerBase只需覆盖格式特定的方法。
代码量从原来的 ~1470 行减少到 ~120 行。
"""
from typing import Any
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import ApiFamily, EndpointKind
from src.core.usage_tokens import extract_cache_creation_tokens_detail
class ClaudeChatHandler(ChatHandlerBase):
"""
Claude Chat Handler - 处理 Claude Chat/CLI API 格式的请求
格式特点:
- 使用 input_tokens/output_tokens
- 支持 cache_creation_input_tokens/cache_read_input_tokens
- 请求格式ClaudeMessagesRequest
"""
FORMAT_ID = "claude:chat"
API_FAMILY = ApiFamily.CLAUDE
ENDPOINT_KIND = EndpointKind.CHAT
def extract_model_from_request(
self,
request_body: dict[str, Any],
path_params: dict[str, Any] | None = None, # noqa: ARG002
) -> str:
"""
从请求中提取模型名 - Claude 格式实现
Claude API 的 model 在请求体顶级字段。
Args:
request_body: 请求体
path_params: URL 路径参数Claude 不使用)
Returns:
模型名
"""
model = request_body.get("model")
return str(model) if model else "unknown"
def apply_mapped_model(
self,
request_body: dict[str, Any],
mapped_model: str,
) -> dict[str, Any]:
"""
将映射后的模型名应用到请求体
Claude API 的 model 在请求体顶级字段。
Args:
request_body: 原始请求体
mapped_model: 映射后的模型名
Returns:
更新了 model 字段的请求体
"""
result = dict(request_body)
result["model"] = mapped_model
return result
async def _convert_request(self, request: Any) -> Any:
"""
将请求转换为 Claude 格式的 Pydantic 对象
注意此方法只做类型转换dict → Pydantic不做跨格式转换。
跨格式转换由调度/执行层TaskService + RequestDispatcher在选中候选后、发送请求前执行
并受全局开关和端点配置控制。
Args:
request: 原始请求对象(应已是 Claude 格式)
Returns:
ClaudeMessagesRequest 对象
"""
from src.models.claude import ClaudeMessagesRequest
# 如果已经是 Claude 格式 Pydantic 对象,直接返回
if isinstance(request, ClaudeMessagesRequest):
return request
# 如果是字典,转换为 Pydantic 对象(假设已是 Claude 格式)
if isinstance(request, dict):
return ClaudeMessagesRequest(**request)
return request
def _extract_usage(self, response: dict) -> dict[str, int]:
"""
从 Claude 响应中提取 token 使用情况
Claude 格式使用:
- input_tokens / output_tokens
- cache_creation_input_tokens / cache_read_input_tokens
- 新格式claude_cache_creation_5_m_tokens / claude_cache_creation_1_h_tokens
"""
usage = response.get("usage", {})
total, t5m, t1h = extract_cache_creation_tokens_detail(usage)
return {
"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"cache_creation_input_tokens": total,
"cache_read_input_tokens": usage.get("cache_read_input_tokens", 0),
"cache_creation_input_tokens_5m": t5m,
"cache_creation_input_tokens_1h": t1h,
}
def _normalize_response(self, response: dict[str, Any]) -> dict[str, Any]:
"""
规范化 Claude 响应
Args:
response: 原始响应
Returns:
规范化后的响应
"""
# 作为中转站,直接透传响应,不做标准化处理
return response

View File

@@ -0,0 +1,248 @@
"""
Claude SSE 流解析器
解析 Claude Messages API 的 Server-Sent Events 流。
"""
import json
from typing import Any
from src.core.usage_tokens import extract_cache_creation_tokens
class ClaudeStreamParser:
"""
Claude SSE 流解析器
解析 Claude Messages API 的 SSE 事件流。
事件类型:
- message_start: 消息开始,包含初始 message 对象
- content_block_start: 内容块开始
- content_block_delta: 内容块增量(文本、工具输入等)
- content_block_stop: 内容块结束
- message_delta: 消息增量,包含 stop_reason 和最终 usage
- message_stop: 消息结束
- ping: 心跳事件
- error: 错误事件
"""
# Claude SSE 事件类型
EVENT_MESSAGE_START = "message_start"
EVENT_MESSAGE_STOP = "message_stop"
EVENT_MESSAGE_DELTA = "message_delta"
EVENT_CONTENT_BLOCK_START = "content_block_start"
EVENT_CONTENT_BLOCK_STOP = "content_block_stop"
EVENT_CONTENT_BLOCK_DELTA = "content_block_delta"
EVENT_PING = "ping"
EVENT_ERROR = "error"
# Delta 类型
DELTA_TEXT = "text_delta"
DELTA_INPUT_JSON = "input_json_delta"
def parse_chunk(self, chunk: bytes | str) -> list[dict[str, Any]]:
"""
解析 SSE 数据块
Args:
chunk: 原始 SSE 数据bytes 或 str
Returns:
解析后的事件列表
"""
if isinstance(chunk, bytes):
text = chunk.decode("utf-8")
else:
text = chunk
events: list[dict[str, Any]] = []
lines = text.strip().split("\n")
current_event_type: str | None = None
for line in lines:
line = line.strip()
if not line:
continue
# 解析事件类型行
if line.startswith("event: "):
current_event_type = line[7:]
continue
# 解析数据行
if line.startswith("data: "):
data_str = line[6:]
# 处理 [DONE] 标记
if data_str == "[DONE]":
events.append({"type": "__done__", "raw": "[DONE]"})
continue
try:
data = json.loads(data_str)
# 如果数据中没有 type使用事件行的类型
if "type" not in data and current_event_type:
data["type"] = current_event_type
events.append(data)
except json.JSONDecodeError:
# 无法解析的数据,跳过
pass
current_event_type = None
return events
def parse_line(self, line: str) -> dict[str, Any] | None:
"""
解析单行 SSE 数据
Args:
line: SSE 数据行(已去除 "data: " 前缀)
Returns:
解析后的事件字典,如果无法解析返回 None
"""
if not line or line == "[DONE]":
return None
try:
result = json.loads(line)
if isinstance(result, dict):
return result
return None
except json.JSONDecodeError:
return None
def is_done_event(self, event: dict[str, Any]) -> bool:
"""
判断是否为结束事件
Args:
event: 事件字典
Returns:
True 如果是结束事件
"""
event_type = event.get("type")
return event_type in (self.EVENT_MESSAGE_STOP, "__done__")
def is_error_event(self, event: dict[str, Any]) -> bool:
"""
判断是否为错误事件
Args:
event: 事件字典
Returns:
True 如果是错误事件
"""
return event.get("type") == self.EVENT_ERROR
def get_event_type(self, event: dict[str, Any]) -> str | None:
"""
获取事件类型
Args:
event: 事件字典
Returns:
事件类型字符串
"""
event_type = event.get("type")
return str(event_type) if event_type is not None else None
def extract_text_delta(self, event: dict[str, Any]) -> str | None:
"""
从 content_block_delta 事件中提取文本增量
Args:
event: 事件字典
Returns:
文本增量,如果不是文本 delta 返回 None
"""
if event.get("type") != self.EVENT_CONTENT_BLOCK_DELTA:
return None
delta = event.get("delta", {})
if delta.get("type") == self.DELTA_TEXT:
text = delta.get("text")
return str(text) if text is not None else None
return None
def extract_usage(self, event: dict[str, Any]) -> dict[str, int] | None:
"""
从事件中提取 token 使用量
Args:
event: 事件字典
Returns:
使用量字典,如果没有使用量信息返回 None
"""
event_type = event.get("type")
# message_start 事件包含初始 usage
if event_type == self.EVENT_MESSAGE_START:
message = event.get("message", {})
usage = message.get("usage", {})
if usage:
return {
"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"cache_creation_tokens": extract_cache_creation_tokens(usage),
"cache_read_tokens": usage.get("cache_read_input_tokens", 0),
}
# message_delta 事件包含最终 usage
if event_type == self.EVENT_MESSAGE_DELTA:
usage = event.get("usage", {})
if usage:
return {
"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"cache_creation_tokens": extract_cache_creation_tokens(usage),
"cache_read_tokens": usage.get("cache_read_input_tokens", 0),
}
return None
def extract_message_id(self, event: dict[str, Any]) -> str | None:
"""
从 message_start 事件中提取消息 ID
Args:
event: 事件字典
Returns:
消息 ID如果不是 message_start 返回 None
"""
if event.get("type") != self.EVENT_MESSAGE_START:
return None
message = event.get("message", {})
msg_id = message.get("id")
return str(msg_id) if msg_id is not None else None
def extract_stop_reason(self, event: dict[str, Any]) -> str | None:
"""
从 message_delta 事件中提取停止原因
Args:
event: 事件字典
Returns:
停止原因,如果没有返回 None
"""
if event.get("type") != self.EVENT_MESSAGE_DELTA:
return None
delta = event.get("delta", {})
reason = delta.get("stop_reason")
return str(reason) if reason is not None else None
__all__ = ["ClaudeStreamParser"]