1
0
Files
nex/docs/conversion_anthropic.md

787 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 DecoderAnthropic → 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 EncoderCanonical → 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 DecoderAnthropic → 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 EncoderCanonical → 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 StreamDecoderAnthropic 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 StreamEncoderCanonical → 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` 查询参数 |
响应 DecoderAnthropic → 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
```
响应 EncoderCanonical → 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` |
响应 DecoderAnthropic → Canonical
```
decodeModelInfoResponse(anthropicResp):
return CanonicalModelInfo {
id: anthropicResp.id, name: anthropicResp.display_name ?? anthropicResp.id,
created: parseTimestamp(anthropicResp.created_at), owned_by: "anthropic" }
```
响应 EncoderCanonical → 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` 已实现 |