mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 引入模块钩子系统,解耦认证逻辑,支持模块/normalizer/parser 自动发现
- 新增 HookDispatcher 钩子分发器,支持 FIRST_RESULT 和 COLLECT_ALL 两种策略 - LDAP 认证逻辑从 AuthService 移至 ldap 模块钩子实现 - Management Token 前缀认证从 pipeline 硬编码改为模块钩子注册 - src/modules/ 改为自动扫描子目录发现 ModuleDefinition - normalizers 和 parsers 注册改为基于类属性自动发现 - OpenAI CLI 增加 /v1/responses/compact 端点和并行 tool_call 支持 - OpenAI CLI normalizer 支持 Chat Completions 格式自动回退 - Codex 适配器增加 compact 模式上下文传递和 header 调整 - HeaderBuilder 改进非 latin-1 字符处理(UTF-8 字节透传) - Gunicorn 增加 graceful_timeout 防止僵尸进程
This commit is contained in:
@@ -130,12 +130,14 @@ def _extract_embedded_status_code(error_info: dict[str, Any] | None) -> int | No
|
||||
class OpenAIResponseParser(ResponseParser):
|
||||
"""OpenAI 格式响应解析器"""
|
||||
|
||||
API_FORMAT = "openai:chat"
|
||||
|
||||
def __init__(self) -> None:
|
||||
from src.api.handlers.openai.stream_parser import OpenAIStreamParser
|
||||
|
||||
self._parser = OpenAIStreamParser()
|
||||
self.name = "openai:chat"
|
||||
self.api_format = "openai:chat"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
||||
if not line or not line.strip():
|
||||
@@ -249,10 +251,12 @@ class OpenAICliResponseParser(OpenAIResponseParser):
|
||||
- 流式事件: response.completed 事件中 usage 嵌套在 response 对象内
|
||||
"""
|
||||
|
||||
API_FORMAT = "openai:cli"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.name = "openai:cli"
|
||||
self.api_format = "openai:cli"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
|
||||
result = ParsedResponse(
|
||||
@@ -384,12 +388,14 @@ class OpenAICliResponseParser(OpenAIResponseParser):
|
||||
class ClaudeResponseParser(ResponseParser):
|
||||
"""Claude 格式响应解析器"""
|
||||
|
||||
API_FORMAT = "claude:chat"
|
||||
|
||||
def __init__(self) -> None:
|
||||
from src.api.handlers.claude.stream_parser import ClaudeStreamParser
|
||||
|
||||
self._parser = ClaudeStreamParser()
|
||||
self.name = "claude:chat"
|
||||
self.api_format = "claude:chat"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
||||
if not line or not line.strip():
|
||||
@@ -522,21 +528,25 @@ class ClaudeResponseParser(ResponseParser):
|
||||
class ClaudeCliResponseParser(ClaudeResponseParser):
|
||||
"""Claude CLI 格式响应解析器"""
|
||||
|
||||
API_FORMAT = "claude:cli"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.name = "claude:cli"
|
||||
self.api_format = "claude:cli"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
|
||||
class GeminiResponseParser(ResponseParser):
|
||||
"""Gemini 格式响应解析器"""
|
||||
|
||||
API_FORMAT = "gemini:chat"
|
||||
|
||||
def __init__(self) -> None:
|
||||
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
|
||||
|
||||
self._parser = GeminiStreamParser()
|
||||
self.name = "gemini:chat"
|
||||
self.api_format = "gemini:chat"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
||||
"""
|
||||
@@ -687,10 +697,12 @@ class GeminiResponseParser(ResponseParser):
|
||||
class GeminiCliResponseParser(GeminiResponseParser):
|
||||
"""Gemini CLI 格式响应解析器"""
|
||||
|
||||
API_FORMAT = "gemini:cli"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.name = "gemini:cli"
|
||||
self.api_format = "gemini:cli"
|
||||
self.name = self.API_FORMAT
|
||||
self.api_format = self.API_FORMAT
|
||||
|
||||
|
||||
# 注册解析器到 core 层注册表(供 services 层通过 format_id 获取)
|
||||
@@ -698,12 +710,20 @@ from src.core.stream_types import get_parser_for_format, register_parser
|
||||
|
||||
|
||||
def register_default_parsers() -> None:
|
||||
register_parser("claude:chat", ClaudeResponseParser)
|
||||
register_parser("claude:cli", ClaudeCliResponseParser)
|
||||
register_parser("openai:chat", OpenAIResponseParser)
|
||||
register_parser("openai:cli", OpenAICliResponseParser)
|
||||
register_parser("gemini:chat", GeminiResponseParser)
|
||||
register_parser("gemini:cli", GeminiCliResponseParser)
|
||||
"""自动发现所有 ResponseParser 子类并注册
|
||||
|
||||
通过 __subclasses__() 递归收集所有 ResponseParser 子类,
|
||||
使用类级别 API_FORMAT 属性获取格式 ID,无需实例化。
|
||||
"""
|
||||
|
||||
def _collect_subclasses(base: type) -> list[type]:
|
||||
subs = base.__subclasses__()
|
||||
return subs + [s for c in subs for s in _collect_subclasses(c)]
|
||||
|
||||
for cls in _collect_subclasses(ResponseParser):
|
||||
api_format = getattr(cls, "API_FORMAT", None)
|
||||
if api_format:
|
||||
register_parser(api_format, cls)
|
||||
|
||||
|
||||
# 模块加载时自动注册(保证 import parsers 即可用,测试也不需要手动初始化)
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
from src.api.handlers.openai.adapter import OpenAIChatAdapter
|
||||
@@ -38,8 +39,32 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
|
||||
return OpenAICliMessageHandler
|
||||
|
||||
def __init__(self, allowed_api_formats: list[str] | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
allowed_api_formats: list[str] | None = None,
|
||||
*,
|
||||
compact: bool = False,
|
||||
):
|
||||
super().__init__(allowed_api_formats)
|
||||
self._compact = compact
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
"""处理 CLI API 请求 -- compact 模式下注入标记并强制非流式"""
|
||||
if self._compact:
|
||||
body = context.ensure_json_body()
|
||||
body["_aether_compact"] = True
|
||||
# compact 端点永远非流式
|
||||
body.pop("stream", None)
|
||||
# 预设 Codex compact 上下文 -- finalize_provider_request 在 envelope
|
||||
# 之前运行,会清除 _aether_compact sentinel,所以在此处提前设置
|
||||
# context var 供 Codex envelope 和 build_codex_url 读取
|
||||
from src.services.provider.adapters.codex.context import (
|
||||
CodexRequestContext,
|
||||
set_codex_request_context,
|
||||
)
|
||||
|
||||
set_codex_request_context(CodexRequestContext(is_compact=True))
|
||||
return await super().handle(context)
|
||||
|
||||
# =========================================================================
|
||||
# 模型列表查询
|
||||
@@ -66,22 +91,29 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
|
||||
@classmethod
|
||||
def build_endpoint_url(
|
||||
cls, base_url: str, request_data: dict[str, Any], model_name: str | None = None
|
||||
cls,
|
||||
base_url: str,
|
||||
request_data: dict[str, Any],
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
compact: bool = False,
|
||||
) -> str:
|
||||
"""构建OpenAI CLI API端点URL(使用 Responses API)
|
||||
|
||||
对于 Codex OAuth 端点(如 chatgpt.com/backend-api/codex),直接追加 /responses;
|
||||
对于标准 OpenAI API,使用 /v1/responses。
|
||||
compact=True 时追加 /compact 后缀。
|
||||
"""
|
||||
suffix = "/responses/compact" if compact else "/responses"
|
||||
base_url = base_url.rstrip("/")
|
||||
# Codex OAuth 端点:chatgpt.com/backend-api/codex -> /responses
|
||||
# Codex OAuth 端点:chatgpt.com/backend-api/codex -> /responses[/compact]
|
||||
if is_codex_url(base_url):
|
||||
return f"{base_url}/responses"
|
||||
return f"{base_url}{suffix}"
|
||||
# 标准 OpenAI API
|
||||
if base_url.endswith("/v1"):
|
||||
return f"{base_url}/responses"
|
||||
return f"{base_url}{suffix}"
|
||||
else:
|
||||
return f"{base_url}/v1/responses"
|
||||
return f"{base_url}/v1{suffix}"
|
||||
|
||||
# build_request_body 使用基类实现
|
||||
# OpenAI CLI normalizer 会自动添加 instructions 字段
|
||||
|
||||
@@ -72,6 +72,20 @@ class OpenAICliMessageHandler(CliMessageHandlerBase):
|
||||
result["model"] = mapped_model
|
||||
return result
|
||||
|
||||
def finalize_provider_request(
|
||||
self,
|
||||
request_body: dict[str, Any],
|
||||
*,
|
||||
mapped_model: str | None,
|
||||
provider_api_format: str | None,
|
||||
) -> dict[str, Any]:
|
||||
# Strip internal sentinel before sending upstream (non-Codex providers
|
||||
# don't have an envelope that removes it).
|
||||
request_body.pop("_aether_compact", None)
|
||||
return super().finalize_provider_request(
|
||||
request_body, mapped_model=mapped_model, provider_api_format=provider_api_format
|
||||
)
|
||||
|
||||
def _process_event_data(
|
||||
self,
|
||||
ctx: StreamContext,
|
||||
|
||||
Reference in New Issue
Block a user