2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
OpenAI CLI Adapter - 基于通用 CLI Adapter 基类的简化实现
|
|
|
|
|
|
|
|
|
|
|
|
继承 CliAdapterBase,只需配置 FORMAT_ID 和 HANDLER_CLASS。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from typing import Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
from src.api.base.context import ApiRequestContext
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
|
|
|
|
|
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
2025-12-19 23:40:42 +08:00
|
|
|
|
from src.config.settings import config
|
2026-03-01 23:55:26 +08:00
|
|
|
|
from src.core.api_format import ApiFamily, EndpointKind
|
2026-03-05 19:04:21 +08:00
|
|
|
|
from src.core.provider_types import ProviderType
|
2026-02-05 11:17:00 +08:00
|
|
|
|
from src.utils.url_utils import is_codex_url
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_cli_adapter
|
|
|
|
|
|
class OpenAICliAdapter(CliAdapterBase):
|
|
|
|
|
|
"""
|
|
|
|
|
|
OpenAI CLI API 适配器
|
|
|
|
|
|
|
|
|
|
|
|
处理 /v1/responses 端点的请求。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
FORMAT_ID = "openai:cli"
|
|
|
|
|
|
API_FAMILY = ApiFamily.OPENAI
|
2025-12-10 20:52:44 +08:00
|
|
|
|
name = "openai.cli"
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def HANDLER_CLASS(self) -> type[CliMessageHandlerBase]:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""延迟导入 Handler 类避免循环依赖"""
|
|
|
|
|
|
from src.api.handlers.openai_cli.handler import OpenAICliMessageHandler
|
|
|
|
|
|
|
|
|
|
|
|
return OpenAICliMessageHandler
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
allowed_api_formats: list[str] | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
compact: bool = False,
|
|
|
|
|
|
):
|
2026-02-01 17:28:00 +08:00
|
|
|
|
super().__init__(allowed_api_formats)
|
2026-02-19 21:26:18 +08:00
|
|
|
|
self._compact = compact
|
|
|
|
|
|
|
|
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any:
|
2026-03-18 12:35:57 +08:00
|
|
|
|
"""处理 CLI API 请求。"""
|
2026-02-19 21:26:18 +08:00
|
|
|
|
if self._compact:
|
|
|
|
|
|
from src.services.provider.adapters.codex.context import (
|
|
|
|
|
|
CodexRequestContext,
|
|
|
|
|
|
set_codex_request_context,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-18 12:35:57 +08:00
|
|
|
|
# Keep compact routing state out of the request body. Transport/policy layers
|
|
|
|
|
|
# read this request-scoped flag directly when legacy compact fallback is needed.
|
2026-02-19 21:26:18 +08:00
|
|
|
|
set_codex_request_context(CodexRequestContext(is_compact=True))
|
2026-03-18 12:35:57 +08:00
|
|
|
|
|
|
|
|
|
|
body = await context.ensure_json_body_async()
|
|
|
|
|
|
# compact 端点永远非流式
|
|
|
|
|
|
body.pop("stream", None)
|
2026-02-19 21:26:18 +08:00
|
|
|
|
return await super().handle(context)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2025-12-25 00:02:56 +08:00
|
|
|
|
@classmethod
|
2026-02-01 17:28:00 +08:00
|
|
|
|
def build_endpoint_url(
|
2026-02-19 21:26:18 +08:00
|
|
|
|
cls,
|
|
|
|
|
|
base_url: str,
|
|
|
|
|
|
request_data: dict[str, Any],
|
|
|
|
|
|
model_name: str | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
compact: bool = False,
|
2026-03-05 19:04:21 +08:00
|
|
|
|
provider_type: str | None = None,
|
2026-02-01 17:28:00 +08:00
|
|
|
|
) -> str:
|
2026-02-04 23:59:45 +08:00
|
|
|
|
"""构建OpenAI CLI API端点URL(使用 Responses API)
|
|
|
|
|
|
|
|
|
|
|
|
对于 Codex OAuth 端点(如 chatgpt.com/backend-api/codex),直接追加 /responses;
|
|
|
|
|
|
对于标准 OpenAI API,使用 /v1/responses。
|
2026-02-19 21:26:18 +08:00
|
|
|
|
compact=True 时追加 /compact 后缀。
|
2026-03-05 19:04:21 +08:00
|
|
|
|
|
|
|
|
|
|
provider_type 优先:仅当 provider_type 为 codex 时才使用 Codex 路由规则;
|
|
|
|
|
|
未传入 provider_type 时回退到 URL 模式匹配(兼容旧调用方)。
|
2026-02-04 23:59:45 +08:00
|
|
|
|
"""
|
2026-02-19 21:26:18 +08:00
|
|
|
|
suffix = "/responses/compact" if compact else "/responses"
|
2025-12-25 00:02:56 +08:00
|
|
|
|
base_url = base_url.rstrip("/")
|
2026-03-05 19:04:21 +08:00
|
|
|
|
# 判断是否按 Codex 规则构建 URL
|
|
|
|
|
|
is_codex = (
|
|
|
|
|
|
(provider_type or "").lower() == ProviderType.CODEX
|
|
|
|
|
|
if provider_type
|
|
|
|
|
|
else is_codex_url(base_url)
|
|
|
|
|
|
)
|
|
|
|
|
|
if is_codex:
|
2026-02-19 21:26:18 +08:00
|
|
|
|
return f"{base_url}{suffix}"
|
2026-02-04 23:59:45 +08:00
|
|
|
|
# 标准 OpenAI API
|
2025-12-25 00:02:56 +08:00
|
|
|
|
if base_url.endswith("/v1"):
|
2026-02-19 21:26:18 +08:00
|
|
|
|
return f"{base_url}{suffix}"
|
2025-12-25 00:02:56 +08:00
|
|
|
|
else:
|
2026-02-19 21:26:18 +08:00
|
|
|
|
return f"{base_url}/v1{suffix}"
|
2026-02-04 23:59:45 +08:00
|
|
|
|
|
2026-01-22 23:25:51 +08:00
|
|
|
|
# build_request_body 使用基类实现
|
2026-02-04 23:59:45 +08:00
|
|
|
|
# OpenAI CLI normalizer 会自动添加 instructions 字段
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def build_request_body(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
request_data: dict[str, Any] | None = None,
|
|
|
|
|
|
*,
|
|
|
|
|
|
base_url: str | None = None,
|
2026-03-05 19:04:21 +08:00
|
|
|
|
provider_type: str | None = None,
|
2026-02-04 23:59:45 +08:00
|
|
|
|
) -> dict[str, Any]:
|
2026-03-18 12:35:57 +08:00
|
|
|
|
"""构建测试请求体。"""
|
2026-02-04 23:59:45 +08:00
|
|
|
|
from src.api.handlers.base.request_builder import build_test_request_body
|
|
|
|
|
|
|
2026-03-18 12:35:57 +08:00
|
|
|
|
del base_url, provider_type
|
|
|
|
|
|
return build_test_request_body(cls.FORMAT_ID, request_data)
|
2025-12-25 00:02:56 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def get_cli_user_agent(cls) -> str | None:
|
2025-12-25 00:02:56 +08:00
|
|
|
|
"""获取OpenAI CLI User-Agent"""
|
|
|
|
|
|
return config.internal_user_agent_openai_cli
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
__all__ = ["OpenAICliAdapter"]
|
2026-03-01 23:55:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@register_cli_adapter
|
|
|
|
|
|
class OpenAICompactAdapter(OpenAICliAdapter):
|
|
|
|
|
|
"""OpenAI Compact Responses adapter (/v1/responses/compact)."""
|
|
|
|
|
|
|
|
|
|
|
|
FORMAT_ID = "openai:compact"
|
|
|
|
|
|
ENDPOINT_KIND = EndpointKind.COMPACT
|
|
|
|
|
|
name = "openai.compact"
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, allowed_api_formats: list[str] | None = None):
|
|
|
|
|
|
super().__init__(allowed_api_formats=allowed_api_formats, compact=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__.append("OpenAICompactAdapter")
|