Files
Aether/_deprecated_py_src/api/handlers/openai_cli/handler.py
fawney19 1d9c77522a 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)
2026-04-03 16:26:16 +08:00

227 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
OpenAI CLI Message Handler - 基于通用 CLI Handler 基类的简化实现
继承 CliMessageHandlerBase只需覆盖格式特定的配置和事件处理逻辑。
代码量从原来的 900+ 行减少到 ~100 行。
"""
from typing import Any
from src.api.handlers.base.cli_handler_base import (
CliMessageHandlerBase,
StreamContext,
)
from src.core.api_format import ApiFamily, EndpointKind
class OpenAICliMessageHandler(CliMessageHandlerBase):
"""
OpenAI CLI Message Handler - 处理 OpenAI CLI Responses API 格式
使用新三层架构 (Provider -> ProviderEndpoint -> ProviderAPIKey)
通过 TaskService/FailoverEngine 实现自动故障转移、健康监控和并发控制
响应格式特点:
- 使用 output[] 数组而非 content[]
- 使用 output_text 类型而非普通 text
- 流式事件response.output_text.delta, response.output_text.done
模型字段:请求体顶级 model 字段
"""
FORMAT_ID = "openai:cli"
API_FAMILY = ApiFamily.OPENAI
ENDPOINT_KIND = EndpointKind.CLI
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 CLI (Responses API) 的 model 在请求体顶级字段。
Args:
request_body: 原始请求体
mapped_model: 映射后的模型名
Returns:
更新了 model 字段的请求体
"""
result = dict(request_body)
result["model"] = mapped_model
return result
def _process_event_data(
self,
ctx: StreamContext,
event_type: str,
data: dict[str, Any],
) -> None:
"""
处理 OpenAI CLI 格式的 SSE 事件
事件类型:
- response.output_text.delta: 文本增量
- response.completed: 响应完成(包含 usage
跨格式转换时(如 provider=claude:chat原始事件数据是 Provider 格式而非 OpenAI CLI 格式。
此时先调用基类方法通过 Provider 格式解析器提取 usage再执行 OpenAI CLI 特定的处理逻辑。
"""
# 跨格式转换时:原始事件是 Provider 格式(如 Claude
# 基类 _process_event_data 会自动选择正确的 Provider 解析器提取 usage/text
if ctx.provider_api_format and ctx.provider_api_format != ctx.client_api_format:
super()._process_event_data(ctx, event_type, data)
return
# 以下是同格式openai:cli的处理逻辑
# 提取 response_id
if not ctx.response_id:
response_obj = data.get("response")
if isinstance(response_obj, dict) and response_obj.get("id"):
ctx.response_id = response_obj["id"]
elif "id" in data:
ctx.response_id = data["id"]
# 处理文本增量
if event_type in ["response.output_text.delta", "response.outtext.delta"]:
delta = data.get("delta")
if isinstance(delta, str):
ctx.append_text(delta)
elif isinstance(delta, dict) and "text" in delta:
ctx.append_text(delta["text"])
# 处理完成事件
elif event_type == "response.completed":
ctx.has_completion = True
response_obj = data.get("response")
if isinstance(response_obj, dict):
ctx.final_response = response_obj
usage_obj = response_obj.get("usage")
if isinstance(usage_obj, dict):
ctx.final_usage = usage_obj
ctx.input_tokens = usage_obj.get("input_tokens", 0)
ctx.output_tokens = usage_obj.get("output_tokens", 0)
details = usage_obj.get("input_tokens_details")
if isinstance(details, dict):
ctx.cached_tokens = details.get("cached_tokens", 0)
# 如果没有收集到文本,从 output 中提取
if not ctx.collected_text and "output" in response_obj:
for output_item in response_obj.get("output", []):
if output_item.get("type") != "message":
continue
for content_item in output_item.get("content", []):
if content_item.get("type") == "output_text":
text = content_item.get("text", "")
if text:
ctx.append_text(text)
# 备用:从顶层 usage 提取
usage_obj = data.get("usage")
if isinstance(usage_obj, dict) and not ctx.final_usage:
ctx.final_usage = usage_obj
ctx.input_tokens = usage_obj.get("input_tokens", 0)
ctx.output_tokens = usage_obj.get("output_tokens", 0)
details = usage_obj.get("input_tokens_details")
if isinstance(details, dict):
ctx.cached_tokens = details.get("cached_tokens", 0)
# 备用:从 response 字段提取
response_obj = data.get("response")
if isinstance(response_obj, dict) and not ctx.final_response:
ctx.final_response = response_obj
def _extract_response_metadata(
self,
response: dict[str, Any],
) -> dict[str, Any]:
"""
从 OpenAI 响应中提取元数据
提取 model、status、response_id 等字段作为元数据。
Args:
response: OpenAI API 响应
Returns:
提取的元数据字典
"""
metadata: dict[str, Any] = {}
# 提取模型名称(实际使用的模型)
if "model" in response:
metadata["model"] = response["model"]
# 提取响应 ID
if "id" in response:
metadata["response_id"] = response["id"]
# 提取状态
if "status" in response:
metadata["status"] = response["status"]
# 提取对象类型
if "object" in response:
metadata["object"] = response["object"]
# 提取系统指纹(如果存在)
if "system_fingerprint" in response:
metadata["system_fingerprint"] = response["system_fingerprint"]
return metadata
def _finalize_stream_metadata(self, ctx: StreamContext) -> None:
"""
从流上下文中提取最终元数据
在流传输完成后调用,从收集的事件中提取元数据。
Args:
ctx: 流上下文
"""
# 从 response_id 提取响应 ID
if ctx.response_id:
ctx.response_metadata["response_id"] = ctx.response_id
# 从 final_response 提取更多元数据
if ctx.final_response and isinstance(ctx.final_response, dict):
if "model" in ctx.final_response:
ctx.response_metadata["model"] = ctx.final_response["model"]
if "status" in ctx.final_response:
ctx.response_metadata["status"] = ctx.final_response["status"]
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"
]
# 如果没有从响应中获取到 model使用上下文中的
if "model" not in ctx.response_metadata and ctx.model:
ctx.response_metadata["model"] = ctx.model