1181 lines
52 KiB
Markdown
1181 lines
52 KiB
Markdown
# OpenAI 协议适配清单
|
||
|
||
> 依据 [conversion_design.md](./conversion_design.md) 附录 D 模板编撰,覆盖 OpenAI API 的全部对接细节。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [协议基本信息](#1-协议基本信息)
|
||
2. [接口识别](#2-接口识别)
|
||
3. [请求头构建](#3-请求头构建)
|
||
4. [核心层 — Chat 请求编解码](#4-核心层--chat-请求编解码)
|
||
5. [核心层 — Chat 响应编解码](#5-核心层--chat-响应编解码)
|
||
6. [核心层 — 流式编解码](#6-核心层--流式编解码)
|
||
7. [扩展层接口](#7-扩展层接口)
|
||
8. [错误编码](#8-错误编码)
|
||
9. [自检清单](#9-自检清单)
|
||
|
||
---
|
||
|
||
## 1. 协议基本信息
|
||
|
||
| 项目 | 说明 |
|
||
| -------- | ----------------------------------- |
|
||
| 协议名称 | `"openai"` |
|
||
| 协议版本 | 无固定版本头,API 持续演进 |
|
||
| Base URL | `https://api.openai.com` |
|
||
| 认证方式 | `Authorization: Bearer <api_key>` |
|
||
|
||
---
|
||
|
||
## 2. 接口识别
|
||
|
||
### 2.1 URL 路径模式
|
||
|
||
| URL 路径 | InterfaceType |
|
||
| ------------------------ | ------------- |
|
||
| `/v1/chat/completions` | CHAT |
|
||
| `/v1/models` | MODELS |
|
||
| `/v1/models/{model}` | MODEL_INFO |
|
||
| `/v1/embeddings` | EMBEDDINGS |
|
||
| `/v1/rerank` | RERANK |
|
||
|
||
### 2.2 detectInterfaceType
|
||
|
||
```
|
||
OpenAI.detectInterfaceType(nativePath):
|
||
if nativePath == "/v1/chat/completions": return CHAT
|
||
if nativePath == "/v1/models": return MODELS
|
||
if nativePath matches "^/v1/models/[^/]+$": return MODEL_INFO
|
||
if nativePath == "/v1/embeddings": return EMBEDDINGS
|
||
if nativePath == "/v1/rerank": return RERANK
|
||
return PASSTHROUGH
|
||
```
|
||
|
||
**说明**:`detectInterfaceType` 由 OpenAI Adapter 实现,根据 OpenAI 协议的 URL 路径约定识别接口类型。
|
||
|
||
### 2.3 接口能力矩阵
|
||
|
||
```
|
||
OpenAI.supportsInterface(type):
|
||
CHAT: return true
|
||
MODELS: return true
|
||
MODEL_INFO: return true
|
||
EMBEDDINGS: return true
|
||
RERANK: return true
|
||
AUDIO: return true
|
||
IMAGES: return true
|
||
default: return false
|
||
```
|
||
|
||
### 2.4 URL 映射表
|
||
|
||
```
|
||
OpenAI.buildUrl(nativePath, interfaceType):
|
||
switch interfaceType:
|
||
case CHAT: return "/v1/chat/completions"
|
||
case MODELS: return "/v1/models"
|
||
case MODEL_INFO: return "/v1/models/{modelId}"
|
||
case EMBEDDINGS: return "/v1/embeddings"
|
||
case RERANK: return "/v1/rerank"
|
||
default: return nativePath
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 请求头构建
|
||
|
||
### 3.1 buildHeaders
|
||
|
||
```
|
||
OpenAI.buildHeaders(provider):
|
||
result = {}
|
||
result["Authorization"] = "Bearer " + provider.api_key
|
||
if provider.adapter_config["organization"]:
|
||
result["OpenAI-Organization"] = provider.adapter_config["organization"]
|
||
result["Content-Type"] = "application/json"
|
||
return result
|
||
```
|
||
|
||
### 3.2 adapter_config 契约
|
||
|
||
| Key | 类型 | 必填 | 默认值 | 说明 |
|
||
| ---------------- | ------ | ---- | ------ | ------------------------------------------------------ |
|
||
| `organization` | String | 否 | — | OpenAI 组织标识,映射为 `OpenAI-Organization` Header |
|
||
|
||
---
|
||
|
||
## 4. 核心层 — Chat 请求编解码
|
||
|
||
### 4.1 Decoder(OpenAI → Canonical)
|
||
|
||
#### 系统消息
|
||
|
||
OpenAI 支持两种系统指令角色:`system` 和 `developer`(o1 及更新模型推荐使用 `developer`)。两者均提取为 `canonical.system`。
|
||
|
||
```
|
||
decodeSystemPrompt(messages):
|
||
systemMsgs = messages.filter(m => m.role == "system" || m.role == "developer")
|
||
remaining = messages.filter(m => m.role != "system" && m.role != "developer")
|
||
if systemMsgs.length == 0: return {system: None, messages: remaining}
|
||
return {system: systemMsgs.map(m => extractText(m.content)).join("\n\n"), messages: remaining}
|
||
```
|
||
|
||
从 `messages` 数组中提取 `role="system"` 和 `role="developer"` 的消息,合并为 `canonical.system`(String),剩余消息作为 `canonical.messages`。
|
||
|
||
#### 消息角色映射
|
||
|
||
| OpenAI role | Canonical role | 说明 |
|
||
| ------------- | --------------------------- | ------------------------------------------------------ |
|
||
| `system` | 提取为 `canonical.system` | 不进入 messages 数组 |
|
||
| `developer` | 提取为 `canonical.system` | 不进入 messages 数组;o1+ 推荐使用 |
|
||
| `user` | `user` | 直接映射 |
|
||
| `assistant` | `assistant` | 需处理 tool_calls 结构差异 |
|
||
| `tool` | `tool` | tool_call_id → tool_use_id |
|
||
| `function` | `tool` | **已废弃**,转为 tool 角色(见下方废弃字段处理) |
|
||
|
||
#### 内容块解码
|
||
|
||
```
|
||
decodeUserContent(content):
|
||
if content is String: return [{type: "text", text: content}]
|
||
return content.map(part => {
|
||
switch part.type:
|
||
"text" → {type: "text", text: part.text}
|
||
"image_url" → {type: "image", source: {url: part.image_url.url, detail: part.image_url.detail}}
|
||
"input_audio" → {type: "audio", source: {data: part.input_audio.data, format: part.input_audio.format}}
|
||
"file" → {type: "file", source: {file_data: part.file.file_data, file_id: part.file.file_id, filename: part.file.filename}}
|
||
})
|
||
|
||
decodeMessage(msg):
|
||
switch msg.role:
|
||
case "user":
|
||
return {role: "user", content: decodeUserContent(msg.content)}
|
||
case "assistant":
|
||
blocks = []
|
||
if msg.content:
|
||
if msg.content is String:
|
||
blocks.append({type: "text", text: msg.content})
|
||
else:
|
||
blocks.append(...msg.content.filter(p => p.type == "text").map(p => ({type: "text", text: p.text})))
|
||
for refusal in msg.content.filter(p => p.type == "refusal"):
|
||
blocks.append({type: "text", text: refusal.refusal})
|
||
if msg.refusal: blocks.append({type: "text", text: msg.refusal})
|
||
if msg.tool_calls:
|
||
for tc in msg.tool_calls:
|
||
switch tc.type:
|
||
"function" → blocks.append({type: "tool_use", id: tc.id, name: tc.function.name,
|
||
input: JSON.parse(tc.function.arguments)})
|
||
"custom" → blocks.append({type: "tool_use", id: tc.id, name: tc.custom.name,
|
||
input: tc.custom.input})
|
||
if msg.function_call: // 已废弃,兼容处理
|
||
blocks.append({type: "tool_use", id: generateId(), name: msg.function_call.name,
|
||
input: JSON.parse(msg.function_call.arguments)})
|
||
return {role: "assistant", content: blocks}
|
||
case "tool":
|
||
return {role: "tool", content: [{
|
||
type: "tool_result", tool_use_id: msg.tool_call_id,
|
||
content: msg.content is String ? msg.content : extractText(msg.content),
|
||
is_error: false}]}
|
||
case "function": // 已废弃,兼容处理
|
||
return {role: "tool", content: [{
|
||
type: "tool_result", tool_use_id: msg.name,
|
||
content: msg.content, is_error: false}]}
|
||
```
|
||
|
||
**关键差异**:
|
||
|
||
- OpenAI 将 `tool_calls` 放在 message 顶层,Canonical 放在 `content` 数组中作为 `ToolUseBlock`
|
||
- OpenAI 用 `tool_call_id` 标识工具结果,Canonical 用 `tool_use_id`
|
||
- `refusal` 编码为 text block
|
||
- `developer` 角色与 `system` 角色同语义,均提取为 canonical.system
|
||
- 自定义工具(`type: "custom"`)的 `input` 为字符串,Function 工具的 `arguments` 为 JSON 字符串
|
||
|
||
#### 工具定义
|
||
|
||
OpenAI 有两类工具:
|
||
|
||
**Function 工具**(`type: "function"`):
|
||
|
||
| OpenAI | Canonical | 说明 |
|
||
| -------------------------------- | ------------------------ | --------------------------- |
|
||
| `tools[].type: "function"` | — | OpenAI 多一层 function 包装 |
|
||
| `tools[].function.name` | `tools[].name` | 直接映射 |
|
||
| `tools[].function.description` | `tools[].description` | 直接映射 |
|
||
| `tools[].function.parameters` | `tools[].input_schema` | 字段名不同 |
|
||
| `tools[].function.strict` | — | 协议特有,忽略 |
|
||
|
||
**Custom 工具**(`type: "custom"`):无 `input_schema`,使用自定义格式(text/grammar)。不映射为 CanonicalTool,跨协议时丢弃。
|
||
|
||
```
|
||
decodeTools(tools):
|
||
result = []
|
||
for tool in (tools ?? []):
|
||
if tool.type == "function":
|
||
result.append(CanonicalTool {
|
||
name: tool.function.name,
|
||
description: tool.function.description,
|
||
input_schema: tool.function.parameters
|
||
})
|
||
// type == "custom": 跨协议时丢弃
|
||
return result.length > 0 ? result : None
|
||
```
|
||
|
||
#### 工具选择
|
||
|
||
OpenAI `tool_choice` 有多种形态:
|
||
|
||
| OpenAI tool_choice | Canonical ToolChoice | 说明 |
|
||
| --------------------------------------------------------- | ------------------------ | ------------------------------------------------------- |
|
||
| `"auto"` | `{type: "auto"}` | 直接映射 |
|
||
| `"none"` | `{type: "none"}` | 直接映射 |
|
||
| `"required"` | `{type: "any"}` | 语义等价 |
|
||
| `{type: "function", function: {name}}` | `{type: "tool", name}` | 命名工具 |
|
||
| `{type: "custom", custom: {name}}` | `{type: "tool", name}` | 自定义工具 |
|
||
| `{type: "allowed_tools", allowed_tools: {mode, tools}}` | — | 协议特有,降级为 mode 映射(auto→auto, required→any) |
|
||
|
||
```
|
||
decodeToolChoice(tool_choice):
|
||
if tool_choice is String:
|
||
switch tool_choice:
|
||
"auto" → {type: "auto"}
|
||
"none" → {type: "none"}
|
||
"required" → {type: "any"}
|
||
elif tool_choice.type == "function":
|
||
return {type: "tool", name: tool_choice.function.name}
|
||
elif tool_choice.type == "custom":
|
||
return {type: "tool", name: tool_choice.custom.name}
|
||
elif tool_choice.type == "allowed_tools":
|
||
mode = tool_choice.allowed_tools.mode // "auto" or "required"
|
||
return mode == "required" ? {type: "any"} : {type: "auto"}
|
||
```
|
||
|
||
#### 参数映射
|
||
|
||
| OpenAI | Canonical | 说明 |
|
||
| -------------------------- | ------------------------------------- | --------------------------- |
|
||
| `max_completion_tokens` | `parameters.max_tokens` | 优先使用;o-series 模型专用 |
|
||
| `max_tokens` | `parameters.max_tokens` | 已废弃,作为回退 |
|
||
| `temperature` | `parameters.temperature` | 直接映射 |
|
||
| `top_p` | `parameters.top_p` | 直接映射 |
|
||
| `frequency_penalty` | `parameters.frequency_penalty` | 直接映射 |
|
||
| `presence_penalty` | `parameters.presence_penalty` | 直接映射 |
|
||
| `stop` (String or Array) | `parameters.stop_sequences` (Array) | Decoder 规范化为 Array |
|
||
| `stream` | `stream` | 直接映射 |
|
||
|
||
```
|
||
decodeParameters(raw):
|
||
return RequestParameters {
|
||
max_tokens: raw.max_completion_tokens ?? raw.max_tokens,
|
||
temperature: raw.temperature,
|
||
top_p: raw.top_p,
|
||
frequency_penalty: raw.frequency_penalty,
|
||
presence_penalty: raw.presence_penalty,
|
||
stop_sequences: normalizeStop(raw.stop)
|
||
}
|
||
|
||
normalizeStop(stop):
|
||
if stop is String: return [stop]
|
||
if stop is Array: return stop
|
||
return None
|
||
```
|
||
|
||
#### 公共字段
|
||
|
||
| OpenAI | Canonical | 提取规则 |
|
||
| ----------------------- | --------------------- | -------------------- |
|
||
| `user` | `user_id` | 顶层字段,直接提取 |
|
||
| `response_format` | `output_format` | 按类型解码 |
|
||
| `parallel_tool_calls` | `parallel_tool_use` | 布尔值直接映射 |
|
||
| `reasoning_effort` | `thinking` | 映射为 thinking 配置 |
|
||
|
||
```
|
||
decodeOutputFormat(format):
|
||
if format is None: return None
|
||
switch format.type:
|
||
"json_object" → {type: "json_object"}
|
||
"json_schema" → {type: "json_schema", json_schema: format.json_schema}
|
||
"text" → null // 默认格式,无需设置
|
||
|
||
decodeThinking(reasoning_effort):
|
||
if reasoning_effort is None: return None
|
||
if reasoning_effort == "none": return ThinkingConfig {type: "disabled"}
|
||
effort = reasoning_effort == "minimal" ? "low" : reasoning_effort
|
||
return ThinkingConfig {type: "enabled", effort: effort}
|
||
```
|
||
|
||
**`reasoning_effort` 映射说明**:
|
||
|
||
- `"none"` → `thinking.type = "disabled"`(不执行推理)
|
||
- `"minimal"` → `thinking.effort = "low"`(Canonical 无 minimal 级别,降级为 low)
|
||
- `"low"` / `"medium"` / `"high"` / `"xhigh"` → 直接映射
|
||
|
||
#### 废弃字段兼容
|
||
|
||
| 废弃字段 | 替代字段 | Decoder 处理 |
|
||
| ----------------- | --------------- | -------------------------------------------------- |
|
||
| `functions` | `tools` | 转换为 `tools` 格式(`type: "function"` 包装) |
|
||
| `function_call` | `tool_choice` | 转换为 `tool_choice` 格式 |
|
||
|
||
```
|
||
decodeDeprecatedFields(raw):
|
||
// functions → tools(仅当 tools 未设置时)
|
||
if raw.tools is None && raw.functions:
|
||
raw.tools = raw.functions.map(f => ({
|
||
type: "function",
|
||
function: {name: f.name, description: f.description, parameters: f.parameters}}))
|
||
// function_call → tool_choice(仅当 tool_choice 未设置时)
|
||
if raw.tool_choice is None && raw.function_call:
|
||
if raw.function_call == "none": raw.tool_choice = "none"
|
||
elif raw.function_call == "auto": raw.tool_choice = "auto"
|
||
else: raw.tool_choice = {type: "function", function: {name: raw.function_call.name}}
|
||
```
|
||
|
||
#### 协议特有字段
|
||
|
||
| 字段 | 处理方式 |
|
||
| -------------------------------- | ------------------------ |
|
||
| `seed` | 忽略(无跨协议等价语义) |
|
||
| `logprobs` | 忽略 |
|
||
| `top_logprobs` | 忽略 |
|
||
| `logit_bias` | 忽略 |
|
||
| `n` | 忽略(仅支持单选择) |
|
||
| `service_tier` | 忽略 |
|
||
| `store` | 忽略 |
|
||
| `metadata` | 忽略 |
|
||
| `modalities` | 忽略(多模态扩展时启用) |
|
||
| `audio` | 忽略(多模态扩展时启用) |
|
||
| `prediction` | 忽略 |
|
||
| `stream_options` | 忽略 |
|
||
| `safety_identifier` | 忽略 |
|
||
| `prompt_cache_key` | 忽略 |
|
||
| `prompt_cache_retention` | 忽略 |
|
||
| `verbosity` | 忽略 |
|
||
| `web_search_options` | 忽略 |
|
||
| `tools[].function.strict` | 忽略 |
|
||
| `tools[].custom` (custom 工具) | 跨协议时丢弃 |
|
||
|
||
#### 协议约束
|
||
|
||
- `messages` 中 `tool` 角色必须紧接在对应的 `assistant`(含 tool_calls)之后
|
||
- `tool` 消息的 `tool_call_id` 必须与 assistant 消息中的 `tool_calls[].id` 匹配
|
||
- `stream_options.include_usage` 可选,OpenAI 特有
|
||
- `stop` 参数在 o3/o4-mini 等最新推理模型上不可用
|
||
|
||
### 4.2 Encoder(Canonical → OpenAI)
|
||
|
||
#### 模型名称
|
||
|
||
使用 `provider.model_name` 覆盖 `canonical.model`。
|
||
|
||
#### 系统消息注入
|
||
|
||
将 `canonical.system` 编码为 `messages[0].role="system"` 的消息,置于 messages 数组头部。
|
||
|
||
```
|
||
encodeSystemPrompt(canonical):
|
||
messages = []
|
||
if canonical.system is String:
|
||
messages.append({role: "system", content: canonical.system})
|
||
elif canonical.system is Array:
|
||
text = canonical.system.map(s => s.text).join("\n\n")
|
||
messages.append({role: "system", content: text})
|
||
return messages + encodeMessages(canonical.messages)
|
||
```
|
||
|
||
#### 消息编码
|
||
|
||
```
|
||
encodeUserContent(blocks):
|
||
if blocks.length == 1 && blocks[0].type == "text":
|
||
return blocks[0].text
|
||
return blocks.map(b => {
|
||
switch b.type:
|
||
"text" → {type: "text", text: b.text}
|
||
"image" → {type: "image_url", image_url: {url: b.source.url, detail: b.source.detail}}
|
||
"audio" → {type: "input_audio", input_audio: {data: b.source.data, format: b.source.format}}
|
||
"file" → {type: "file", file: {file_data: b.source.file_data, file_id: b.source.file_id, filename: b.source.filename}}
|
||
})
|
||
|
||
encodeMessage(msg):
|
||
switch msg.role:
|
||
case "user":
|
||
return {role: "user", content: encodeUserContent(msg.content)}
|
||
case "assistant":
|
||
message = {}
|
||
textParts = msg.content.filter(b => b.type == "text")
|
||
toolUses = msg.content.filter(b => b.type == "tool_use")
|
||
if textParts.length > 0:
|
||
message.content = textParts.map(b => b.text).join("")
|
||
elif toolUses.length > 0:
|
||
message.content = null
|
||
else:
|
||
message.content = ""
|
||
if toolUses.length > 0:
|
||
message.tool_calls = toolUses.map(tu => ({
|
||
id: tu.id, type: "function",
|
||
function: {name: tu.name, arguments: JSON.stringify(tu.input)}}))
|
||
return {role: "assistant", ...message}
|
||
case "tool":
|
||
results = msg.content.filter(b => b.type == "tool_result")
|
||
if results.length > 0:
|
||
return {role: "tool", tool_call_id: results[0].tool_use_id,
|
||
content: results[0].content}
|
||
```
|
||
|
||
#### 角色约束处理
|
||
|
||
OpenAI 要求 assistant 和 user 角色严格交替。当 Canonical 消息序列中存在连续同角色消息时,需合并为单条消息。
|
||
|
||
#### 工具编码
|
||
|
||
```
|
||
encodeTools(canonical):
|
||
if canonical.tools:
|
||
result.tools = canonical.tools.map(t => ({
|
||
type: "function",
|
||
function: {name: t.name, description: t.description, parameters: t.input_schema}}))
|
||
if canonical.tool_choice:
|
||
result.tool_choice = encodeToolChoice(canonical.tool_choice)
|
||
|
||
encodeToolChoice(choice):
|
||
switch choice.type:
|
||
"auto" → "auto"
|
||
"none" → "none"
|
||
"any" → "required"
|
||
"tool" → {type: "function", function: {name: choice.name}}
|
||
```
|
||
|
||
#### 公共字段编码
|
||
|
||
```
|
||
encodeOutputFormat(format):
|
||
if format is None: return None
|
||
switch format.type:
|
||
"json_object" → {type: "json_object"}
|
||
"json_schema" → {type: "json_schema", json_schema: format.json_schema}
|
||
|
||
encodeRequest(canonical, provider):
|
||
result = {
|
||
model: provider.model_name,
|
||
messages: encodeSystemPrompt(canonical) + canonical.messages.flatMap(encodeMessage),
|
||
stream: canonical.stream
|
||
}
|
||
|
||
// 参数
|
||
if canonical.parameters.max_tokens:
|
||
result.max_completion_tokens = canonical.parameters.max_tokens
|
||
if canonical.parameters.temperature is not None:
|
||
result.temperature = canonical.parameters.temperature
|
||
if canonical.parameters.top_p is not None:
|
||
result.top_p = canonical.parameters.top_p
|
||
if canonical.parameters.frequency_penalty is not None:
|
||
result.frequency_penalty = canonical.parameters.frequency_penalty
|
||
if canonical.parameters.presence_penalty is not None:
|
||
result.presence_penalty = canonical.parameters.presence_penalty
|
||
if canonical.parameters.stop_sequences:
|
||
result.stop = canonical.parameters.stop_sequences
|
||
|
||
// 工具
|
||
if canonical.tools:
|
||
result.tools = canonical.tools.map(t => ({
|
||
type: "function",
|
||
function: {name: t.name, description: t.description, parameters: t.input_schema}}))
|
||
if canonical.tool_choice:
|
||
result.tool_choice = encodeToolChoice(canonical.tool_choice)
|
||
|
||
// 公共字段
|
||
if canonical.user_id:
|
||
result.user = canonical.user_id
|
||
if canonical.output_format:
|
||
result.response_format = encodeOutputFormat(canonical.output_format)
|
||
if canonical.parallel_tool_use != null:
|
||
result.parallel_tool_calls = canonical.parallel_tool_use
|
||
if canonical.thinking:
|
||
if canonical.thinking.type == "disabled":
|
||
result.reasoning_effort = "none"
|
||
elif canonical.thinking.effort:
|
||
result.reasoning_effort = canonical.thinking.effort
|
||
else:
|
||
result.reasoning_effort = "medium"
|
||
return result
|
||
```
|
||
|
||
**编码说明**:
|
||
|
||
- 使用 `max_completion_tokens`(非废弃的 `max_tokens`)输出 token 上限
|
||
- `frequency_penalty` 和 `presence_penalty` 仅在非 null 时输出
|
||
- `thinking` 映射为 `reasoning_effort`:disabled → "none",有 effort 值则直接映射,否则默认 "medium"
|
||
|
||
#### 降级处理
|
||
|
||
对照架构文档 §8.4 三级降级策略,确认每个不支持字段的处理:
|
||
|
||
| Canonical 字段 | OpenAI 不支持时 | 降级策略 |
|
||
| --------------------------------- | -------------------------------------------------- | ---------------------------------- |
|
||
| `thinking.budget_tokens` | OpenAI 使用 `reasoning_effort` 而非 token 级控制 | 替代方案:估算映射为 effort 近似值 |
|
||
| `stop_reason: "content_filter"` | `finish_reason: "content_filter"` | 自动映射(OpenAI 支持此值) |
|
||
| `stop_reason: "stop_sequence"` | OpenAI 无独立值 | 自动映射为 `"stop"` |
|
||
| `parameters.top_k` | OpenAI 不支持 `top_k` | 丢弃 |
|
||
|
||
---
|
||
|
||
## 5. 核心层 — Chat 响应编解码
|
||
|
||
逐字段对照 §4.7 CanonicalResponse 确认映射关系。
|
||
|
||
### 5.1 响应结构
|
||
|
||
```
|
||
OpenAI 响应顶层结构:
|
||
{
|
||
id: String,
|
||
object: "chat.completion",
|
||
created: Number,
|
||
model: String,
|
||
choices: [{
|
||
index: 0,
|
||
message: {
|
||
role: "assistant",
|
||
content: String | null,
|
||
refusal: String | null,
|
||
tool_calls: [{
|
||
id: String,
|
||
type: "function",
|
||
function: { name: String, arguments: String }
|
||
}] | null,
|
||
annotations: [{
|
||
type: "url_citation",
|
||
url_citation: { start_index, end_index, title, url }
|
||
}] | null,
|
||
audio: { id, data, expires_at, transcript } | null
|
||
},
|
||
finish_reason: String,
|
||
logprobs: { content, refusal } | null
|
||
}],
|
||
usage: {
|
||
prompt_tokens: Number,
|
||
completion_tokens: Number,
|
||
total_tokens: Number,
|
||
prompt_tokens_details: { cached_tokens, audio_tokens },
|
||
completion_tokens_details: { reasoning_tokens, audio_tokens, accepted_prediction_tokens, rejected_prediction_tokens }
|
||
},
|
||
service_tier: String | null,
|
||
system_fingerprint: String | null
|
||
}
|
||
```
|
||
|
||
**兼容性说明**:部分 OpenAI 兼容提供商(如 DeepSeek)在 response 中返回非标准的 `reasoning_content` 字段。Decoder 会检测并处理此字段,将其解码为 ThinkingBlock。
|
||
|
||
### 5.2 Decoder(OpenAI → Canonical)
|
||
|
||
```
|
||
decodeResponse(openaiResp):
|
||
choice = openaiResp.choices[0]
|
||
blocks = []
|
||
if choice.message.content: blocks.append({type: "text", text: choice.message.content})
|
||
if choice.message.refusal: blocks.append({type: "text", text: choice.message.refusal})
|
||
|
||
// reasoning_content: 非标准字段,来自兼容提供商
|
||
if choice.message.reasoning_content:
|
||
blocks.append({type: "thinking", thinking: choice.message.reasoning_content})
|
||
|
||
if choice.message.tool_calls:
|
||
for tc in choice.message.tool_calls:
|
||
switch tc.type:
|
||
"function" → blocks.append({type: "tool_use", id: tc.id, name: tc.function.name,
|
||
input: JSON.parse(tc.function.arguments)})
|
||
"custom" → blocks.append({type: "tool_use", id: tc.id, name: tc.custom.name,
|
||
input: tc.custom.input})
|
||
return CanonicalResponse {
|
||
id: openaiResp.id,
|
||
model: openaiResp.model,
|
||
content: blocks,
|
||
stop_reason: mapFinishReason(choice.finish_reason),
|
||
usage: decodeUsage(openaiResp.usage)
|
||
}
|
||
```
|
||
|
||
**内容块解码**:
|
||
|
||
- `content` → TextBlock
|
||
- `refusal` → TextBlock
|
||
- `reasoning_content` → ThinkingBlock(非标准字段,来自兼容提供商)
|
||
- `tool_calls[].type: "function"` → ToolUseBlock(从 message 顶层提取到 content 数组)
|
||
- `tool_calls[].type: "custom"` → ToolUseBlock(input 为字符串)
|
||
|
||
**停止原因映射**:
|
||
|
||
| OpenAI finish_reason | Canonical stop_reason | 说明 |
|
||
| -------------------- | --------------------- | ---------------------------- |
|
||
| `"stop"` | `"end_turn"` | 自然结束或匹配 stop sequence |
|
||
| `"length"` | `"max_tokens"` | 达到 token 上限 |
|
||
| `"tool_calls"` | `"tool_use"` | 模型调用工具 |
|
||
| `"content_filter"` | `"content_filter"` | 内容过滤 |
|
||
| `"function_call"` | `"tool_use"` | 已废弃,等同于 tool_calls |
|
||
| 其他 | `"end_turn"` | 兜底 |
|
||
|
||
**Token 用量映射**:
|
||
|
||
| OpenAI usage | Canonical Usage |
|
||
| ---------------------------------------------- | -------------------------------- |
|
||
| `prompt_tokens` | `input_tokens` |
|
||
| `completion_tokens` | `output_tokens` |
|
||
| `prompt_tokens_details.cached_tokens` | `cache_read_tokens` |
|
||
| — | `cache_creation_tokens` (null) |
|
||
| `completion_tokens_details.reasoning_tokens` | `reasoning_tokens` |
|
||
|
||
```
|
||
decodeUsage(usage):
|
||
if usage is None: return CanonicalUsage {input_tokens: 0, output_tokens: 0}
|
||
return CanonicalUsage {
|
||
input_tokens: usage.prompt_tokens,
|
||
output_tokens: usage.completion_tokens,
|
||
cache_read_tokens: usage.prompt_tokens_details?.cached_tokens,
|
||
cache_creation_tokens: null,
|
||
reasoning_tokens: usage.completion_tokens_details?.reasoning_tokens
|
||
}
|
||
```
|
||
|
||
**协议特有内容**:
|
||
|
||
| 字段 | 处理方式 |
|
||
| ---------------------- | ---------------------------------------------- |
|
||
| `refusal` | 解码为 text block |
|
||
| `reasoning_content` | 解码为 ThinkingBlock(非标准,来自兼容提供商) |
|
||
| `annotations` | 忽略(协议特有,不晋升为公共字段) |
|
||
| `audio` | 忽略(多模态扩展时启用) |
|
||
| `logprobs` | 忽略 |
|
||
| `service_tier` | 忽略 |
|
||
| `system_fingerprint` | 忽略 |
|
||
| `created` | 忽略 |
|
||
|
||
### 5.3 Encoder(Canonical → OpenAI)
|
||
|
||
```
|
||
encodeResponse(canonical):
|
||
textParts = canonical.content.filter(b => b.type == "text")
|
||
thinkingParts = canonical.content.filter(b => b.type == "thinking")
|
||
toolUses = canonical.content.filter(b => b.type == "tool_use")
|
||
|
||
message = {role: "assistant"}
|
||
if textParts.length > 0:
|
||
message.content = textParts.map(b => b.text).join("")
|
||
elif toolUses.length > 0:
|
||
message.content = null
|
||
else:
|
||
message.content = ""
|
||
|
||
// reasoning_content: 非标准字段,输出给兼容提供商
|
||
if thinkingParts.length > 0:
|
||
message.reasoning_content = thinkingParts.map(b => b.thinking).join("")
|
||
|
||
if toolUses.length > 0:
|
||
message.tool_calls = toolUses.map(tu => ({
|
||
id: tu.id, type: "function",
|
||
function: {name: tu.name, arguments: JSON.stringify(tu.input)}}))
|
||
|
||
return {
|
||
id: canonical.id,
|
||
object: "chat.completion",
|
||
model: canonical.model,
|
||
choices: [{
|
||
index: 0,
|
||
message: message,
|
||
finish_reason: mapCanonicalToFinishReason(canonical.stop_reason)
|
||
}],
|
||
usage: encodeUsage(canonical.usage)
|
||
}
|
||
|
||
encodeUsage(usage):
|
||
return {
|
||
prompt_tokens: usage.input_tokens,
|
||
completion_tokens: usage.output_tokens,
|
||
total_tokens: usage.input_tokens + usage.output_tokens,
|
||
prompt_tokens_details: usage.cache_read_tokens ? {cached_tokens: usage.cache_read_tokens} : null,
|
||
completion_tokens_details: usage.reasoning_tokens ? {reasoning_tokens: usage.reasoning_tokens} : null
|
||
}
|
||
```
|
||
|
||
**内容块编码**:
|
||
|
||
- TextBlock → `message.content`
|
||
- ToolUseBlock → `message.tool_calls`(从 content 数组提取到 message 顶层)
|
||
- ThinkingBlock → `reasoning_content`(非标准字段,兼容提供商使用)
|
||
|
||
**停止原因映射**:
|
||
|
||
| Canonical stop_reason | OpenAI finish_reason |
|
||
| --------------------- | -------------------- |
|
||
| `"end_turn"` | `"stop"` |
|
||
| `"max_tokens"` | `"length"` |
|
||
| `"tool_use"` | `"tool_calls"` |
|
||
| `"content_filter"` | `"content_filter"` |
|
||
| `"stop_sequence"` | `"stop"` |
|
||
| `"refusal"` | `"stop"` |
|
||
| 其他 | `"stop"` |
|
||
|
||
**降级处理**:
|
||
|
||
| Canonical 字段 | OpenAI 不支持时 | 降级策略 |
|
||
| -------------------------------- | --------------- | ----------------------------- |
|
||
| `stop_reason: "stop_sequence"` | OpenAI 无独立值 | 映射为 `"stop"`(自动映射) |
|
||
| `stop_reason: "refusal"` | OpenAI 无独立值 | 映射为 `"stop"`(自动映射) |
|
||
| `cache_creation_tokens` | OpenAI 无此字段 | 丢弃 |
|
||
|
||
---
|
||
|
||
## 6. 核心层 — 流式编解码
|
||
|
||
### 6.1 SSE 格式
|
||
|
||
OpenAI 使用无命名的 SSE delta chunk:
|
||
|
||
```
|
||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4",
|
||
"choices":[{"index":0,"delta":{...},"finish_reason":null}]}
|
||
|
||
data: [DONE]
|
||
```
|
||
|
||
**delta 结构**:
|
||
|
||
```
|
||
delta: {
|
||
role?: "assistant" | "user" | "system" | "developer" | "tool",
|
||
content?: String,
|
||
tool_calls?: [{index: Number, id?: String, function?: {name?: String, arguments?: String}, type?: "function"}],
|
||
refusal?: String,
|
||
function_call?: {name?: String, arguments?: String} // 已废弃
|
||
}
|
||
```
|
||
|
||
**兼容性说明**:部分兼容提供商在 delta 中返回非标准的 `reasoning_content` 字段。StreamDecoder 会检测并处理。
|
||
|
||
### 6.2 StreamDecoder(OpenAI SSE → Canonical 事件)
|
||
|
||
| OpenAI chunk | Canonical 事件 | 说明 |
|
||
| ------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------- |
|
||
| 首个 chunk (id/model) | MessageStartEvent | 从顶层字段提取 id、model |
|
||
| `delta.content` 首次出现 | ContentBlockStart(text) + ContentBlockDelta(text_delta) | 新 text block 开始 |
|
||
| `delta.content` 后续 | ContentBlockDelta(text_delta) | 追加文本 |
|
||
| `delta.tool_calls[i]` 首次出现 | ContentBlockStart(tool_use) | 新 tool block,提取 id、name |
|
||
| `delta.tool_calls[i].function.arguments` | ContentBlockDelta(input_json_delta) | 增量 JSON 参数 |
|
||
| `delta.reasoning_content` 首次 | ContentBlockStart(thinking) + ContentBlockDelta(thinking_delta) | 新 thinking block(非标准,来自兼容提供商) |
|
||
| `delta.reasoning_content` 后续 | ContentBlockDelta(thinking_delta) | 追加思考内容 |
|
||
| `delta.refusal` 首次 | ContentBlockStart(text) + ContentBlockDelta(text_delta) | 新 text block |
|
||
| `finish_reason` 非空 | ContentBlockStop × N + MessageDeltaEvent + MessageStopEvent | 关闭所有 open blocks |
|
||
| usage chunk(choices=[]) | MessageDeltaEvent(usage) | `stream_options.include_usage` 触发的用量 chunk |
|
||
| `[DONE]` | flush() | 触发 decoder flush |
|
||
|
||
**Decoder 伪代码**:
|
||
|
||
```
|
||
StreamDecoder.processChunk(rawChunk):
|
||
events = []
|
||
|
||
// 解析 SSE data
|
||
if rawChunk == "[DONE]":
|
||
// 关闭所有 open blocks
|
||
for idx in openBlocks:
|
||
events.append(ContentBlockStopEvent {index: idx})
|
||
if messageStarted:
|
||
events.append(MessageStopEvent {})
|
||
return events
|
||
|
||
data = JSON.parse(rawChunk)
|
||
|
||
// 首个 chunk: MessageStart
|
||
if !messageStarted:
|
||
events.append(MessageStartEvent {message: {id: data.id, model: data.model, usage: null}})
|
||
messageStarted = true
|
||
|
||
for choice in data.choices:
|
||
delta = choice.delta
|
||
|
||
// role 出现时不产生事件(仅用于首个 chunk 标记)
|
||
|
||
// text content
|
||
if delta.content != null:
|
||
if !openBlocks.has(textBlockIndex):
|
||
events.append(ContentBlockStartEvent {index: textBlockIndex, content_block: {type: "text", text: ""}})
|
||
openBlocks.add(textBlockIndex)
|
||
currentBlockType[textBlockIndex] = "text"
|
||
events.append(ContentBlockDeltaEvent {index: textBlockIndex, delta: {type: "text_delta", text: delta.content}})
|
||
|
||
// reasoning_content (非标准,来自兼容提供商)
|
||
if delta.reasoning_content != null:
|
||
if !openBlocks.has(thinkingBlockIndex):
|
||
events.append(ContentBlockStartEvent {index: thinkingBlockIndex, content_block: {type: "thinking", thinking: ""}})
|
||
openBlocks.add(thinkingBlockIndex)
|
||
currentBlockType[thinkingBlockIndex] = "thinking"
|
||
events.append(ContentBlockDeltaEvent {index: thinkingBlockIndex, delta: {type: "thinking_delta", thinking: delta.reasoning_content}})
|
||
|
||
// refusal
|
||
if delta.refusal != null:
|
||
if !openBlocks.has(refusalBlockIndex):
|
||
events.append(ContentBlockStartEvent {index: refusalBlockIndex, content_block: {type: "text", text: ""}})
|
||
openBlocks.add(refusalBlockIndex)
|
||
events.append(ContentBlockDeltaEvent {index: refusalBlockIndex, delta: {type: "text_delta", text: delta.refusal}})
|
||
|
||
// tool calls
|
||
if delta.tool_calls:
|
||
for tc in delta.tool_calls:
|
||
idx = tc.index
|
||
if tc.id != null:
|
||
// 新 tool call block
|
||
toolCallIdMap[idx] = tc.id
|
||
toolCallNameMap[idx] = tc.function?.name
|
||
toolCallArguments[idx] = ""
|
||
blockIndex = allocateBlockIndex(idx)
|
||
events.append(ContentBlockStartEvent {
|
||
index: blockIndex,
|
||
content_block: {type: "tool_use", id: tc.id, name: tc.function?.name, input: {}}})
|
||
openBlocks.add(blockIndex)
|
||
currentBlockType[blockIndex] = "tool_use"
|
||
currentBlockId[blockIndex] = idx
|
||
if tc.function?.arguments:
|
||
toolCallArguments[currentBlockId[toolUseBlockIndex]] += tc.function.arguments
|
||
events.append(ContentBlockDeltaEvent {
|
||
index: toolUseBlockIndex,
|
||
delta: {type: "input_json_delta", partial_json: tc.function.arguments}})
|
||
|
||
// finish_reason
|
||
if choice.finish_reason != null:
|
||
for idx in openBlocks:
|
||
events.append(ContentBlockStopEvent {index: idx})
|
||
openBlocks.clear()
|
||
events.append(MessageDeltaEvent {delta: {stop_reason: mapFinishReason(choice.finish_reason)}, usage: null})
|
||
events.append(MessageStopEvent {})
|
||
|
||
// usage chunk (choices 为空)
|
||
if data.choices.length == 0 && data.usage:
|
||
accumulatedUsage = decodeUsage(data.usage)
|
||
events.append(MessageDeltaEvent {delta: {stop_reason: null}, usage: accumulatedUsage})
|
||
|
||
return events
|
||
```
|
||
|
||
### 6.3 StreamDecoder 状态机
|
||
|
||
```
|
||
StreamDecoderState {
|
||
messageStarted: Boolean
|
||
openBlocks: Set<Integer>
|
||
currentBlockType: Map<Integer, String>
|
||
currentBlockId: Map<Integer, String>
|
||
toolCallIdMap: Map<Integer, String> // OpenAI tool_calls 数组索引 → id
|
||
toolCallNameMap: Map<Integer, String> // OpenAI tool_calls 数组索引 → name
|
||
toolCallArguments: Map<Integer, StringBuffer> // OpenAI tool_calls 数组索引 → 累积参数
|
||
textBlockStarted: Boolean // 追踪 text block 生命周期
|
||
thinkingBlockStarted: Boolean // 追踪 thinking block 生命周期(非标准)
|
||
utf8Remainder: Option<ByteArray> // UTF-8 跨 chunk 安全
|
||
accumulatedUsage: Option<CanonicalUsage>
|
||
}
|
||
```
|
||
|
||
**关键处理**:
|
||
|
||
- **工具调用索引映射**:OpenAI `tool_calls[i]` 的 `i` 不一定是连续的,需要用 Map 维护索引到 id/name 的映射
|
||
- **参数累积**:`tool_calls[i].function.arguments` 是增量 JSON 片段,需要累积直到 block 结束
|
||
- **UTF-8 安全**:跨 chunk 截断的 UTF-8 字节需要用 `utf8Remainder` 缓冲
|
||
- **`reasoning_content`**:非标准字段,来自兼容提供商(如 DeepSeek),处理方式与 `content` 类似
|
||
- **usage chunk**:当 `stream_options.include_usage` 启用时,最后一个 chunk 的 `choices` 为空数组,仅含 `usage`
|
||
|
||
### 6.4 StreamEncoder(Canonical → OpenAI SSE)
|
||
|
||
| Canonical 事件 | OpenAI chunk | 说明 |
|
||
| ----------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------- |
|
||
| MessageStartEvent | `{id, model, object: "chat.completion.chunk", choices:[{delta:{role:"assistant"}, index:0}]}` | 首个 chunk |
|
||
| ContentBlockStart(text) | 缓冲,不输出 | 等待首次 delta 时合并输出 |
|
||
| ContentBlockDelta(text_delta) | `{choices:[{delta:{content:"..."}}]}` | 首次输出时合并 block_start 信息 |
|
||
| ContentBlockStart(tool_use) | 缓冲,不输出 | 等待首次 delta 时合并输出 |
|
||
| ContentBlockDelta(input_json_delta) | `{choices:[{delta:{tool_calls:[{index, id?, function:{name?, arguments}}]}}]}` | 首次含 id 和 name,后续仅含 arguments |
|
||
| ContentBlockStart(thinking) | 缓冲,不输出 | 等待首次 delta |
|
||
| ContentBlockDelta(thinking_delta) | `{choices:[{delta:{reasoning_content:"..."}}]}` | 非标准字段(兼容提供商使用) |
|
||
| ContentBlockStop | 不输出 | 静默 |
|
||
| MessageDeltaEvent | `{choices:[{delta:{}, finish_reason:"..."}]}` | 包含 stop_reason 映射 |
|
||
| MessageDeltaEvent(usage only) | `{choices:[], usage: {...}}` | 用量信息 chunk |
|
||
| MessageStopEvent | `data: [DONE]` | 流结束 |
|
||
| PingEvent | 丢弃 | 不输出 |
|
||
| ErrorEvent | 丢弃 | 不输出(OpenAI 无流式错误事件) |
|
||
|
||
**Encoder 伪代码**:
|
||
|
||
```
|
||
StreamEncoderState {
|
||
bufferedStart: Option<ContentBlockStartEvent> // 缓冲的 block start 事件
|
||
toolCallIndexMap: Map<String, Integer> // tool_use_id → OpenAI tool_calls 数组索引
|
||
nextToolCallIndex: Integer // 下一个可用索引
|
||
}
|
||
|
||
StreamEncoder.encodeEvent(event):
|
||
switch event.type:
|
||
case "message_start":
|
||
return [{id: event.message.id, model: event.message.model,
|
||
object: "chat.completion.chunk", created: now(),
|
||
choices: [{index: 0, delta: {role: "assistant"}, finish_reason: null}]}]
|
||
|
||
case "content_block_start":
|
||
bufferedStart = event // 缓冲,不立即输出
|
||
if event.content_block.type == "tool_use":
|
||
idx = nextToolCallIndex++
|
||
toolCallIndexMap[event.content_block.id] = idx
|
||
return []
|
||
|
||
case "content_block_delta":
|
||
chunks = []
|
||
switch event.delta.type:
|
||
"text_delta":
|
||
delta = {content: event.delta.text}
|
||
if bufferedStart:
|
||
// 首次 delta,合并 start 信息(OpenAI 不需要额外的 start 信息)
|
||
bufferedStart = null
|
||
chunks.append({choices: [{index: 0, delta: delta, finish_reason: null}]})
|
||
|
||
"input_json_delta":
|
||
tcIdx = toolCallIndexMap[currentBlockId[event.index]]
|
||
delta = {}
|
||
if bufferedStart:
|
||
// 首次 delta,含 id 和 name
|
||
start = bufferedStart.content_block
|
||
delta.tool_calls = [{index: tcIdx, id: start.id,
|
||
function: {name: start.name, arguments: event.delta.partial_json},
|
||
type: "function"}]
|
||
bufferedStart = null
|
||
else:
|
||
delta.tool_calls = [{index: tcIdx,
|
||
function: {arguments: event.delta.partial_json}}]
|
||
chunks.append({choices: [{index: 0, delta: delta, finish_reason: null}]})
|
||
|
||
"thinking_delta":
|
||
delta = {reasoning_content: event.delta.thinking}
|
||
if bufferedStart:
|
||
bufferedStart = null
|
||
chunks.append({choices: [{index: 0, delta: delta, finish_reason: null}]})
|
||
|
||
return chunks
|
||
|
||
case "content_block_stop":
|
||
return []
|
||
|
||
case "message_delta":
|
||
chunks = []
|
||
if event.delta.stop_reason:
|
||
finishReason = mapCanonicalToFinishReason(event.delta.stop_reason)
|
||
chunks.append({choices: [{index: 0, delta: {}, finish_reason: finishReason}]})
|
||
if event.usage:
|
||
chunks.append({choices: [], usage: encodeUsage(event.usage)})
|
||
return chunks
|
||
|
||
case "message_stop":
|
||
return ["[DONE]"]
|
||
```
|
||
|
||
**缓冲策略**:
|
||
|
||
- `ContentBlockStart` 不立即输出,等待首次 `ContentBlockDelta` 合并
|
||
- 合并首次 delta 时,将 start 信息(如 tool id/name)一起编码
|
||
|
||
---
|
||
|
||
## 7. 扩展层接口
|
||
|
||
### 7.1 /models & /models/
|
||
|
||
**列表接口** `GET /v1/models`:
|
||
|
||
| 项目 | 说明 |
|
||
| ------------ | ----------------- |
|
||
| 接口是否存在 | 是 |
|
||
| 请求格式 | GET 请求,无 body |
|
||
|
||
响应 Decoder(OpenAI → Canonical):
|
||
|
||
```
|
||
decodeModelsResponse(openaiResp):
|
||
return CanonicalModelList {
|
||
models: openaiResp.data.map(m => CanonicalModel {
|
||
id: m.id, name: m.id, created: m.created, owned_by: m.owned_by })}
|
||
```
|
||
|
||
响应 Encoder(Canonical → OpenAI):
|
||
|
||
```
|
||
encodeModelsResponse(canonical):
|
||
return {object: "list",
|
||
data: canonical.models.map(m => ({id: m.id, object: "model",
|
||
created: m.created ?? 0, owned_by: m.owned_by ?? "unknown"}))}
|
||
```
|
||
|
||
**详情接口** `GET /v1/models/{model}`:
|
||
|
||
| 项目 | 说明 |
|
||
| ------------ | ----------------- |
|
||
| 接口是否存在 | 是 |
|
||
| 请求格式 | GET 请求,无 body |
|
||
|
||
响应 Decoder(OpenAI → Canonical):
|
||
|
||
```
|
||
decodeModelInfoResponse(openaiResp):
|
||
return CanonicalModelInfo {
|
||
id: openaiResp.id, name: openaiResp.id,
|
||
created: openaiResp.created, owned_by: openaiResp.owned_by }
|
||
```
|
||
|
||
响应 Encoder(Canonical → OpenAI):
|
||
|
||
```
|
||
encodeModelInfoResponse(canonical):
|
||
return {id: canonical.id, object: "model",
|
||
created: canonical.created ?? 0, owned_by: canonical.owned_by ?? "unknown"}
|
||
```
|
||
|
||
**字段映射**(列表和详情共用):
|
||
|
||
| OpenAI | Canonical | 说明 |
|
||
| -------------------------- | --------------------- | ----------- |
|
||
| `data[].id` | `models[].id` | 直接映射 |
|
||
| `data[].object: "model"` | — | 固定值 |
|
||
| `data[].created` | `models[].created` | Unix 时间戳 |
|
||
| `data[].owned_by` | `models[].owned_by` | 直接映射 |
|
||
|
||
### 7.2 /embeddings
|
||
|
||
| 项目 | 说明 |
|
||
| ------------ | ----------------------- |
|
||
| 接口是否存在 | 是 |
|
||
| URL 路径 | `POST /v1/embeddings` |
|
||
|
||
**请求 Decoder**(OpenAI → Canonical):
|
||
|
||
```
|
||
decodeEmbeddingRequest(raw):
|
||
return CanonicalEmbeddingRequest {
|
||
model: raw.model,
|
||
input: raw.input,
|
||
encoding_format: raw.encoding_format,
|
||
dimensions: raw.dimensions
|
||
}
|
||
```
|
||
|
||
**请求 Encoder**(Canonical → OpenAI):
|
||
|
||
```
|
||
encodeEmbeddingRequest(canonical, provider):
|
||
result = {model: provider.model_name, input: canonical.input}
|
||
if canonical.encoding_format: result.encoding_format = canonical.encoding_format
|
||
if canonical.dimensions: result.dimensions = canonical.dimensions
|
||
return result
|
||
```
|
||
|
||
**响应 Decoder**(OpenAI → Canonical):
|
||
|
||
```
|
||
decodeEmbeddingResponse(openaiResp):
|
||
return CanonicalEmbeddingResponse {
|
||
data: openaiResp.data, model: openaiResp.model, usage: openaiResp.usage }
|
||
```
|
||
|
||
**响应 Encoder**(Canonical → OpenAI):
|
||
|
||
```
|
||
encodeEmbeddingResponse(canonical):
|
||
return {object: "list", data: canonical.data, model: canonical.model, usage: canonical.usage}
|
||
```
|
||
|
||
### 7.3 /rerank
|
||
|
||
| 项目 | 说明 |
|
||
| ------------ | ------------------- |
|
||
| 接口是否存在 | 是 |
|
||
| URL 路径 | `POST /v1/rerank` |
|
||
|
||
**请求/响应编解码**:按 CanonicalRerankRequest / CanonicalRerankResponse 格式映射。
|
||
|
||
---
|
||
|
||
## 8. 错误编码
|
||
|
||
### 8.1 错误响应格式
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"message": "Error message",
|
||
"type": "invalid_request_error",
|
||
"param": null,
|
||
"code": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 encodeError
|
||
|
||
```
|
||
OpenAI.encodeError(error):
|
||
return {
|
||
error: {
|
||
message: error.message,
|
||
type: mapErrorCode(error.code),
|
||
param: error.details?.param ?? null,
|
||
code: error.code
|
||
}
|
||
}
|
||
|
||
mapErrorCode(code):
|
||
switch code:
|
||
INVALID_INPUT → "invalid_request_error"
|
||
MISSING_REQUIRED_FIELD → "invalid_request_error"
|
||
INCOMPATIBLE_FEATURE → "invalid_request_error"
|
||
TOOL_CALL_PARSE_ERROR → "invalid_request_error"
|
||
JSON_PARSE_ERROR → "invalid_request_error"
|
||
RATE_LIMIT → "rate_limit_error"
|
||
AUTHENTICATION → "authentication_error"
|
||
default → "server_error"
|
||
```
|
||
|
||
### 8.3 常用 HTTP 状态码
|
||
|
||
| HTTP Status | 说明 |
|
||
| ----------- | -------------- |
|
||
| 400 | 请求格式错误 |
|
||
| 401 | 认证失败 |
|
||
| 403 | 无权限 |
|
||
| 404 | 接口不存在 |
|
||
| 429 | 速率限制 |
|
||
| 500 | 服务器内部错误 |
|
||
| 503 | 服务不可用 |
|
||
|
||
---
|
||
|
||
## 9. 自检清单
|
||
|
||
| 章节 | 检查项 |
|
||
| ---- | -------------------------------------------------------------------------------------------- |
|
||
| §2 | [x] `detectInterfaceType(nativePath)` 已实现,所有已知路径已覆盖 |
|
||
| §2 | [x] 所有 InterfaceType 的 `supportsInterface` 返回值已确定 |
|
||
| §2 | [x] 所有 InterfaceType 的 `buildUrl` 映射已确定 |
|
||
| §3 | [x]`buildHeaders(provider)` 已实现,adapter_config 契约已文档化 |
|
||
| §4 | [x] Chat 请求的 Decoder 和 Encoder 已实现(逐字段对照 §4.1/§4.2) |
|
||
| §4 | [x] 系统消息提取包含 `system` 和 `developer` 两种角色 |
|
||
| §4 | [x] 角色映射和消息顺序约束已处理(assistant/user 交替合并) |
|
||
| §4 | [x] 工具调用(tool_calls / tool_use / tool_result)的编解码已处理(含 custom 类型) |
|
||
| §4 | [x]`frequency_penalty` 和 `presence_penalty` 已映射到 Canonical(非忽略) |
|
||
| §4 | [x]`max_completion_tokens` 和 `max_tokens` 的优先级已处理 |
|
||
| §4 | [x]`reasoning_effort` 到 `thinking` 的映射已处理(含 "none" 和 "minimal") |
|
||
| §4 | [x] 废弃字段(functions / function_call)的兼容处理已实现 |
|
||
| §4 | [x] 协议特有字段已识别并确定处理方式(logprobs/n/seed/modalities/web_search_options 等忽略) |
|
||
| §5 | [x] Chat 响应的 Decoder 和 Encoder 已实现(逐字段对照 §4.7) |
|
||
| §5 | [x] stop_reason / finish_reason 映射表已确认 |
|
||
| §5 | [x] usage 字段映射已确认(prompt_tokens ↔ input_tokens) |
|
||
| §5 | [x]`reasoning_content`(非标准)的编解码已处理 |
|
||
| §5 | [x]`annotations` 等协议特有响应字段已识别并确定处理方式 |
|
||
| §6 | [x] 流式 StreamDecoder 和 StreamEncoder 已实现(对照 §4.8) |
|
||
| §6 | [x] 流式 `reasoning_content`(非标准)的处理已覆盖 |
|
||
| §6 | [x] usage chunk(choices 为空)的处理已覆盖 |
|
||
| §7 | [x] 扩展层接口的编解码已实现(/models、/models/{model}、/embeddings、/rerank) |
|
||
| §8 | [x]`encodeError` 已实现 |
|