2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
OpenAI CLI Message Handler - 基于通用 CLI Handler 基类的简化实现
|
|
|
|
|
|
|
|
|
|
|
|
继承 CliMessageHandlerBase,只需覆盖格式特定的配置和事件处理逻辑。
|
|
|
|
|
|
代码量从原来的 900+ 行减少到 ~100 行。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from typing import Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from src.api.handlers.base.cli_handler_base import (
|
|
|
|
|
|
CliMessageHandlerBase,
|
|
|
|
|
|
StreamContext,
|
|
|
|
|
|
)
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from src.core.api_format import ApiFamily, EndpointKind
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenAICliMessageHandler(CliMessageHandlerBase):
|
|
|
|
|
|
"""
|
|
|
|
|
|
OpenAI CLI Message Handler - 处理 OpenAI CLI Responses API 格式
|
|
|
|
|
|
|
|
|
|
|
|
使用新三层架构 (Provider -> ProviderEndpoint -> ProviderAPIKey)
|
2026-02-02 21:16:28 +08:00
|
|
|
|
通过 TaskService/FailoverEngine 实现自动故障转移、健康监控和并发控制
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
响应格式特点:
|
|
|
|
|
|
- 使用 output[] 数组而非 content[]
|
|
|
|
|
|
- 使用 output_text 类型而非普通 text
|
|
|
|
|
|
- 流式事件:response.output_text.delta, response.output_text.done
|
|
|
|
|
|
|
|
|
|
|
|
模型字段:请求体顶级 model 字段
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
FORMAT_ID = "openai:cli"
|
|
|
|
|
|
API_FAMILY = ApiFamily.OPENAI
|
|
|
|
|
|
ENDPOINT_KIND = EndpointKind.CLI
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
def extract_model_from_request(
|
|
|
|
|
|
self,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
request_body: dict[str, Any],
|
|
|
|
|
|
path_params: dict[str, Any] | None = None, # noqa: ARG002
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> 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,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
request_body: dict[str, Any],
|
2025-12-10 20:52:44 +08:00
|
|
|
|
mapped_model: str,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> dict[str, Any]:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
data: dict[str, Any],
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
处理 OpenAI CLI 格式的 SSE 事件
|
|
|
|
|
|
|
|
|
|
|
|
事件类型:
|
|
|
|
|
|
- response.output_text.delta: 文本增量
|
|
|
|
|
|
- response.completed: 响应完成(包含 usage)
|
2026-02-07 14:30:46 +08:00
|
|
|
|
|
|
|
|
|
|
跨格式转换时(如 provider=claude:chat),原始事件数据是 Provider 格式而非 OpenAI CLI 格式。
|
|
|
|
|
|
此时先调用基类方法通过 Provider 格式解析器提取 usage,再执行 OpenAI CLI 特定的处理逻辑。
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
2026-02-07 14:30:46 +08:00
|
|
|
|
# 跨格式转换时:原始事件是 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)的处理逻辑
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
# 提取 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):
|
2025-12-16 02:39:20 +08:00
|
|
|
|
ctx.append_text(delta)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
elif isinstance(delta, dict) and "text" in delta:
|
2025-12-16 02:39:20 +08:00
|
|
|
|
ctx.append_text(delta["text"])
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 处理完成事件
|
|
|
|
|
|
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:
|
2025-12-16 02:39:20 +08:00
|
|
|
|
ctx.append_text(text)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 备用:从顶层 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,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
response: dict[str, Any],
|
|
|
|
|
|
) -> dict[str, Any]:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
从 OpenAI 响应中提取元数据
|
|
|
|
|
|
|
|
|
|
|
|
提取 model、status、response_id 等字段作为元数据。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
response: OpenAI API 响应
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
提取的元数据字典
|
|
|
|
|
|
"""
|
2026-01-30 03:10:21 +08:00
|
|
|
|
metadata: dict[str, Any] = {}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 提取模型名称(实际使用的模型)
|
|
|
|
|
|
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:
|
2026-02-01 17:28:00 +08:00
|
|
|
|
ctx.response_metadata["system_fingerprint"] = ctx.final_response[
|
|
|
|
|
|
"system_fingerprint"
|
|
|
|
|
|
]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 如果没有从响应中获取到 model,使用上下文中的
|
|
|
|
|
|
if "model" not in ctx.response_metadata and ctx.model:
|
|
|
|
|
|
ctx.response_metadata["model"] = ctx.model
|