787 lines
29 KiB
Markdown
787 lines
29 KiB
Markdown
# Anthropic 协议适配清单
|
||
|
||
> 依据 [conversion_design.md](./conversion_design.md) 附录 D 模板编撰,覆盖 Anthropic API 的全部对接细节。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [协议基本信息](#1-协议基本信息)
|
||
2. [接口识别](#2-接口识别)
|
||
3. [请求头构建](#3-请求头构建)
|
||
4. [核心层 — Chat 请求编解码](#4-核心层--chat-请求编解码)
|
||
5. [核心层 — Chat 响应编解码](#5-核心层--chat-响应编解码)
|
||
6. [核心层 — 流式编解码](#6-核心层--流式编解码)
|
||
7. [扩展层接口](#7-扩展层接口)
|
||
8. [错误编码](#8-错误编码)
|
||
9. [自检清单](#9-自检清单)
|
||
|
||
---
|
||
|
||
## 1. 协议基本信息
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 协议名称 | `"anthropic"` |
|
||
| 协议版本 | `2023-06-01`(通过 `anthropic-version` Header 传递) |
|
||
| Base URL | `https://api.anthropic.com` |
|
||
| 认证方式 | `x-api-key: <api_key>` |
|
||
|
||
---
|
||
|
||
## 2. 接口识别
|
||
|
||
### 2.1 URL 路径模式
|
||
|
||
| URL 路径 | InterfaceType |
|
||
|----------|---------------|
|
||
| `/v1/messages` | CHAT |
|
||
| `/v1/models` | MODELS |
|
||
| `/v1/models/{model}` | MODEL_INFO |
|
||
| `/v1/batches` | 透传 |
|
||
| `/v1/messages/count_tokens` | 透传 |
|
||
| `/v1/*` | 透传 |
|
||
|
||
### 2.2 接口能力矩阵
|
||
|
||
```
|
||
Anthropic.supportsInterface(type):
|
||
CHAT: return true
|
||
MODELS: return true
|
||
MODEL_INFO: return true
|
||
EMBEDDINGS: return false // Anthropic 无此接口
|
||
RERANK: return false // Anthropic 无此接口
|
||
default: return false
|
||
```
|
||
|
||
### 2.3 URL 映射表
|
||
|
||
```
|
||
Anthropic.buildUrl(nativePath, interfaceType):
|
||
switch interfaceType:
|
||
case CHAT: return "/v1/messages"
|
||
case MODELS: return "/v1/models"
|
||
case MODEL_INFO: return "/v1/models/{modelId}"
|
||
default: return nativePath
|
||
```
|
||
|
||
不支持 EMBEDDINGS 和 RERANK(`supportsInterface` 返回 false),引擎会自动走透传逻辑。
|
||
|
||
---
|
||
|
||
## 3. 请求头构建
|
||
|
||
### 3.1 buildHeaders
|
||
|
||
```
|
||
Anthropic.buildHeaders(provider):
|
||
result = {}
|
||
result["x-api-key"] = provider.api_key
|
||
result["anthropic-version"] = provider.adapter_config["anthropic_version"] ?? "2023-06-01"
|
||
if provider.adapter_config["anthropic_beta"]:
|
||
result["anthropic-beta"] = provider.adapter_config["anthropic_beta"].join(",")
|
||
result["Content-Type"] = "application/json"
|
||
return result
|
||
```
|
||
|
||
### 3.2 adapter_config 契约
|
||
|
||
| Key | 类型 | 必填 | 默认值 | 说明 |
|
||
|-----|------|------|--------|------|
|
||
| `anthropic_version` | String | 否 | `"2023-06-01"` | API 版本,映射为 `anthropic-version` Header |
|
||
| `anthropic_beta` | Array\<String\> | 否 | `[]` | Beta 功能标识列表,逗号拼接为 `anthropic-beta` Header |
|
||
|
||
---
|
||
|
||
## 4. 核心层 — Chat 请求编解码
|
||
|
||
### 4.1 Decoder(Anthropic → Canonical)
|
||
|
||
#### 系统消息
|
||
|
||
```
|
||
decodeSystem(system):
|
||
if system is None: return None
|
||
if system is String: return system
|
||
return system.map(s => SystemBlock {text: s.text})
|
||
```
|
||
|
||
Anthropic 使用顶层 `system` 字段(String 或 SystemBlock 数组),直接提取为 `canonical.system`。
|
||
|
||
#### 消息角色映射
|
||
|
||
| Anthropic role | Canonical role | 说明 |
|
||
|----------------|---------------|------|
|
||
| `user` | `user` | 直接映射;可能包含 tool_result |
|
||
| `assistant` | `assistant` | 直接映射 |
|
||
|
||
**关键差异**:Anthropic 没有 `system` 和 `tool` 角色。system 通过顶层字段传递;tool_result 嵌入 user 消息的 content 数组中。
|
||
|
||
#### 内容块解码
|
||
|
||
```
|
||
decodeContentBlocks(content):
|
||
if content is String: return [{type: "text", text: content}]
|
||
return content.map(block => {
|
||
switch block.type:
|
||
"text" → TextBlock{text: block.text}
|
||
"tool_use" → ToolUseBlock{id: block.id, name: block.name, input: block.input}
|
||
"tool_result" → ToolResultBlock{tool_use_id: block.tool_use_id, ...}
|
||
"thinking" → ThinkingBlock{thinking: block.thinking}
|
||
"redacted_thinking" → 丢弃 // 仅 Anthropic 使用,不在中间层保留
|
||
})
|
||
```
|
||
|
||
**tool_result 角色转换**:
|
||
|
||
```
|
||
decodeMessage(msg):
|
||
switch msg.role:
|
||
case "user":
|
||
blocks = decodeContentBlocks(msg.content)
|
||
toolResults = blocks.filter(b => b.type == "tool_result")
|
||
others = blocks.filter(b => b.type != "tool_result")
|
||
if toolResults.length > 0:
|
||
return [
|
||
...(others.length > 0 ? [{role: "user", content: others}] : []),
|
||
{role: "tool", content: toolResults}]
|
||
return [{role: "user", content: blocks}]
|
||
case "assistant":
|
||
return [{role: "assistant", content: decodeContentBlocks(msg.content)}]
|
||
```
|
||
|
||
Anthropic user 消息中的 `tool_result` 块被拆分为独立的 Canonical `tool` 角色消息。
|
||
|
||
#### 工具定义
|
||
|
||
| Anthropic | Canonical | 说明 |
|
||
|-----------|-----------|------|
|
||
| `tools[].name` | `tools[].name` | 直接映射 |
|
||
| `tools[].description` | `tools[].description` | 直接映射 |
|
||
| `tools[].input_schema` | `tools[].input_schema` | 字段名相同 |
|
||
| `tools[].type` | — | Anthropic 无 function 包装层 |
|
||
|
||
#### 工具选择
|
||
|
||
| Anthropic tool_choice | Canonical ToolChoice |
|
||
|-----------------------|---------------------|
|
||
| `{type: "auto"}` | `{type: "auto"}` |
|
||
| `{type: "none"}` | `{type: "none"}` |
|
||
| `{type: "any"}` | `{type: "any"}` |
|
||
| `{type: "tool", name}` | `{type: "tool", name}` |
|
||
|
||
#### 参数映射
|
||
|
||
| Anthropic | Canonical | 说明 |
|
||
|-----------|-----------|------|
|
||
| `max_tokens` | `parameters.max_tokens` | 直接映射;Anthropic 必填 |
|
||
| `temperature` | `parameters.temperature` | 直接映射 |
|
||
| `top_p` | `parameters.top_p` | 直接映射 |
|
||
| `top_k` | `parameters.top_k` | 直接映射 |
|
||
| `stop_sequences` (Array) | `parameters.stop_sequences` (Array) | 直接映射 |
|
||
| `stream` | `stream` | 直接映射 |
|
||
|
||
#### 新增公共字段
|
||
|
||
```
|
||
decodeExtras(raw):
|
||
user_id = raw.metadata?.user_id
|
||
output_format = decodeOutputFormat(raw.output_config)
|
||
parallel_tool_use = raw.disable_parallel_tool_use == true ? false : null
|
||
thinking = raw.thinking ? ThinkingConfig {
|
||
type: raw.thinking.type, // "enabled" | "disabled" | "adaptive"
|
||
budget_tokens: raw.thinking.budget_tokens,
|
||
effort: raw.output_config?.effort } : null
|
||
```
|
||
|
||
**ThinkingConfig 三种类型解码**:
|
||
|
||
| Anthropic thinking.type | Canonical thinking.type | 说明 |
|
||
|-------------------------|----------------------|------|
|
||
| `"enabled"` | `"enabled"` | 有 budget_tokens,直接映射 |
|
||
| `"disabled"` | `"disabled"` | 直接映射 |
|
||
| `"adaptive"` | `"adaptive"` | Anthropic 自动决定是否启用思考,映射为 `"adaptive"`(新增 Canonical 值) |
|
||
|
||
> **注意**:`thinking.display`(`"summarized"` / `"omitted"`)为 Anthropic 特有字段,控制响应中思考内容的显示方式,不晋升为公共字段。
|
||
|
||
**output_config 解码**:
|
||
|
||
```
|
||
decodeOutputFormat(output_config):
|
||
if output_config?.format?.type == "json_schema":
|
||
return { type: "json_schema", json_schema: { name: "output", schema: output_config.format.schema, strict: true } }
|
||
return null
|
||
```
|
||
|
||
| Anthropic | Canonical | 提取规则 |
|
||
|-----------|-----------|---------|
|
||
| `metadata.user_id` | `user_id` | 从嵌套对象提取 |
|
||
| `output_config.format` | `output_format` | 仅支持 `json_schema` 类型;映射为 Canonical OutputFormat |
|
||
| `output_config.effort` | `thinking.effort` | `"low"` / `"medium"` / `"high"` / `"xhigh"` / `"max"` 直接映射 |
|
||
| `disable_parallel_tool_use` | `parallel_tool_use` | **语义反转**:true → false |
|
||
| `thinking.type` | `thinking.type` | 直接映射 |
|
||
| `thinking.budget_tokens` | `thinking.budget_tokens` | 直接映射 |
|
||
|
||
#### 协议特有字段
|
||
|
||
| 字段 | 处理方式 |
|
||
|------|---------|
|
||
| `cache_control` | 忽略(仅 Anthropic 使用,不晋升为公共字段) |
|
||
| `redacted_thinking` | 解码时丢弃,不在中间层保留 |
|
||
| `metadata` (除 user_id) | 忽略 |
|
||
| `thinking.display` | 忽略(控制响应显示方式,不影响请求语义) |
|
||
| `container` | 忽略(容器标识,协议特有) |
|
||
| `inference_geo` | 忽略(地理区域控制,协议特有) |
|
||
| `service_tier` | 忽略(服务层级选择,协议特有) |
|
||
|
||
#### 协议约束
|
||
|
||
- `max_tokens` 为**必填**字段
|
||
- messages 必须以 `user` 角色开始
|
||
- `user` 和 `assistant` 角色必须严格交替(除连续 tool_result 场景)
|
||
- tool_result 必须紧跟在包含对应 tool_use 的 assistant 消息之后
|
||
|
||
### 4.2 Encoder(Canonical → Anthropic)
|
||
|
||
#### 模型名称
|
||
|
||
使用 `provider.model_name` 覆盖 `canonical.model`。
|
||
|
||
#### 系统消息注入
|
||
|
||
```
|
||
encodeSystem(system):
|
||
if system is String: return system
|
||
return system.map(s => ({text: s.text}))
|
||
```
|
||
|
||
将 `canonical.system` 编码为 Anthropic 顶层 `system` 字段。
|
||
|
||
#### 消息编码
|
||
|
||
**关键差异**:Canonical 的 `tool` 角色消息需要合并到 Anthropic 的 `user` 消息中:
|
||
|
||
```
|
||
encodeMessages(canonical):
|
||
result = []
|
||
for msg in canonical.messages:
|
||
switch msg.role:
|
||
case "user":
|
||
result.append({role: "user", content: encodeContentBlocks(msg.content)})
|
||
case "assistant":
|
||
result.append({role: "assistant", content: encodeContentBlocks(msg.content)})
|
||
case "tool":
|
||
// tool 角色转为 Anthropic 的 user 消息内 tool_result 块
|
||
toolResults = msg.content.filter(b => b.type == "tool_result")
|
||
if result.length > 0 && result.last.role == "user":
|
||
result.last.content = result.last.content + toolResults
|
||
else:
|
||
result.append({role: "user", content: toolResults})
|
||
```
|
||
|
||
#### 角色约束处理
|
||
|
||
Anthropic 要求 user/assistant 严格交替。编码时需要:
|
||
1. 将 Canonical `tool` 角色合并到相邻 `user` 消息中
|
||
2. 确保首条消息为 `user` 角色(若无,自动注入空 user 消息)
|
||
3. 合并连续同角色消息
|
||
|
||
#### 工具编码
|
||
|
||
```
|
||
encodeTools(canonical):
|
||
if canonical.tools:
|
||
result.tools = canonical.tools.map(t => ({
|
||
name: t.name, description: t.description, input_schema: t.input_schema}))
|
||
|
||
encodeToolChoice(choice):
|
||
switch choice.type:
|
||
"auto" → {type: "auto"}
|
||
"none" → {type: "none"}
|
||
"any" → {type: "any"}
|
||
"tool" → {type: "tool", name: choice.name}
|
||
```
|
||
|
||
#### 公共字段编码
|
||
|
||
```
|
||
encodeRequest(canonical, provider):
|
||
result = {
|
||
model: provider.model_name,
|
||
messages: encodeMessages(canonical),
|
||
max_tokens: canonical.parameters.max_tokens,
|
||
temperature: canonical.parameters.temperature,
|
||
top_p: canonical.parameters.top_p,
|
||
top_k: canonical.parameters.top_k,
|
||
stream: canonical.stream
|
||
}
|
||
if canonical.system:
|
||
result.system = encodeSystem(canonical.system)
|
||
if canonical.parameters.stop_sequences:
|
||
result.stop_sequences = canonical.parameters.stop_sequences
|
||
if canonical.user_id:
|
||
result.metadata = {user_id: canonical.user_id}
|
||
if canonical.output_format or canonical.thinking?.effort:
|
||
result.output_config = {}
|
||
if canonical.output_format:
|
||
result.output_config.format = encodeOutputFormat(canonical.output_format)
|
||
if canonical.thinking?.effort:
|
||
result.output_config.effort = canonical.thinking.effort
|
||
if canonical.parallel_tool_use == false:
|
||
result.disable_parallel_tool_use = true
|
||
if canonical.tools:
|
||
result.tools = canonical.tools.map(t => ({
|
||
name: t.name, description: t.description, input_schema: t.input_schema}))
|
||
if canonical.tool_choice:
|
||
result.tool_choice = encodeToolChoice(canonical.tool_choice)
|
||
if canonical.thinking:
|
||
result.thinking = encodeThinkingConfig(canonical.thinking)
|
||
return result
|
||
|
||
encodeThinkingConfig(canonical):
|
||
switch canonical.type:
|
||
"enabled":
|
||
cfg = {type: "enabled", budget_tokens: canonical.budget_tokens}
|
||
return cfg
|
||
"disabled":
|
||
return {type: "disabled"}
|
||
"adaptive":
|
||
return {type: "adaptive"}
|
||
return {type: "disabled"}
|
||
|
||
encodeOutputFormat(output_format):
|
||
switch output_format.type:
|
||
"json_schema":
|
||
return {type: "json_schema", schema: output_format.json_schema.schema}
|
||
"json_object":
|
||
return {type: "json_schema", schema: {type: "object"}}
|
||
```
|
||
|
||
#### 降级处理
|
||
|
||
对照架构文档 §8.4 三级降级策略,确认每个不支持字段的处理:
|
||
|
||
| Canonical 字段 | Anthropic 不支持时 | 降级策略 |
|
||
|---------------|-------------------|---------|
|
||
| `thinking.effort` | Anthropic 通过 `output_config.effort` 传递 | 自动映射为 `output_config.effort` |
|
||
| `stop_reason: "content_filter"` | Anthropic 无此值 | 自动映射为 `"end_turn"` |
|
||
| `output_format: "text"` | Anthropic 无 text 输出格式 | 丢弃,不设置 output_config |
|
||
| `output_format: "json_object"` | Anthropic 用 json_schema 替代 | 替代方案:生成空 schema 的 json_schema |
|
||
|
||
---
|
||
|
||
## 5. 核心层 — Chat 响应编解码
|
||
|
||
逐字段对照 §4.7 CanonicalResponse 确认映射关系。
|
||
|
||
### 5.1 响应结构
|
||
|
||
```
|
||
Anthropic 响应顶层结构:
|
||
{
|
||
id: String,
|
||
type: "message",
|
||
role: "assistant",
|
||
model: String,
|
||
content: [ContentBlock...],
|
||
stop_reason: String,
|
||
stop_sequence: String | null,
|
||
stop_details: Object | null,
|
||
container: Object | null,
|
||
usage: { input_tokens, output_tokens, cache_read_input_tokens?, cache_creation_input_tokens?,
|
||
cache_creation?, inference_geo?, server_tool_use?, service_tier? }
|
||
}
|
||
```
|
||
|
||
**新增字段**(对比 §4.7 CanonicalResponse):
|
||
|
||
| Anthropic 字段 | 说明 |
|
||
|----------------|------|
|
||
| `stop_details` | 结构化拒绝信息:`{type: "refusal", category, explanation}`,仅 `stop_reason == "refusal"` 时存在 |
|
||
| `container` | 容器信息:`{id, expires_at}`,仅使用 code execution 工具时存在 |
|
||
|
||
### 5.2 Decoder(Anthropic → Canonical)
|
||
|
||
```
|
||
decodeResponse(anthropicResp):
|
||
blocks = []
|
||
for block in anthropicResp.content:
|
||
switch block.type:
|
||
"text" → blocks.append({type: "text", text: block.text})
|
||
"tool_use" → blocks.append({type: "tool_use", id: block.id, name: block.name, input: block.input})
|
||
"thinking" → blocks.append({type: "thinking", thinking: block.thinking})
|
||
"redacted_thinking" → 丢弃 // 仅 Anthropic 使用,不在中间层保留
|
||
return CanonicalResponse {id, model, content: blocks, stop_reason: mapStopReason(anthropicResp.stop_reason),
|
||
usage: CanonicalUsage {input_tokens, output_tokens,
|
||
cache_read_tokens: anthropicResp.usage.cache_read_input_tokens,
|
||
cache_creation_tokens: anthropicResp.usage.cache_creation_input_tokens}}
|
||
```
|
||
|
||
**内容块解码**:
|
||
- `text` → TextBlock(直接映射;忽略 `citations` 字段)
|
||
- `tool_use` → ToolUseBlock(直接映射;忽略 `caller` 字段)
|
||
- `thinking` → ThinkingBlock(直接映射;忽略 `signature` 字段)
|
||
- `redacted_thinking` → 丢弃(协议特有,不晋升为公共字段)
|
||
- `server_tool_use` / `web_search_tool_result` / `code_execution_tool_result` 等 → 丢弃(服务端工具块,协议特有)
|
||
|
||
**停止原因映射**:
|
||
|
||
| Anthropic stop_reason | Canonical stop_reason | 说明 |
|
||
|-----------------------|-----------------------|------|
|
||
| `"end_turn"` | `"end_turn"` | 直接映射 |
|
||
| `"max_tokens"` | `"max_tokens"` | 直接映射 |
|
||
| `"tool_use"` | `"tool_use"` | 直接映射 |
|
||
| `"stop_sequence"` | `"stop_sequence"` | 直接映射 |
|
||
| `"pause_turn"` | `"pause_turn"` | 长轮次暂停,映射为 Canonical 新增值 |
|
||
| `"refusal"` | `"refusal"` | 安全拒绝,直接映射 |
|
||
|
||
**Token 用量映射**:
|
||
|
||
| Anthropic usage | Canonical Usage | 说明 |
|
||
|-----------------|-----------------|------|
|
||
| `input_tokens` | `input_tokens` | 直接映射 |
|
||
| `output_tokens` | `output_tokens` | 直接映射 |
|
||
| `cache_read_input_tokens` | `cache_read_tokens` | 字段名映射 |
|
||
| `cache_creation_input_tokens` | `cache_creation_tokens` | 字段名映射 |
|
||
| `cache_creation` | — | 协议特有(按 TTL 细分),不晋升 |
|
||
| `inference_geo` | — | 协议特有,不晋升 |
|
||
| `server_tool_use` | — | 协议特有,不晋升 |
|
||
| `service_tier` | — | 协议特有,不晋升 |
|
||
| — | `reasoning_tokens` | Anthropic 不返回此字段,始终为 null |
|
||
|
||
**协议特有内容**:
|
||
|
||
| 字段 | 处理方式 |
|
||
|------|---------|
|
||
| `redacted_thinking` | 解码时丢弃 |
|
||
| `stop_sequence` | 解码时忽略(Canonical 用 stop_reason 覆盖) |
|
||
| `stop_details` | 解码时忽略(协议特有,不晋升) |
|
||
| `container` | 解码时忽略(协议特有,不晋升) |
|
||
| `text.citations` | 解码时忽略(协议特有,不晋升) |
|
||
| `tool_use.caller` | 解码时忽略(协议特有,不晋升) |
|
||
| `thinking.signature` | 解码时忽略(协议特有,不晋升;同协议透传时自然保留) |
|
||
|
||
### 5.3 Encoder(Canonical → Anthropic)
|
||
|
||
```
|
||
encodeResponse(canonical):
|
||
blocks = canonical.content.map(block => {
|
||
switch block.type:
|
||
"text" → {type: "text", text: block.text}
|
||
"tool_use" → {type: "tool_use", id: block.id, name: block.name, input: block.input}
|
||
"thinking" → {type: "thinking", thinking: block.thinking}})
|
||
return {id: canonical.id, type: "message", role: "assistant", model: canonical.model,
|
||
content: blocks,
|
||
stop_reason: mapCanonicalStopReason(canonical.stop_reason),
|
||
stop_sequence: None,
|
||
usage: {input_tokens: canonical.usage.input_tokens, output_tokens: canonical.usage.output_tokens,
|
||
cache_read_input_tokens: canonical.usage.cache_read_tokens,
|
||
cache_creation_input_tokens: canonical.usage.cache_creation_tokens}}
|
||
```
|
||
|
||
**内容块编码**:
|
||
- TextBlock → `{type: "text", text}`(直接映射)
|
||
- ToolUseBlock → `{type: "tool_use", id, name, input}`(直接映射)
|
||
- ThinkingBlock → `{type: "thinking", thinking}`(直接映射)
|
||
|
||
**停止原因映射**:
|
||
|
||
| Canonical stop_reason | Anthropic stop_reason |
|
||
|-----------------------|-----------------------|
|
||
| `"end_turn"` | `"end_turn"` |
|
||
| `"max_tokens"` | `"max_tokens"` |
|
||
| `"tool_use"` | `"tool_use"` |
|
||
| `"stop_sequence"` | `"stop_sequence"` |
|
||
| `"pause_turn"` | `"pause_turn"` |
|
||
| `"refusal"` | `"refusal"` |
|
||
| `"content_filter"` | `"end_turn"`(降级) |
|
||
|
||
**降级处理**:
|
||
|
||
| Canonical 字段 | Anthropic 不支持时 | 降级策略 |
|
||
|---------------|-------------------|---------|
|
||
| `stop_reason: "content_filter"` | Anthropic 无此值 | 自动映射为 `"end_turn"` |
|
||
| `reasoning_tokens` | Anthropic 无此字段 | 丢弃 |
|
||
|
||
**协议特有内容**:
|
||
|
||
| 字段 | 处理方式 |
|
||
|------|---------|
|
||
| `redacted_thinking` | 编码时不产出 |
|
||
| `stop_sequence` | 编码时始终为 null |
|
||
| `stop_details` | 编码时不产出 |
|
||
| `container` | 编码时不产出 |
|
||
| `text.citations` | 编码时不产出 |
|
||
| `thinking.signature` | 编码时不产出(同协议透传时自然保留) |
|
||
|
||
---
|
||
|
||
## 6. 核心层 — 流式编解码
|
||
|
||
### 6.1 SSE 格式
|
||
|
||
Anthropic 使用命名 SSE 事件,与 CanonicalStreamEvent 几乎 1:1 对应:
|
||
|
||
```
|
||
event: message_start
|
||
data: {"type":"message_start","message":{"id":"msg_xxx","model":"claude-4",...}}
|
||
|
||
event: content_block_start
|
||
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
||
|
||
event: content_block_delta
|
||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
|
||
|
||
event: content_block_stop
|
||
data: {"type":"content_block_stop","index":0}
|
||
|
||
event: message_delta
|
||
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":10}}
|
||
|
||
event: message_stop
|
||
data: {"type":"message_stop"}
|
||
|
||
event: ping
|
||
data: {"type":"ping"}
|
||
```
|
||
|
||
### 6.2 StreamDecoder(Anthropic SSE → Canonical 事件)
|
||
|
||
Anthropic SSE 事件与 CanonicalStreamEvent 几乎 1:1 映射,状态机最简单:
|
||
|
||
| Anthropic SSE 事件 | Canonical 事件 | 说明 |
|
||
|---|---|---|
|
||
| `message_start` | MessageStartEvent | 直接映射 |
|
||
| `content_block_start` | ContentBlockStartEvent | 直接映射 content_block |
|
||
| `content_block_delta` | ContentBlockDeltaEvent | 见下方 delta 类型映射表 |
|
||
| `content_block_stop` | ContentBlockStopEvent | 直接映射 |
|
||
| `message_delta` | MessageDeltaEvent | 直接映射 delta 和 usage |
|
||
| `message_stop` | MessageStopEvent | 直接映射 |
|
||
| `ping` | PingEvent | 直接映射 |
|
||
| `error` | ErrorEvent | 直接映射 |
|
||
|
||
**delta 类型映射**(`content_block_delta` 事件内):
|
||
|
||
| Anthropic delta 类型 | Canonical delta 类型 | 说明 |
|
||
|---------------------|---------------------|------|
|
||
| `text_delta` | `{type: "text_delta", text}` | 直接映射 |
|
||
| `input_json_delta` | `{type: "input_json_delta", partial_json}` | 直接映射 |
|
||
| `thinking_delta` | `{type: "thinking_delta", thinking}` | 直接映射 |
|
||
| `citations_delta` | 丢弃 | 协议特有,不晋升为公共字段 |
|
||
| `signature_delta` | 丢弃 | 协议特有(用于多轮思考签名连续性),不晋升 |
|
||
|
||
**content_block_start 类型映射**:
|
||
|
||
| Anthropic content_block 类型 | Canonical content_block | 说明 |
|
||
|------------------------------|----------------------|------|
|
||
| `{type: "text", text: ""}` | `{type: "text", text: ""}` | 直接映射 |
|
||
| `{type: "tool_use", id, name, input: {}}` | `{type: "tool_use", id, name, input: {}}` | 直接映射 |
|
||
| `{type: "thinking", thinking: ""}` | `{type: "thinking", thinking: ""}` | 直接映射 |
|
||
| `{type: "redacted_thinking", data: ""}` | 丢弃整个 block | 跳过后续 delta 直到 content_block_stop |
|
||
| `server_tool_use` / `web_search_tool_result` 等 | 丢弃 | 服务端工具块,协议特有 |
|
||
|
||
### 6.3 StreamDecoder 状态机
|
||
|
||
```
|
||
StreamDecoderState {
|
||
messageStarted: Boolean
|
||
openBlocks: Set<Integer>
|
||
currentBlockType: Map<Integer, String>
|
||
currentBlockId: Map<Integer, String>
|
||
redactedBlocks: Set<Integer> // 追踪需要丢弃的 redacted_thinking block
|
||
utf8Remainder: Option<ByteArray> // UTF-8 跨 chunk 安全
|
||
accumulatedUsage: Option<CanonicalUsage>
|
||
}
|
||
```
|
||
|
||
Anthropic Decoder 无需 OpenAI 的 `toolCallIdMap` / `toolCallNameMap` / `toolCallArguments`,因为 Anthropic 的事件已经有明确的结构。
|
||
|
||
**关键处理**:
|
||
|
||
- **`redacted_thinking`**:在 `content_block_start` 事件中检测类型,将 index 加入 `redactedBlocks`,后续 delta 和 stop 事件均丢弃
|
||
- **`citations_delta` / `signature_delta`**:在 delta 映射时直接丢弃,不影响 block 生命周期
|
||
- **`server_tool_use` 等服务端工具块**:与 `redacted_thinking` 处理方式一致,加入 `redactedBlocks` 丢弃
|
||
- **UTF-8 安全**:跨 chunk 截断的 UTF-8 字节需要用 `utf8Remainder` 缓冲
|
||
- **usage 累积**:`message_delta` 中的 usage 与 `message_start` 中的 usage 合并
|
||
|
||
### 6.4 StreamEncoder(Canonical → Anthropic SSE)
|
||
|
||
| Canonical 事件 | Anthropic SSE 事件 | 说明 |
|
||
|---|---|---|
|
||
| MessageStartEvent | `event: message_start` | 直接映射 |
|
||
| ContentBlockStartEvent | `event: content_block_start` | 直接映射 content_block |
|
||
| ContentBlockDeltaEvent | `event: content_block_delta` | 见下方 delta 编码表 |
|
||
| ContentBlockStopEvent | `event: content_block_stop` | 直接映射 |
|
||
| MessageDeltaEvent | `event: message_delta` | 直接映射 |
|
||
| MessageStopEvent | `event: message_stop` | 直接映射 |
|
||
| PingEvent | `event: ping` | 直接映射 |
|
||
| ErrorEvent | `event: error` | 直接映射 |
|
||
|
||
**delta 编码表**:
|
||
|
||
| Canonical delta 类型 | Anthropic delta 类型 | 说明 |
|
||
|---------------------|---------------------|------|
|
||
| `{type: "text_delta", text}` | `text_delta` | 直接映射 |
|
||
| `{type: "input_json_delta", partial_json}` | `input_json_delta` | 直接映射 |
|
||
| `{type: "thinking_delta", thinking}` | `thinking_delta` | 直接映射 |
|
||
|
||
**缓冲策略**:无需缓冲,每个 Canonical 事件直接编码为对应的 Anthropic SSE 事件。
|
||
|
||
**SSE 编码格式**:
|
||
|
||
```
|
||
event: <event_type>\n
|
||
data: <json_payload>\n
|
||
\n
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 扩展层接口
|
||
|
||
### 7.1 /models & /models/{model}
|
||
|
||
**列表接口** `GET /v1/models`:
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口是否存在 | 是 |
|
||
| 请求格式 | GET 请求,支持 `limit`、`after_id`、`before_id` 查询参数 |
|
||
|
||
响应 Decoder(Anthropic → Canonical):
|
||
|
||
```
|
||
decodeModelsResponse(anthropicResp):
|
||
return CanonicalModelList {
|
||
models: anthropicResp.data.map(m => CanonicalModel {
|
||
id: m.id, name: m.display_name ?? m.id, created: parseTimestamp(m.created_at),
|
||
owned_by: "anthropic"})}
|
||
|
||
parseTimestamp(timestamp):
|
||
// Anthropic 返回 RFC 3339 字符串(如 "2025-05-14T00:00:00Z"),需转为 Unix 时间戳
|
||
return rfc3339ToUnix(timestamp) ?? 0
|
||
```
|
||
|
||
响应 Encoder(Canonical → Anthropic):
|
||
|
||
```
|
||
encodeModelsResponse(canonical):
|
||
return {data: canonical.models.map(m => ({
|
||
id: m.id,
|
||
display_name: m.name ?? m.id,
|
||
created_at: m.created ? unixToRfc3339(m.created) : epochRfc3339(),
|
||
type: "model"})),
|
||
has_more: false,
|
||
first_id: canonical.models[0]?.id, last_id: canonical.models.last?.id}
|
||
```
|
||
|
||
**详情接口** `GET /v1/models/{model}`:
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口是否存在 | 是 |
|
||
| 请求格式 | GET 请求,路径参数 `model_id` |
|
||
|
||
响应 Decoder(Anthropic → Canonical):
|
||
|
||
```
|
||
decodeModelInfoResponse(anthropicResp):
|
||
return CanonicalModelInfo {
|
||
id: anthropicResp.id, name: anthropicResp.display_name ?? anthropicResp.id,
|
||
created: parseTimestamp(anthropicResp.created_at), owned_by: "anthropic" }
|
||
```
|
||
|
||
响应 Encoder(Canonical → Anthropic):
|
||
|
||
```
|
||
encodeModelInfoResponse(canonical):
|
||
return {id: canonical.id,
|
||
display_name: canonical.name ?? canonical.id,
|
||
created_at: canonical.created ? unixToRfc3339(canonical.created) : epochRfc3339(),
|
||
type: "model"}
|
||
```
|
||
|
||
**字段映射**(列表和详情共用):
|
||
|
||
| Anthropic | Canonical | 说明 |
|
||
|-----------|-----------|------|
|
||
| `data[].id` | `models[].id` | 直接映射 |
|
||
| `data[].display_name` | `models[].name` | Anthropic 特有的显示名称 |
|
||
| `data[].created_at` | `models[].created` | **类型转换**:Anthropic 为 RFC 3339 字符串,Canonical 为 Unix 时间戳 |
|
||
| `data[].type: "model"` | — | 固定值 |
|
||
| `has_more` | — | 编码时固定为 false |
|
||
| `first_id` / `last_id` | — | 从列表提取 |
|
||
| `data[].capabilities` | — | 协议特有,不晋升 |
|
||
| `data[].max_input_tokens` | — | 协议特有,不晋升 |
|
||
| `data[].max_tokens` | — | 协议特有,不晋升 |
|
||
|
||
**跨协议对接示例**(入站 `/anthropic/v1/models`,目标 OpenAI):
|
||
|
||
```
|
||
入站: GET /anthropic/v1/models, x-api-key: sk-ant-xxx
|
||
→ client=anthropic, provider=openai
|
||
→ URL: /v1/models, Headers: Authorization: Bearer sk-xxx
|
||
|
||
OpenAI 上游响应: {object: "list", data: [{id: "gpt-4o", object: "model", created: 1700000000, owned_by: "openai"}]}
|
||
→ OpenAI.decodeModelsResponse → CanonicalModelList
|
||
→ Anthropic.encodeModelsResponse
|
||
|
||
返回客户端: {data: [{id: "gpt-4o", display_name: "gpt-4o", created_at: "2023-11-04T18:26:40Z", type: "model"}],
|
||
has_more: false, first_id: "gpt-4o", last_id: "gpt-4o"}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 错误编码
|
||
|
||
### 8.1 错误响应格式
|
||
|
||
```json
|
||
{
|
||
"type": "error",
|
||
"error": {
|
||
"type": "invalid_request_error",
|
||
"message": "Error message"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 encodeError
|
||
|
||
```
|
||
Anthropic.encodeError(error):
|
||
return {type: "error", error: {type: error.code, message: error.message}}
|
||
```
|
||
|
||
### 8.3 常用 HTTP 状态码
|
||
|
||
| HTTP Status | 说明 |
|
||
|-------------|------|
|
||
| 400 | 请求格式错误 |
|
||
| 401 | 认证失败(无效 API key) |
|
||
| 403 | 无权限访问 |
|
||
| 404 | 接口不存在 |
|
||
| 429 | 速率限制 |
|
||
| 500 | 服务器内部错误 |
|
||
| 529 | 服务过载 |
|
||
|
||
---
|
||
|
||
## 9. 自检清单
|
||
|
||
| 章节 | 检查项 |
|
||
|------|--------|
|
||
| §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] 角色映射和消息顺序约束已处理(tool→user 合并、首消息 user 保证、交替约束) |
|
||
| §4 | [x] 工具调用(tool_use / tool_result)的编解码已处理 |
|
||
| §4 | [x] 协议特有字段已识别并确定处理方式(cache_control 忽略、redacted_thinking 丢弃) |
|
||
| §5 | [x] Chat 响应的 Decoder 和 Encoder 已实现(逐字段对照 §4.7) |
|
||
| §5 | [x] stop_reason 映射表已确认 |
|
||
| §5 | [x] usage 字段映射已确认(input_tokens / cache_read_input_tokens 等) |
|
||
| §6 | [x] 流式 StreamDecoder 和 StreamEncoder 已实现(对照 §4.8) |
|
||
| §7 | [x] 扩展层接口的编解码已实现(/models、/models/{model}) |
|
||
| §8 | [x] `encodeError` 已实现 |
|