# Protocol Adapter - Anthropic

## Purpose

实现 Anthropic 协议的完整 ProtocolAdapter，支持请求/响应编解码、流式转换和错误处理，遵循 Anthropic Messages API 规范。

### Requirement: 实现 Anthropic ProtocolAdapter

系统 SHALL 全新实现 Anthropic 协议的完整 ProtocolAdapter，对照 `docs/conversion_anthropic.md`。不沿用旧 `internal/protocol/anthropic/` 代码。

- `protocolName()` SHALL 返回 `"anthropic"`
- `supportsPassthrough()` SHALL 返回 true
- `buildHeaders(provider)` SHALL 构建 `x-api-key`、`anthropic-version`、`anthropic-beta`、`Content-Type`
- `buildUrl(nativePath, interfaceType)` SHALL 按接口类型映射 URL 路径
- `supportsInterface()` SHALL 对 CHAT、MODELS、MODEL_INFO 返回 true，对 EMBEDDINGS、RERANK 返回 false

#### Scenario: 认证 Header 构建

- **WHEN** 调用 buildHeaders(provider)
- **THEN** SHALL 设置 `x-api-key: <provider.api_key>`
- **THEN** SHALL 设置 `anthropic-version`（默认 `"2023-06-01"`，从 adapter_config 可覆盖）
- **WHEN** adapter_config 包含 anthropic_beta
- **THEN** SHALL 以逗号拼接为 `anthropic-beta` Header

#### Scenario: URL 映射

- **WHEN** interfaceType == CHAT
- **THEN** SHALL 映射为 `/v1/messages`
- **WHEN** interfaceType == MODELS
- **THEN** SHALL 映射为 `/v1/models`
- **WHEN** interfaceType == EMBEDDINGS 或 RERANK
- **THEN** SHALL NOT 调用 buildUrl（supportsInterface 返回 false，引擎走透传）

### Requirement: Anthropic 请求解码（Anthropic → Canonical）

系统 SHALL 实现完整的 Anthropic MessagesRequest 到 CanonicalRequest 的解码。

#### Scenario: System 消息提取

- **WHEN** Anthropic 请求包含顶层 `system` 字段
- **THEN** String 类型 SHALL 直接提取为 canonical.system
- **THEN** SystemBlock 数组 SHALL 提取为 canonical.system（Array）

#### Scenario: User 消息中 tool_result 拆分

- **WHEN** Anthropic user 消息的 content 中包含 tool_result 块
- **THEN** SHALL 将 tool_result 块拆分为独立的 CanonicalMessage{role: "tool"}
- **THEN** 非 tool_result 块 SHALL 保留为独立的 CanonicalMessage{role: "user"}
- **THEN** 仅 tool_result 块时 SHALL 只产出 tool 角色消息

#### Scenario: 参数映射

- **WHEN** 解码 Anthropic 请求参数
- **THEN** max_tokens SHALL 直接映射
- **THEN** temperature/top_p/top_k SHALL 直接映射
- **THEN** stop_sequences SHALL 直接映射

#### Scenario: ThinkingConfig 解码

- **WHEN** 解码 Anthropic thinking 字段
- **THEN** type="enabled" SHALL 映射为 Canonical thinking.type="enabled"
- **THEN** type="disabled" SHALL 映射为 Canonical thinking.type="disabled"
- **THEN** type="adaptive" SHALL 映射为 Canonical thinking.type="adaptive"
- **THEN** budget_tokens 和 output_config.effort SHALL 直接映射

#### Scenario: 公共字段提取

- **WHEN** 解码 Anthropic 公共字段
- **THEN** metadata.user_id SHALL 提取为 user_id
- **THEN** output_config.format（json_schema 类型）SHALL 提取为 output_format
- **THEN** disable_parallel_tool_use SHALL 反转映射为 parallel_tool_use（true → false）

#### Scenario: 协议特有字段处理

- **WHEN** 解码遇到 redacted_thinking
- **THEN** SHALL 丢弃，不在中间层保留
- **WHEN** 解码遇到 cache_control
- **THEN** SHALL 忽略，不晋升为公共字段

### Requirement: Anthropic 请求编码（Canonical → Anthropic）

系统 SHALL 实现完整的 CanonicalRequest 到 Anthropic MessagesRequest 的编码。

#### Scenario: System 消息注入

- **WHEN** canonical.system 不为空
- **THEN** SHALL 编码为 Anthropic 顶层 `system` 字段

#### Scenario: Tool 角色合并到 User 消息

- **WHEN** CanonicalMessage{role: "tool"} 出现在消息序列中
- **THEN** SHALL 将其 tool_result 块合并到相邻的 Anthropic user 消息的 content 数组中
- **WHEN** 相邻前一条不是 user 消息
- **THEN** SHALL 创建新的 user 消息来承载 tool_result 块

#### Scenario: 首消息 user 保证

- **WHEN** 编码后的 Anthropic messages 数组首条消息不是 user 角色
- **THEN** SHALL 自动注入一条空 user 消息到头部

#### Scenario: 角色交替约束

- **WHEN** 编码后存在连续同角色消息
- **THEN** SHALL 合并为单条消息（content 数组拼接）

#### Scenario: 参数编码

- **WHEN** 编码 CanonicalRequest 参数
- **THEN** parameters.max_tokens SHALL 直接映射（Anthropic 必填）
- **THEN** parameters.top_k SHALL 直接映射
- **THEN** canonical.thinking.type="enabled" SHALL 映射为 thinking{type: "enabled", budget_tokens}
- **THEN** canonical.thinking.type="adaptive" SHALL 映射为 thinking{type: "adaptive"}

#### Scenario: 公共字段编码

- **WHEN** canonical.user_id 不为空
- **THEN** SHALL 编码为 metadata.user_id
- **WHEN** canonical.parallel_tool_use == false
- **THEN** SHALL 编码为 disable_parallel_tool_use: true
- **WHEN** canonical.output_format 存在
- **THEN** SHALL 编码为 output_config.format

#### Scenario: 降级处理

- **WHEN** canonical.output_format.type == "json_object"
- **THEN** SHALL 降级为 output_config.format{type: "json_schema", schema: {type: "object"}}
- **WHEN** canonical.output_format.type == "text"
- **THEN** SHALL 丢弃，不设置 output_config

### Requirement: Anthropic 响应解码（Anthropic → Canonical）

系统 SHALL 实现 Anthropic MessagesResponse 到 CanonicalResponse 的解码。

#### Scenario: 内容块解码

- **WHEN** Anthropic response 包含 text 块
- **THEN** SHALL 解码为 TextBlock
- **WHEN** 包含 tool_use 块
- **THEN** SHALL 解码为 ToolUseBlock
- **WHEN** 包含 thinking 块
- **THEN** SHALL 解码为 ThinkingBlock
- **WHEN** 包含 redacted_thinking 块
- **THEN** SHALL 丢弃

#### Scenario: 停止原因映射

- **WHEN** 解码 stop_reason
- **THEN** "end_turn" SHALL 映射为 "end_turn"
- **THEN** "max_tokens" SHALL 映射为 "max_tokens"
- **THEN** "tool_use" SHALL 映射为 "tool_use"
- **THEN** "stop_sequence" SHALL 映射为 "stop_sequence"
- **THEN** "refusal" SHALL 映射为 "refusal"
- **THEN** "pause_turn" SHALL 映射为 "pause_turn"

#### Scenario: Usage 映射

- **WHEN** 解码 Anthropic usage
- **THEN** input_tokens SHALL 直接映射
- **THEN** output_tokens SHALL 直接映射
- **THEN** cache_read_input_tokens SHALL 映射为 cache_read_tokens
- **THEN** cache_creation_input_tokens SHALL 映射为 cache_creation_tokens

### Requirement: Anthropic 响应编码（Canonical → Anthropic）

系统 SHALL 实现 CanonicalResponse 到 Anthropic MessagesResponse 的编码。

#### Scenario: 降级处理

- **WHEN** canonical.stop_reason 为 "content_filter"
- **THEN** SHALL 降级映射为 "end_turn"
- **WHEN** canonical.reasoning_tokens 不为空
- **THEN** SHALL 丢弃（Anthropic 无此字段）

### Requirement: Anthropic 流式解码器

系统 SHALL 实现 AnthropicStreamDecoder，将 Anthropic 命名 SSE 事件转换为 CanonicalStreamEvent。

Decoder 几乎 1:1 映射，维护最小状态机：
- messageStarted: 是否已发送 MessageStartEvent
- redactedBlocks: 需要丢弃的 block index 集合
- utf8Remainder: UTF-8 跨 chunk 安全缓冲

#### Scenario: 命名事件 1:1 映射

- **WHEN** 收到 `event: message_start`
- **THEN** SHALL 发出 MessageStartEvent
- **WHEN** 收到 `event: content_block_start`
- **THEN** SHALL 发出 ContentBlockStartEvent
- **WHEN** 收到 `event: content_block_delta`
- **THEN** SHALL 发出 ContentBlockDeltaEvent
- **WHEN** 收到 `event: content_block_stop`
- **THEN** SHALL 发出 ContentBlockStopEvent
- **WHEN** 收到 `event: message_delta`
- **THEN** SHALL 发出 MessageDeltaEvent
- **WHEN** 收到 `event: message_stop`
- **THEN** SHALL 发出 MessageStopEvent
- **WHEN** 收到 `event: ping`
- **THEN** SHALL 发出 PingEvent
- **WHEN** 收到 `event: error`
- **THEN** SHALL 发出 ErrorEvent

#### Scenario: redacted_thinking 块丢弃

- **WHEN** content_block_start 事件中 content_block.type 为 "redacted_thinking"
- **THEN** SHALL 将 index 加入 redactedBlocks
- **THEN** 后续该 index 的 delta 和 stop 事件 SHALL 丢弃

#### Scenario: 协议特有 delta 丢弃

- **WHEN** delta 类型为 citations_delta 或 signature_delta
- **THEN** SHALL 丢弃，不影响 block 生命周期

#### Scenario: 服务端工具块丢弃

- **WHEN** content_block_start 事件中类型为 server_tool_use / web_search_tool_result 等
- **THEN** SHALL 丢弃整个 block

### Requirement: Anthropic 流式编码器

系统 SHALL 实现 AnthropicStreamEncoder，将 CanonicalStreamEvent 编码为 Anthropic 命名 SSE 事件。

#### Scenario: 直接映射，无缓冲

- **WHEN** 收到任意 CanonicalStreamEvent
- **THEN** SHALL 直接编码为对应的 Anthropic SSE 事件
- **THEN** SHALL NOT 缓冲等待（与 OpenAI 编码器不同）

#### Scenario: SSE 格式

- **WHEN** 编码输出
- **THEN** SHALL 使用 `event: <type>\ndata: <json>\n\n` 格式

#### Scenario: Delta 类型编码

- **WHEN** delta.type == "text_delta"
- **THEN** SHALL 编码为 Anthropic text_delta
- **WHEN** delta.type == "input_json_delta"
- **THEN** SHALL 编码为 Anthropic input_json_delta
- **WHEN** delta.type == "thinking_delta"
- **THEN** SHALL 编码为 Anthropic thinking_delta

### Requirement: Anthropic 错误编码

系统 SHALL 实现 Anthropic 协议的错误编码。

#### Scenario: 错误响应格式

- **WHEN** 调用 encodeError(conversionError)
- **THEN** SHALL 返回 `{type: "error", error: {type: <error_code>, message: <message>}}`

### Requirement: Anthropic 扩展层接口编解码

系统 SHALL 实现 Anthropic 协议的扩展层接口编解码（仅 Models）。

#### Scenario: /models 列表接口

- **WHEN** 解码 Anthropic models 响应
- **THEN** data[].display_name SHALL 映射为 models[].name
- **THEN** data[].created_at（RFC 3339）SHALL 转换为 models[].created（Unix 时间戳）
- **WHEN** 编码 CanonicalModelList 为 Anthropic 格式
- **THEN** SHALL 输出包含 has_more、first_id、last_id 的结构
- **THEN** models[].created（Unix 时间戳）SHALL 转换为 RFC 3339 字符串

#### Scenario: /models 详情接口

- **WHEN** 解码/编码 model 详情
- **THEN** SHALL 处理 display_name ↔ name 和 RFC 3339 ↔ Unix 时间戳的转换

#### Scenario: EMBEDDINGS 和 RERANK 不支持

- **WHEN** interfaceType 为 EMBEDDINGS 或 RERANK
- **THEN** supportsInterface SHALL 返回 false
- **THEN** 引擎 SHALL 走透传或返回空响应
### Requirement: 模型详情路径识别

Anthropic 适配器 SHALL 正确识别包含统一模型 ID 的模型详情路径。

#### Scenario: 含斜杠的统一模型 ID 路径

- **WHEN** 路径为 `/v1/models/anthropic/claude-3-opus`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`

#### Scenario: 含多段斜杠的统一模型 ID 路径

- **WHEN** 路径为 `/v1/models/azure/deployments/gpt-4`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`

#### Scenario: 模型列表路径不受影响

- **WHEN** 路径为 `/v1/models`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModels`

### Requirement: 提取统一模型 ID

Anthropic 适配器 SHALL 从路径中提取统一模型 ID。

#### Scenario: 标准路径提取

- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/anthropic/claude-3-opus")`
- **THEN** SHALL 返回 `"anthropic/claude-3-opus"`

#### Scenario: 复杂路径提取

- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/azure/accounts/org/models/gpt-4")`
- **THEN** SHALL 返回 `"azure/accounts/org/models/gpt-4"`

#### Scenario: 非模型详情路径

- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models")`
- **THEN** SHALL 返回错误

### Requirement: 从请求体提取 model

Anthropic 适配器 SHALL 按 InterfaceType 从请求体中提取 model 值。

#### Scenario: Chat 请求提取 model

- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeChat)`，body 为 `{"model":"anthropic/claude-3-opus","messages":[...]}`
- **THEN** SHALL 返回 `"anthropic/claude-3-opus"`

#### Scenario: 无 model 字段

- **WHEN** 调用 `ExtractModelName(body, ifaceType)`，body 中不含 model 字段
- **THEN** SHALL 返回错误

### Requirement: 最小化改写请求体 model 字段

Anthropic 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写请求体中的 model 字段，其余字段保持原始 bytes。

#### Scenario: 请求体 model 改写（统一 ID → 上游名）

- **WHEN** 调用 `RewriteRequestModelName(body, "claude-3-opus-20240229", InterfaceTypeChat)`
- **THEN** SHALL 将请求体中 model 字段替换为 `"claude-3-opus-20240229"`，其余字段原样保留

### Requirement: 最小化改写响应体 model 字段

Anthropic 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写响应体中的 model 字段，其余字段保持原始 bytes。请求体和响应体的 model 字段位置可能不同，各自独立实现。

#### Scenario: 响应体 model 改写（上游名 → 统一 ID）

- **WHEN** 调用 `RewriteResponseModelName(body, "anthropic/claude-3-opus", InterfaceTypeChat)`
- **THEN** SHALL 将响应体中 model 字段替换为 `"anthropic/claude-3-opus"`，其余字段原样保留

### Requirement: Anthropic 适配器保持 v1 前缀

Anthropic 适配器 SHALL 在路径检测和构建时保持 `/v1` 前缀。

#### Scenario: DetectInterfaceType 检测带 v1 的路径

- **WHEN** 调用 `DetectInterfaceType("/v1/messages")`
- **THEN** SHALL 返回 `InterfaceTypeChat`

- **WHEN** 调用 `DetectInterfaceType("/v1/models")`
- **THEN** SHALL 返回 `InterfaceTypeModels`

#### Scenario: BuildUrl 返回带 v1 的路径

- **WHEN** 调用 `BuildUrl(nativePath, InterfaceTypeChat)`
- **THEN** SHALL 返回 `/v1/messages`

- **WHEN** 调用 `BuildUrl(nativePath, InterfaceTypeModels)`
- **THEN** SHALL 返回 `/v1/models`

#### Scenario: 模型详情路径识别

- **WHEN** 路径为 `/v1/models/anthropic/claude-3-opus`
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`

#### Scenario: 提取统一模型 ID

- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/anthropic/claude-3-opus")`
- **THEN** SHALL 返回 `"anthropic/claude-3-opus"`
