1
0
Files
nex/openspec/specs/protocol-adapter-openai/spec.md
lanyuanxiaoyao 56ecc73d1b docs: 整合 openspec 规范,合并配置和前端相关独立 spec
将 cli-config、config-priority、env-config 合并入 config-management;
将 tdesign-integration、recharts-integration、frontend-config-ui、
frontend-testing、stats-dashboard 合并入新的 frontend/spec.md;
清理其余 spec 中的冗余标记,补充缺失场景。
2026-04-20 19:55:56 +08:00

272 lines
10 KiB
Markdown
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.
# Protocol Adapter - OpenAI
## Purpose
实现 OpenAI 协议的完整 ProtocolAdapter支持请求/响应编解码、流式转换和错误处理,遵循 OpenAI Chat Completions API 规范。
### Requirement: 实现 OpenAI ProtocolAdapter
系统 SHALL 全新实现 OpenAI 协议的完整 ProtocolAdapter对照 `docs/conversion_openai.md`。不沿用旧 `internal/protocol/openai/` 代码。
- `protocolName()` SHALL 返回 `"openai"`
- `supportsPassthrough()` SHALL 返回 true
- `buildHeaders(provider)` SHALL 构建 `Authorization: Bearer <api_key>``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 <provider.api_key>`
- **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.systemString
- **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.argumentsJSON 字符串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** stopString 或 ArraySHALL 规范化为 parameters.stop_sequencesArray
- **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 <20><>息中 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 映射为 CanonicalModelListdata[].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 做字段映射