9.9 KiB
9.9 KiB
ADDED Requirements
Requirement: 实现 OpenAI ProtocolAdapter
系统 SHALL 实现 OpenAI 协议的完整 ProtocolAdapter,对照 docs/conversion_openai.md。
protocolName()SHALL 返回"openai"supportsPassthrough()SHALL 返回 truebuildHeaders(provider)SHALL 构建Authorization: Bearer <api_key>和Content-Type: application/jsonbuildUrl(nativePath, interfaceType)SHALL 按接口类型映射 URL 路径supportsInterface()SHALL 对 CHAT、MODELS、MODEL_INFO、EMBEDDINGS、RERANK 返回 true
Scenario: 认证 Header 构建
- WHEN 调用 buildHeaders(provider)
- THEN SHALL 设置
Authorization: Bearer <provider.api_key> - THEN SHALL 设置
Content-Type: application/json - WHEN provider.adapter_config 包含 organization
- THEN SHALL 设置
OpenAI-OrganizationHeader
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 做字段映射