mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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:
26
_deprecated_py_src/api/handlers/openai/__init__.py
Normal file
26
_deprecated_py_src/api/handlers/openai/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""OpenAI handler package (lazy exports)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
|
||||
"OpenAIChatAdapter": (".adapter", "OpenAIChatAdapter"),
|
||||
"OpenAIChatHandler": (".handler", "OpenAIChatHandler"),
|
||||
"OpenAIVideoAdapter": (".video_adapter", "OpenAIVideoAdapter"),
|
||||
"OpenAIVideoHandler": (".video_handler", "OpenAIVideoHandler"),
|
||||
}
|
||||
|
||||
__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
|
||||
123
_deprecated_py_src/api/handlers/openai/adapter.py
Normal file
123
_deprecated_py_src/api/handlers/openai/adapter.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
OpenAI Chat Adapter - 基于 ChatAdapterBase 的 OpenAI Chat API 适配器
|
||||
|
||||
处理 /v1/chat/completions 端点的 OpenAI Chat 格式请求。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register_adapter
|
||||
class OpenAIChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
OpenAI Chat Completions API 适配器
|
||||
|
||||
处理 OpenAI Chat 格式的请求(/v1/chat/completions 端点)。
|
||||
"""
|
||||
|
||||
FORMAT_ID = "openai:chat"
|
||||
API_FAMILY = ApiFamily.OPENAI
|
||||
name = "openai.chat"
|
||||
|
||||
@property
|
||||
def HANDLER_CLASS(self) -> type[ChatHandlerBase]:
|
||||
"""延迟导入 Handler 类避免循环依赖"""
|
||||
from src.api.handlers.openai.handler import OpenAIChatHandler
|
||||
|
||||
return OpenAIChatHandler
|
||||
|
||||
def __init__(self, allowed_api_formats: list[str] | None = None):
|
||||
super().__init__(allowed_api_formats)
|
||||
|
||||
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(
|
||||
400, "Request body must be a JSON object", "invalid_request_error"
|
||||
)
|
||||
|
||||
required_fields = ["model", "messages"]
|
||||
missing = [f for f in required_fields if f not in original_request_body]
|
||||
if missing:
|
||||
return self._error_response(
|
||||
400,
|
||||
f"Missing required fields: {', '.join(missing)}",
|
||||
"invalid_request_error",
|
||||
)
|
||||
|
||||
try:
|
||||
return OpenAIRequest.model_validate(original_request_body, strict=False)
|
||||
except ValueError as e:
|
||||
return self._error_response(400, str(e), "invalid_request_error")
|
||||
except Exception as e:
|
||||
logger.warning(f"Pydantic验证警告(将继续处理): {str(e)}")
|
||||
return OpenAIRequest.model_construct(
|
||||
model=original_request_body.get("model"),
|
||||
messages=original_request_body.get("messages", []),
|
||||
stream=original_request_body.get("stream", False),
|
||||
max_tokens=original_request_body.get("max_tokens"),
|
||||
)
|
||||
|
||||
def _build_audit_metadata(self, payload: dict[str, Any], request_obj: Any) -> dict[str, Any]:
|
||||
"""构建 OpenAI Chat 特定的审计元数据"""
|
||||
role_counts = {}
|
||||
for message in request_obj.messages:
|
||||
role_counts[message.role] = role_counts.get(message.role, 0) + 1
|
||||
|
||||
return {
|
||||
"action": "openai_chat_completion",
|
||||
"model": request_obj.model,
|
||||
"stream": bool(request_obj.stream),
|
||||
"max_tokens": request_obj.max_tokens,
|
||||
"temperature": request_obj.temperature,
|
||||
"top_p": request_obj.top_p,
|
||||
"messages_count": len(request_obj.messages),
|
||||
"message_roles": role_counts,
|
||||
"tools_count": len(request_obj.tools or []),
|
||||
"response_format": bool(request_obj.response_format),
|
||||
"user_identifier": request_obj.user,
|
||||
}
|
||||
|
||||
def _error_response(self, status_code: int, message: str, error_type: str) -> JSONResponse:
|
||||
"""生成 OpenAI 格式的错误响应"""
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"error": {
|
||||
"type": error_type,
|
||||
"message": message,
|
||||
"code": status_code,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@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:
|
||||
"""构建OpenAI API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
if base_url.endswith("/v1"):
|
||||
return f"{base_url}/chat/completions"
|
||||
else:
|
||||
return f"{base_url}/v1/chat/completions"
|
||||
|
||||
|
||||
__all__ = ["OpenAIChatAdapter"]
|
||||
127
_deprecated_py_src/api/handlers/openai/handler.py
Normal file
127
_deprecated_py_src/api/handlers/openai/handler.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
OpenAI Chat Handler - 基于通用 Chat Handler 基类的简化实现
|
||||
|
||||
继承 ChatHandlerBase,只需覆盖格式特定的方法。
|
||||
代码量从原来的 ~1315 行减少到 ~100 行。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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):
|
||||
"""
|
||||
OpenAI Chat Handler - 处理 OpenAI Chat Completions API 格式的请求
|
||||
|
||||
格式特点:
|
||||
- 使用 prompt_tokens/completion_tokens
|
||||
- 不支持 cache tokens
|
||||
- 请求格式:OpenAIRequest
|
||||
"""
|
||||
|
||||
FORMAT_ID = "openai:chat"
|
||||
API_FAMILY = ApiFamily.OPENAI
|
||||
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:
|
||||
"""
|
||||
从请求中提取模型名 - OpenAI 格式实现
|
||||
|
||||
OpenAI API 的 model 在请求体顶级字段。
|
||||
|
||||
Args:
|
||||
request_body: 请求体
|
||||
path_params: URL 路径参数(OpenAI 不使用)
|
||||
|
||||
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]:
|
||||
"""
|
||||
将映射后的模型名应用到请求体
|
||||
|
||||
OpenAI 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: Request) -> None:
|
||||
"""
|
||||
将请求转换为 OpenAI 格式的 Pydantic 对象
|
||||
|
||||
注意:此方法只做类型转换(dict → Pydantic),不做跨格式转换。
|
||||
跨格式转换由调度/执行层(TaskService + RequestDispatcher)在选中候选后、发送请求前执行,
|
||||
并受全局开关和端点配置控制。
|
||||
|
||||
Args:
|
||||
request: 原始请求对象(应已是 OpenAI 格式)
|
||||
|
||||
Returns:
|
||||
OpenAIRequest 对象
|
||||
"""
|
||||
from src.models.openai import OpenAIRequest
|
||||
|
||||
# 如果已经是 OpenAI 格式 Pydantic 对象,直接返回
|
||||
if isinstance(request, OpenAIRequest):
|
||||
return request
|
||||
|
||||
# 如果是字典,转换为 Pydantic 对象(假设已是 OpenAI 格式)
|
||||
if isinstance(request, dict):
|
||||
return OpenAIRequest(**request)
|
||||
|
||||
return request
|
||||
|
||||
def _extract_usage(self, response: dict) -> dict[str, int]:
|
||||
"""
|
||||
从 OpenAI 响应中提取 token 使用情况
|
||||
|
||||
OpenAI 格式使用:
|
||||
- prompt_tokens / completion_tokens
|
||||
- 不支持 cache tokens
|
||||
"""
|
||||
usage = response.get("usage", {})
|
||||
|
||||
return {
|
||||
"input_tokens": usage.get("prompt_tokens", 0),
|
||||
"output_tokens": usage.get("completion_tokens", 0),
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": 0,
|
||||
}
|
||||
|
||||
def _normalize_response(self, response: dict) -> dict:
|
||||
"""
|
||||
规范化 OpenAI 响应
|
||||
|
||||
Args:
|
||||
response: 原始响应
|
||||
|
||||
Returns:
|
||||
规范化后的响应
|
||||
"""
|
||||
# 作为中转站,直接透传响应,不做标准化处理
|
||||
return response
|
||||
187
_deprecated_py_src/api/handlers/openai/stream_parser.py
Normal file
187
_deprecated_py_src/api/handlers/openai/stream_parser.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
OpenAI SSE 流解析器
|
||||
|
||||
解析 OpenAI Chat Completions API 的 Server-Sent Events 流。
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
class OpenAIStreamParser:
|
||||
"""
|
||||
OpenAI SSE 流解析器
|
||||
|
||||
解析 OpenAI Chat Completions API 的 SSE 事件流。
|
||||
|
||||
OpenAI 流格式:
|
||||
- 每个 chunk 是一个 JSON 对象,包含 choices 数组
|
||||
- choices[0].delta 包含增量内容
|
||||
- choices[0].finish_reason 表示结束原因
|
||||
- 流结束时发送 data: [DONE]
|
||||
"""
|
||||
|
||||
def parse_chunk(self, chunk: bytes | str) -> list[dict[str, Any]]:
|
||||
"""
|
||||
解析 SSE 数据块
|
||||
|
||||
Args:
|
||||
chunk: 原始 SSE 数据(bytes 或 str)
|
||||
|
||||
Returns:
|
||||
解析后的 chunk 列表
|
||||
"""
|
||||
if isinstance(chunk, bytes):
|
||||
text = chunk.decode("utf-8")
|
||||
else:
|
||||
text = chunk
|
||||
|
||||
chunks: list[dict[str, Any]] = []
|
||||
lines = text.strip().split("\n")
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# 解析数据行
|
||||
if line.startswith("data: "):
|
||||
data_str = line[6:]
|
||||
|
||||
# 处理 [DONE] 标记
|
||||
if data_str == "[DONE]":
|
||||
chunks.append({"__done__": True})
|
||||
continue
|
||||
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
chunks.append(data)
|
||||
except json.JSONDecodeError:
|
||||
# 无法解析的数据,跳过
|
||||
pass
|
||||
|
||||
return chunks
|
||||
|
||||
def parse_line(self, line: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
解析单行 SSE 数据
|
||||
|
||||
Args:
|
||||
line: SSE 数据行(已去除 "data: " 前缀)
|
||||
|
||||
Returns:
|
||||
解析后的 chunk 字典,如果无法解析返回 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_chunk(self, chunk: dict[str, Any]) -> bool:
|
||||
"""
|
||||
判断是否为结束 chunk
|
||||
|
||||
Args:
|
||||
chunk: chunk 字典
|
||||
|
||||
Returns:
|
||||
True 如果是结束 chunk
|
||||
"""
|
||||
# 内部标记
|
||||
if chunk.get("__done__"):
|
||||
return True
|
||||
|
||||
# 检查 finish_reason
|
||||
choices = chunk.get("choices", [])
|
||||
if choices:
|
||||
finish_reason = choices[0].get("finish_reason")
|
||||
return finish_reason is not None
|
||||
|
||||
return False
|
||||
|
||||
def get_finish_reason(self, chunk: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
获取结束原因
|
||||
|
||||
Args:
|
||||
chunk: chunk 字典
|
||||
|
||||
Returns:
|
||||
结束原因字符串
|
||||
"""
|
||||
choices = chunk.get("choices", [])
|
||||
if choices:
|
||||
reason = choices[0].get("finish_reason")
|
||||
return str(reason) if reason is not None else None
|
||||
return None
|
||||
|
||||
def extract_text_delta(self, chunk: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
从 chunk 中提取文本增量
|
||||
|
||||
Args:
|
||||
chunk: chunk 字典
|
||||
|
||||
Returns:
|
||||
文本增量,如果没有返回 None
|
||||
"""
|
||||
choices = chunk.get("choices", [])
|
||||
if not choices:
|
||||
return None
|
||||
|
||||
delta = choices[0].get("delta", {})
|
||||
content = delta.get("content")
|
||||
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
|
||||
return None
|
||||
|
||||
def extract_tool_calls_delta(self, chunk: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""
|
||||
从 chunk 中提取工具调用增量
|
||||
|
||||
Args:
|
||||
chunk: chunk 字典
|
||||
|
||||
Returns:
|
||||
工具调用列表,如果没有返回 None
|
||||
"""
|
||||
choices = chunk.get("choices", [])
|
||||
if not choices:
|
||||
return None
|
||||
|
||||
delta = choices[0].get("delta", {})
|
||||
tool_calls = delta.get("tool_calls")
|
||||
if isinstance(tool_calls, list):
|
||||
return tool_calls
|
||||
return None
|
||||
|
||||
def extract_role(self, chunk: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
从 chunk 中提取角色
|
||||
|
||||
通常只在第一个 chunk 中出现。
|
||||
|
||||
Args:
|
||||
chunk: chunk 字典
|
||||
|
||||
Returns:
|
||||
角色字符串
|
||||
"""
|
||||
choices = chunk.get("choices", [])
|
||||
if not choices:
|
||||
return None
|
||||
|
||||
delta = choices[0].get("delta", {})
|
||||
role = delta.get("role")
|
||||
return str(role) if role is not None else None
|
||||
|
||||
|
||||
__all__ = ["OpenAIStreamParser"]
|
||||
24
_deprecated_py_src/api/handlers/openai/video_adapter.py
Normal file
24
_deprecated_py_src/api/handlers/openai/video_adapter.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
OpenAI Video Adapter - 基于 VideoAdapterBase 的 Sora 适配器
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.api.handlers.base.video_adapter_base import VideoAdapterBase
|
||||
from src.api.handlers.base.video_handler_base import VideoHandlerBase
|
||||
from src.core.api_format import ApiFamily
|
||||
|
||||
|
||||
class OpenAIVideoAdapter(VideoAdapterBase):
|
||||
FORMAT_ID = "openai:video"
|
||||
API_FAMILY = ApiFamily.OPENAI
|
||||
name = "openai.video"
|
||||
|
||||
@property
|
||||
def HANDLER_CLASS(self) -> type[VideoHandlerBase]:
|
||||
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
|
||||
|
||||
return OpenAIVideoHandler
|
||||
|
||||
|
||||
__all__ = ["OpenAIVideoAdapter"]
|
||||
1299
_deprecated_py_src/api/handlers/openai/video_handler.py
Normal file
1299
_deprecated_py_src/api/handlers/openai/video_handler.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user