# Protocol Adapter - OpenAI ## ADDED Requirements ### Requirement: 实现 OpenAI ProtocolAdapter 系统 SHALL 全新实现 OpenAI 协议的完整 ProtocolAdapter,对照 `docs/conversion_openai.md`。不沿用旧 `internal/protocol/openai/` 代码。 - `protocolName()` SHALL 返回 `"openai"` - `supportsPassthrough()` SHALL 返回 true - `buildHeaders(provider)` SHALL 构建 `Authorization: Bearer ` 和 `Content-Type: application/json` - `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径 - `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK 返回 true #### Scenario: 认证 Header 构建 - **WHEN** 调用 buildHeaders(provider) - **THEN** SHALL 设置 `Authorization: Bearer ` - **THEN** SHALL 设置 `Content-Type: application/json` - **WHEN** provider.adapter_config 包含 organization - **THEN** SHALL 设置 `OpenAI-Organization` Header #### Scenario: URL 映射 - **WHEN** interfaceType == CHAT - **THEN** SHALL 映射为 `/v1/chat/completions` - **WHEN** interfaceType == MODELS - **THEN** SHALL 映射为 `/v1/models` - **WHEN** interfaceType == EMBEDDINGS - **THEN** SHALL 映射为 `/v1/embeddings` ### Requirement: OpenAI 请求解码(OpenAI → Canonical) 系统 SHALL 实现完整的 OpenAI ChatCompletionRequest 到 CanonicalRequest 的解码。 #### Scenario: System/Developer 消息提取 - **WHEN** OpenAI messages 中包含 role="system" 或 role="developer" 的消息 - **THEN** SHALL 提取为 canonical.system(String) - **THEN** 多条 system/developer 消息 SHALL 合并以 `\n\n` 分隔 - **THEN** SHALL 从 messages 数组中移除这些消息 #### Scenario: Assistant 消息中的 tool_calls 解码 - **WHEN** OpenAI assistant 消息包含 tool_calls - **THEN** SHALL 将每个 tool_call 解码为 ContentBlock{type: "tool_use", id, name, input} - **THEN** function.arguments(JSON 字符串)SHALL 解析为原始 JSON 对象 #### Scenario: Tool 消息解码 - **WHEN** OpenAI 消息 role="tool" - **THEN** SHALL 解码为 CanonicalMessage{role: "tool", content: [ToolResultBlock{tool_use_id, content}]} - **THEN** tool_call_id SHALL 映射为 tool_use_id #### Scenario: 参数映射 - **WHEN** 解码 OpenAI 请求参数 - **THEN** max_completion_tokens(优先)或 max_tokens SHALL 映射为 parameters.max_tokens - **THEN** stop(String 或 Array)SHALL 规范化为 parameters.stop_sequences(Array) - **THEN** temperature/top_p/frequency_penalty/presence_penalty SHALL 直接映射 #### Scenario: 公共字段映射 - **WHEN** 解码 OpenAI 公共字段 - **THEN** user SHALL 映射为 user_id - **THEN** response_format SHALL 映射为 output_format - **THEN** parallel_tool_calls SHALL 映射为 parallel_tool_use - **THEN** reasoning_effort SHALL 映射为 thinking 配置("none" → disabled, 其他 → enabled+effort) #### Scenario: 废弃字段兼容 - **WHEN** OpenAI 请求包含 functions 或 function_call 字段 - **THEN** SHALL 转换为对应的 tools/tool_choice 格式 - **THEN** 仅在 tools/tool_choice 未设置时使用废弃字段 ### Requirement: OpenAI 请求编码(Canonical → OpenAI) 系统 SHALL 实现完整的 CanonicalRequest 到 OpenAI ChatCompletionRequest 的编码。 #### Scenario: 模型名称覆盖 - **WHEN** 编码请求 - **THEN** SHALL 使用 provider.model_name 覆盖 canonical.model #### Scenario: System 消息注入 - **WHEN** canonical.system 不为空 - **THEN** SHALL 编码为 messages 数组头部的 role="system" 消息 #### Scenario: Assistant ��息中 tool_calls 编码 - **WHEN** CanonicalMessage{role: "assistant"} 包含 tool_use 类型 ContentBlock - **THEN** SHALL 提取到 message.tool_calls 数组({id, type: "function", function: {name, arguments}}) - **THEN** arguments SHALL 序列化为 JSON 字符串 #### Scenario: 角色交替合并 - **WHEN** Canonical 消息序列中存在连续同角色消息 - **THEN** SHALL 合并为单条 OpenAI 消息 - **THEN** 文本内容 SHALL 合并连接 #### Scenario: 参数编码 - **WHEN** 编码 CanonicalRequest 参数 - **THEN** parameters.max_tokens SHALL 映射为 max_completion_tokens - **THEN** thinking.type=="disabled" SHALL 映射为 reasoning_effort="none" - **THEN** thinking.effort SHALL 直接映射为 reasoning_effort ### Requirement: OpenAI 响应解码(OpenAI → Canonical) 系统 SHALL 实现 OpenAI ChatCompletionResponse 到 CanonicalResponse 的解码。 #### Scenario: 内容块解码 - **WHEN** OpenAI response.choice[0].message 包含 content - **THEN** SHALL 解码为 TextBlock - **WHEN** 包含 tool_calls - **THEN** SHALL 解码为 ToolUseBlock 数组 - **WHEN** 包含 reasoning_content(非标准,兼容提供商) - **THEN** SHALL 解码为 ThinkingBlock #### Scenario: 停止原因映射 - **WHEN** 解码 finish_reason - **THEN** "stop" SHALL 映射为 "end_turn" - **THEN** "length" SHALL 映射为 "max_tokens" - **THEN** "tool_calls" SHALL 映射为 "tool_use" - **THEN** "content_filter" SHALL 映射为 "content_filter" #### Scenario: Usage 映射 - **WHEN** 解码 OpenAI usage - **THEN** prompt_tokens SHALL 映射为 input_tokens - **THEN** completion_tokens SHALL 映射为 output_tokens - **THEN** prompt_tokens_details.cached_tokens SHALL 映射为 cache_read_tokens ### Requirement: OpenAI 响应编码(Canonical → OpenAI) 系统 SHALL 实现 CanonicalResponse 到 OpenAI ChatCompletionResponse 的编码。 #### Scenario: ThinkingBlock 编码 - **WHEN** CanonicalResponse 包含 ThinkingBlock - **THEN** SHALL 编码为 message.reasoning_content(非标准字段,兼容提供商使用) #### Scenario: 降级处理 - **WHEN** canonical.stop_reason 为 "stop_sequence" 或 "refusal" - **THEN** SHALL 映射为 finish_reason "stop" - **WHEN** canonical.stop_reason 为 "pause_turn" - **THEN** SHALL 映射为 finish_reason "stop"(降级) ### Requirement: OpenAI 流式解码器 系统 SHALL 实现 OpenAIStreamDecoder,将 OpenAI SSE delta chunk 转换为 CanonicalStreamEvent。 Decoder SHALL 维护状态机: - messageStarted: 是否已发送 MessageStartEvent - openBlocks: 当前打开的 block index 集合 - toolCallIdMap/toolCallNameMap/toolCallArguments: 工具调用索引映射和参数累积 - textBlockStarted/thinkingBlockStarted: 文本/思考 block 生命周期追踪 - utf8Remainder: UTF-8 跨 chunk 安全缓冲 #### Scenario: 首个 chunk 触发 MessageStartEvent - **WHEN** 收到第一个有效 chunk - **THEN** SHALL 发出 MessageStartEvent,包含 id 和 model #### Scenario: delta.content 触发 text block 事件 - **WHEN** 收到 delta.content 首次出现 - **THEN** SHALL 发出 ContentBlockStartEvent(text) + ContentBlockDeltaEvent(text_delta) - **WHEN** 收到 delta.content 后续出现 - **THEN** SHALL 发出 ContentBlockDeltaEvent(text_delta) #### Scenario: delta.tool_calls 触发 tool_use block 事件 - **WHEN** delta.tool_calls[i] 首次出现(含 id) - **THEN** SHALL 发出 ContentBlockStartEvent(tool_use) - **WHEN** delta.tool_calls[i].function.arguments 增量到达 - **THEN** SHALL 发出 ContentBlockDeltaEvent(input_json_delta) #### Scenario: delta.reasoning_content 触发 thinking block 事件 - **WHEN** delta.reasoning_content 出现(非标准字段) - **THEN** SHALL 发出 ContentBlockStartEvent(thinking) + ContentBlockDeltaEvent(thinking_delta) #### Scenario: finish_reason 触发关闭事件 - **WHEN** finish_reason 非空 - **THEN** SHALL 为所有 open blocks 发出 ContentBlockStopEvent - **THEN** SHALL 发出 MessageDeltaEvent(含 stop_reason 映射) - **THEN** SHALL 发出 MessageStopEvent #### Scenario: usage chunk 处理 - **WHEN** 收到 choices 为空但含 usage 的 chunk - **THEN** SHALL 发出 MessageDeltaEvent(仅含 usage) #### Scenario: [DONE] 信号处理 - **WHEN** 收到 `data: [DONE]` - **THEN** SHALL 触发 flush() 关闭所有 open blocks #### Scenario: UTF-8 跨 chunk 安全 - **WHEN** chunk 边界截断了 UTF-8 多字节序列 - **THEN** SHALL 使用 utf8Remainder 缓冲不完整字节 - **THEN** 下一个 chunk 到达时 SHALL 拼接后重新解析 ### Requirement: OpenAI 流式编码器 系统 SHALL 实现 OpenAIStreamEncoder,将 CanonicalStreamEvent 编码为 OpenAI SSE chunk。 Encoder SHALL 维护状态: - bufferedStart: 缓冲的 ContentBlockStartEvent - toolCallIndexMap: tool_use_id → OpenAI tool_calls 数组索引映射 #### Scenario: ContentBlockStart 缓冲策略 - **WHEN** 收到 ContentBlockStartEvent - **THEN** SHALL NOT 立即输出,缓冲等待首次 ContentBlockDeltaEvent #### Scenario: ContentBlockDelta 合并输出 - **WHEN** 收到 ContentBlockDeltaEvent 且有缓冲的 StartEvent - **THEN** SHALL 合并 Start 信息(如 tool id/name)与 delta 数据一起输出 - **WHEN** 无缓冲 StartEvent - **THEN** SHALL 仅输出 delta 数据 #### Scenario: MessageStopEvent 输出 [DONE] - **WHEN** 收到 MessageStopEvent - **THEN** SHALL 输出 `data: [DONE]` #### Scenario: PingEvent 和 ErrorEvent 处理 - **WHEN** 收到 PingEvent 或 ErrorEvent - **THEN** SHALL 不输出(OpenAI 无流式错误/心跳事件) ### Requirement: OpenAI 错误编码 系统 SHALL 实现 OpenAI 协议的错误编码。 #### Scenario: 错误响应格式 - **WHEN** 调用 encodeError(conversionError) - **THEN** SHALL 返回 `{error: {message, type, param: null, code}}` - **THEN** ErrorCode SHALL 映射为 OpenAI 错误类型(如 INVALID_INPUT → "invalid_request_error") ### Requirement: OpenAI 扩展层接口编解码 系统 SHALL 实现 OpenAI 协议的扩展层接口编解码。 #### Scenario: /models 列表接口 - **WHEN** 解码 OpenAI models 响应 - **THEN** SHALL 映射为 CanonicalModelList(data[].id → models[].id, created, owned_by) - **WHEN** 编码 CanonicalModelList 为 OpenAI 格式 - **THEN** SHALL 输出 `{object: "list", data: [...]}` #### Scenario: /embeddings 接口 - **WHEN** 解码/编码 embedding 请求和响应 - **THEN** SHALL 使用 CanonicalEmbeddingRequest/Response 做字段映射 #### Scenario: /rerank 接口 - **WHEN** 解码/编码 rerank 请求和响应 - **THEN** SHALL 使用 CanonicalRerankRequest/Response 做字段映射