# 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 detectInterfaceType

```
Anthropic.detectInterfaceType(nativePath):
    if nativePath == "/v1/messages": return CHAT
    if nativePath == "/v1/models": return MODELS
    if nativePath matches "^/v1/models/[^/]+$": return MODEL_INFO
    return PASSTHROUGH
```

**说明**：`detectInterfaceType` 由 Anthropic Adapter 实现，根据 Anthropic 协议的 URL 路径约定识别接口类型。

### 2.3 接口能力矩阵

```
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.4 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] `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] 角色映射和消息顺序约束已处理（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` 已实现 |
