# 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: ` | --- ## 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\ | 否 | `[]` | 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 currentBlockType: Map currentBlockId: Map redactedBlocks: Set // 追踪需要丢弃的 redacted_thinking block utf8Remainder: Option // UTF-8 跨 chunk 安全 accumulatedUsage: Option } ``` 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: \n data: \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` 已实现 |