# IR 字段归属决策表(Field Homing) > **版本**: v1.0 · **生成依据**: SDK 2026-05-14 快照 (openai@6.37.0 / @anthropic-ai/sdk@0.96.0 / @google/genai@2.2.0) > **用途**: PR-1 编码前的字段归属仲裁。每个字段只能有一个主归属。有争议的字段在本文件讨论并锁定,不在 PR review 中再争。 --- ## 目录 1. [决策规则](#1-决策规则) 2. [归属标签速查](#2-归属标签速查) 3. [OpenAI Chat 字段归属](#3-openai-chat-字段归属) 4. [OpenAI Responses 字段归属](#4-openai-responses-字段归属) 5. [Anthropic Messages 字段归属](#5-anthropic-messages-字段归属) 6. [Google Gemini 字段归属](#6-google-gemini-字段归属) 7. [ContentBlock 跨协议映射](#7-contentblock-跨协议映射) 8. [响应字段归属](#8-响应字段归属) 9. [错误规范化映射](#9-错误规范化映射) 10. [缓存控制跨协议映射](#10-缓存控制跨协议映射) 11. [争议字段决策记录](#11-争议字段决策记录) --- ## 1. 决策规则 字段归属的优先级排序(从高到低): | 优先级 | 规则 | 归属到 | |--------|------|--------| | R1 | 被 Nyro 网关基础组件消费(路由、限流、配额、缓存 key、重试判断等) | **IR 核心** | | R2 | ≥ 2 个协议族有语义等价字段,且行为一致 | **IR 核心** | | R3 | 影响 Codec 行为(编解码时需要读取),但只在 1 个协议族存在 | **ProtocolExt** | | R4 | 纯透传(下游不改语义、不读取),客户端写什么 egress 发什么 | **VendorBag.passthrough_safe** | | R5 | 只在 SDK 层存在,不进入 HTTP wire | **DROP(不进 IR)** | | R6 | 已废弃但需要兼容 | **VendorBag.passthrough_safe** + 文档标注 DEPR | **Ext vs VendorBag 判断标准**: - 如果 Codec 编码时需要 `if let Some(ext) = req.ext.downcast::() { ... }` 才能处理该字段 → 归 **ProtocolExt** - 如果 Encoder 只需 `serde_json::to_value(bag.passthrough_safe)` 合并到 body → 归 **VendorBag** --- ## 2. 归属标签速查 | 标签 | 含义 | Rust 类型 | |------|------|-----------| | `IR` | `AiRequest` 核心字段 | `AiRequest::field` | | `OAIChat` | OpenAI Chat Ext | `OpenAIChatExt::field` | | `OAIResp` | OpenAI Responses Ext | `OpenAIResponsesExt::field` | | `ANT` | Anthropic Ext | `AnthropicExt::field` | | `GGL` | Google Gemini Ext | `GoogleExt::field` | | `BAG↓pass` | 透传 bag(egress 原样注入) | `VendorExtensions::passthrough_safe` | | `BAG↓ing` | 入口 bag(Decoder 专用,不透传) | `VendorExtensions::ingress` | | `DROP` | SDK 专有字段,不进 IR | — | | `DEPR` | 已废弃,仅兼容 | 标注 deprecated | --- ## 3. OpenAI Chat 字段归属 **Wire**: `POST /v1/chat/completions` · SDK: openai@6.37.0 ### 请求体字段 | 字段 | 归属 | IR 映射路径 | 说明 | |------|------|------------|------| | `messages` | IR | `AiRequest.messages` | 消息列表,含多模态 content parts | | `model` | IR | `AiRequest.model` | | | `temperature` | IR | `AiRequest.generation.temperature` | 0-2 范围 | | `top_p` | IR | `AiRequest.generation.top_p` | | | `max_completion_tokens` | IR | `AiRequest.generation.max_tokens` | 规范字段名 | | `max_tokens` | IR | `AiRequest.generation.max_tokens` | `max_completion_tokens` 的别名;Decoder 合并 | | `stop` | IR | `AiRequest.generation.stop` | `String \| String[]` → `Vec` | | `seed` | IR | `AiRequest.generation.seed` | | | `presence_penalty` | IR | `AiRequest.generation.presence_penalty` | 与 Google `presencePenalty` 语义等价 | | `frequency_penalty` | IR | `AiRequest.generation.frequency_penalty` | 与 Google `frequencyPenalty` 语义等价 | | `tools` | IR | `AiRequest.tools` | `FunctionDefinition[]` → `Vec` | | `tool_choice` | IR | `AiRequest.tool_config.choice` | `none/auto/required/function{}` → `ToolChoice` enum | | `parallel_tool_calls` | IR | `AiRequest.tool_config.parallel` | 与 Responses API 语义等价 | | `reasoning_effort` | IR | `AiRequest.reasoning.effort` | `none/low/medium/high/xhigh` → `ReasoningEffort` enum | | `response_format` | IR | `AiRequest.response_format` | `text / json_object / json_schema` → `ResponseFormat` | | `stream` | IR | `AiRequest.stream` | | | `audio` | OAIChat | `OpenAIChatExt.audio` | 音频输出参数,仅此协议 | | `logit_bias` | OAIChat | `OpenAIChatExt.logit_bias` | token 偏置,OAI 专有 | | `logprobs` | OAIChat | `OpenAIChatExt.logprobs` | Encoder 专读 | | `top_logprobs` | OAIChat | `OpenAIChatExt.top_logprobs` | 依赖 logprobs | | `modalities` | OAIChat | `OpenAIChatExt.modalities` | `["text","audio"]` 多模态输出 | | `n` | OAIChat | `OpenAIChatExt.n` | 多候选数,GGL `candidateCount` 不对齐(分类方式不同) | | `prediction` | OAIChat | `OpenAIChatExt.prediction` | 推测解码 | | `prompt_cache_retention` | OAIChat | `OpenAIChatExt.prompt_cache_retention` | `in_memory \| 24h`,仅 OAI | | `stream_options` | OAIChat | `OpenAIChatExt.stream_options` | `include_usage` 等 | | `verbosity` | OAIChat | `OpenAIChatExt.verbosity` | `low/medium/high` 输出详略 | | `web_search_options` | OAIChat | `OpenAIChatExt.web_search_options` | Chat API 内置 web search | | `metadata` | BAG↓pass | `VendorExtensions.passthrough_safe["metadata"]` | OAI KV store,Nyro 不读取 | | `prompt_cache_key` | BAG↓pass | `VendorExtensions.passthrough_safe["prompt_cache_key"]` | OAI cache hit 优化 key | | `safety_identifier` | BAG↓pass | `VendorExtensions.passthrough_safe["safety_identifier"]` | 用户哈希 ID | | `service_tier` | BAG↓pass | `VendorExtensions.passthrough_safe["service_tier"]` | `auto/default/flex/scale/priority` | | `store` | BAG↓pass | `VendorExtensions.passthrough_safe["store"]` | 数据保留 flag | | `user` | BAG↓pass | `VendorExtensions.passthrough_safe["user"]` | 用户标识(被 safety_identifier 替代) | | `function_call` | BAG↓pass | `VendorExtensions.passthrough_safe["function_call"]` | DEPR: 旧版函数调用 | | `functions` | BAG↓pass | `VendorExtensions.passthrough_safe["functions"]` | DEPR: 旧版函数定义 | ### 消息 Content Part 映射 | OAI Content Part | IR ContentBlock variant | |-----------------|------------------------| | `ChatCompletionContentPartText` | `ContentBlock::Text` | | `ChatCompletionContentPartImage` | `ContentBlock::Image` | | `ChatCompletionContentPartInputAudio` | `ContentBlock::Audio` | | `File` (in ContentPart) | `ContentBlock::File` | | `tool_calls[].function` (assistant) | `ContentBlock::ToolUse` | | tool message (role=tool) | `ContentBlock::ToolResult` | | `ChatCompletionContentPartRefusal` | `ContentBlock::Refusal` | --- ## 4. OpenAI Responses 字段归属 **Wire**: `POST /v1/responses` · SDK: openai@6.37.0 ### 请求体字段 | 字段 | 归属 | IR 映射路径 | 说明 | |------|------|------------|------| | `model` | IR | `AiRequest.model` | | | `input` | IR | `AiRequest.messages` | `string \| ResponseInput` → 消息列表;Decoder 展开 | | `instructions` | IR | `AiRequest.system` | 系统消息 | | `temperature` | IR | `AiRequest.generation.temperature` | | | `top_p` | IR | `AiRequest.generation.top_p` | | | `max_output_tokens` | IR | `AiRequest.generation.max_tokens` | 同 `max_completion_tokens` | | `tools` | IR | `AiRequest.tools` | function + built-in tools | | `tool_choice` | IR | `AiRequest.tool_config.choice` | | | `parallel_tool_calls` | IR | `AiRequest.tool_config.parallel` | | | `reasoning` | IR | `AiRequest.reasoning` | `{effort, summary}` → `ReasoningConfig` | | `text` | IR | `AiRequest.response_format` | `ResponseTextConfig.format` → `ResponseFormat` | | `stream` | IR | `AiRequest.stream` | | | `background` | OAIResp | `OpenAIResponsesExt.background` | 后台模式,Encoder 写入 | | `context_management` | OAIResp | `OpenAIResponsesExt.context_management` | 上下文压缩策略 | | `conversation` | OAIResp | `OpenAIResponsesExt.conversation` | 有状态会话 ID | | `include` | OAIResp | `OpenAIResponsesExt.include` | 额外输出项,Encoder 写入 | | `previous_response_id` | OAIResp | `OpenAIResponsesExt.previous_response_id` | 多轮对话链 | | `prompt` | OAIResp | `OpenAIResponsesExt.prompt` | prompt 模板引用 | | `prompt_cache_retention` | OAIResp | `OpenAIResponsesExt.prompt_cache_retention` | 与 Chat API 相同语义但不合并 IR(不跨协议通用) | | `stream_options` | OAIResp | `OpenAIResponsesExt.stream_options` | | | `top_logprobs` | OAIResp | `OpenAIResponsesExt.top_logprobs` | | | `truncation` | OAIResp | `OpenAIResponsesExt.truncation` | `auto \| disabled` 上下文截断策略 | | `metadata` | BAG↓pass | `VendorExtensions.passthrough_safe["metadata"]` | | | `prompt_cache_key` | BAG↓pass | `VendorExtensions.passthrough_safe["prompt_cache_key"]` | | | `safety_identifier` | BAG↓pass | `VendorExtensions.passthrough_safe["safety_identifier"]` | | | `service_tier` | BAG↓pass | `VendorExtensions.passthrough_safe["service_tier"]` | | | `store` | BAG↓pass | `VendorExtensions.passthrough_safe["store"]` | | | `user` | BAG↓pass | `VendorExtensions.passthrough_safe["user"]` | | ### 流式事件 (53 个) 归属原则 Responses API 流式事件不归属到 `AiRequest`,由 `StreamParser` 消费并映射到 `AiStreamDelta` 变体: | 事件组 | AiStreamDelta 映射 | |--------|-------------------| | `response.created` / `response.in_progress` / `response.queued` | `AiStreamDelta::ResponseLifecycle` | | `response.output_item.added` | `AiStreamDelta::ItemStart { index, item_type }` | | `response.text.delta` / `response.reasoning_text.delta` | `AiStreamDelta::TextDelta { delta, content_index }` | | `response.function_call_arguments.delta` | `AiStreamDelta::ToolCallArgsDelta { delta, call_id }` | | `response.function_call_arguments.done` | `AiStreamDelta::ToolCallArgsDone { call_id, name, arguments }` | | `response.output_item.done` | `AiStreamDelta::ItemDone { index }` | | `response.completed` | `AiStreamDelta::Done { usage }` | | `response.failed` / `response.incomplete` | `AiStreamDelta::StreamError { error }` | | `response.error` | `AiStreamDelta::StreamError { error }` | | 其他 built-in tool 事件 (web_search/file_search/code_interpreter/mcp/image_gen) | `AiStreamDelta::ServerToolEvent { event_type, item_id, payload }` | --- ## 5. Anthropic Messages 字段归属 **Wire**: `POST /v1/messages` · SDK: @anthropic-ai/sdk@0.96.0 ### 请求体字段 | 字段 | 归属 | IR 映射路径 | 说明 | |------|------|------------|------| | `model` | IR | `AiRequest.model` | | | `messages` | IR | `AiRequest.messages` | `ContentBlockParam[]` union(16+ 类型) | | `max_tokens` | IR | `AiRequest.generation.max_tokens` | | | `system` | IR | `AiRequest.system` | `string \| TextBlockParam[]`(支持 cache_control) | | `stream` | IR | `AiRequest.stream` | | | `temperature` | IR | `AiRequest.generation.temperature` | | | `top_p` | IR | `AiRequest.generation.top_p` | | | `tools` | IR | `AiRequest.tools` | user `Tool` → `ToolSpec`;server tools → `AnthropicExt.server_tools` | | `tool_choice` | IR | `AiRequest.tool_config.choice` | `ToolChoiceAuto/Any/Tool/None` → `ToolChoice` | | `thinking` | IR | `AiRequest.reasoning` | `ThinkingConfigParam` → `ReasoningConfig {enabled, budget_tokens, display}` | | `stop_sequences` | IR | `AiRequest.generation.stop` | | | `top_k` | ANT | `AnthropicExt.top_k` | Anthropic 专有采样参数 | | `container` | ANT | `AnthropicExt.container` | 代码执行容器配置 | | `inference_geo` | ANT | `AnthropicExt.inference_geo` | 地理位置路由 | | `output_config` | ANT | `AnthropicExt.output_config` | `{effort, format: json_schema}` 结构化输出 | | `service_tier` | ANT | `AnthropicExt.service_tier` | Anthropic 有自己的 tier 定义,不与 OAI 合并 | | `metadata` | BAG↓pass | `VendorExtensions.passthrough_safe["metadata"]` | `{user_id}` | | (beta headers) | BAG↓ing | `VendorExtensions.ingress["anthropic-beta"]` | Decoder 读取 request header | ### ContentBlockParam 映射 Anthropic 的 `ContentBlockParam` 是一个 16 类型联合体,全部映射到 `ContentBlock` enum: | Anthropic ContentBlockParam | IR ContentBlock variant | cache_control 携带 | |-----------------------------|------------------------|-------------------| | `TextBlockParam` | `ContentBlock::Text` | ✓ `cache_control` 字段 | | `ImageBlockParam` | `ContentBlock::Image` | ✓ | | `DocumentBlockParam` | `ContentBlock::Document` | ✓ | | `SearchResultBlockParam` | `ContentBlock::SearchResult` | ✓ | | `ThinkingBlockParam` | `ContentBlock::Thinking` | ✗ | | `RedactedThinkingBlockParam` | `ContentBlock::RedactedThinking` | ✗ | | `ToolUseBlockParam` | `ContentBlock::ToolUse` | ✓ | | `ToolResultBlockParam` | `ContentBlock::ToolResult` | ✓ | | `ServerToolUseBlockParam` | `ContentBlock::ServerToolUse` | ✓ | | `WebSearchToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: WebSearch }` | ✓ | | `WebFetchToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: WebFetch }` | ✓ | | `CodeExecutionToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: CodeExecution }` | ✓ | | `BashCodeExecutionToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: BashCodeExecution }` | ✓ | | `TextEditorCodeExecutionToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: TextEditor }` | ✓ | | `ToolSearchToolResultBlockParam` | `ContentBlock::ServerToolResult { server_type: ToolSearch }` | ✓ | | `ContainerUploadBlockParam` | `ContentBlock::ContainerUpload` | ✓ | ### Server Tools 归属 Anthropic 的 server tool 规范(`ToolBash`, `WebSearchTool`, `CodeExecutionTool` 等)进入 `AnthropicExt.server_tools`,不映射到 `AiRequest.tools`(后者仅存放用户定义工具): | Anthropic Server Tool Type | 归属 | |---------------------------|------| | `Tool` (custom, type=custom) | IR: `AiRequest.tools` → `ToolSpec` | | `ToolBash20250124` | ANT: `AnthropicExt.server_tools` | | `CodeExecutionTool20250522/20250825/20260120` | ANT: `AnthropicExt.server_tools` | | `MemoryTool20250818` | ANT: `AnthropicExt.server_tools` | | `ToolTextEditor20250124/20250429/20250728` | ANT: `AnthropicExt.server_tools` | | `WebSearchTool20250305/20260209` | ANT: `AnthropicExt.server_tools` | | `WebFetchTool20250910/20260209/20260309` | ANT: `AnthropicExt.server_tools` | | `ToolSearchToolBm25_20251119` | ANT: `AnthropicExt.server_tools` | | `ToolSearchToolRegex20251119` | ANT: `AnthropicExt.server_tools` | --- ## 6. Google Gemini 字段归属 **Wire**: `POST /v1beta/{model}:generateContent` · SDK: @google/genai@2.2.0 注:SDK `config: GenerateContentConfig` 是 wrapper;wire 层字段嵌套在 `generationConfig` 对象中。 ### 请求体字段 | SDK 字段 | Wire 字段 | 归属 | IR 映射路径 | 说明 | |---------|-----------|------|------------|------| | `model` | (URL path) | IR | `AiRequest.model` | 写入 URL,不在 body | | `contents` | `contents` | IR | `AiRequest.messages` | `Content[]` → messages;`Content.role` = user/model | | `systemInstruction` | `systemInstruction` | IR | `AiRequest.system` | `Content` 格式 | | `temperature` | `generationConfig.temperature` | IR | `AiRequest.generation.temperature` | | | `topP` | `generationConfig.topP` | IR | `AiRequest.generation.top_p` | camelCase | | `maxOutputTokens` | `generationConfig.maxOutputTokens` | IR | `AiRequest.generation.max_tokens` | | | `stopSequences` | `generationConfig.stopSequences` | IR | `AiRequest.generation.stop` | | | `presencePenalty` | `generationConfig.presencePenalty` | IR | `AiRequest.generation.presence_penalty` | | | `frequencyPenalty` | `generationConfig.frequencyPenalty` | IR | `AiRequest.generation.frequency_penalty` | | | `seed` | `generationConfig.seed` | IR | `AiRequest.generation.seed` | | | `responseSchema` | `generationConfig.responseSchema` | IR | `AiRequest.response_format` | Schema 大写 type 需归一化 | | `safetySettings` | `safetySettings` | IR | `AiRequest.safety_settings` | `Vec` | | `tools` | `tools` | IR | `AiRequest.tools` | `functionDeclarations[]` → `ToolSpec` | | `topK` | `generationConfig.topK` | GGL | `GoogleExt.top_k` | | | `candidateCount` | `generationConfig.candidateCount` | GGL | `GoogleExt.candidate_count` | | | `responseLogprobs` | `generationConfig.responseLogprobs` | GGL | `GoogleExt.response_logprobs` | | | `logprobs` | `generationConfig.logprobs` | GGL | `GoogleExt.logprobs` | | | `responseMimeType` | `generationConfig.responseMimeType` | GGL | `GoogleExt.response_mime_type` | | | `responseJsonSchema` | `generationConfig.responseJsonSchema` | GGL | `GoogleExt.response_json_schema` | responseSchema 的 JSON Schema 变体 | | `toolConfig` | `toolConfig` | GGL | `GoogleExt.tool_config` | function calling mode | | `cachedContent` | `cachedContent` | GGL | `GoogleExt.cached_content` | GGL 服务端缓存引用 | | `responseModalities` | `generationConfig.responseModalities` | GGL | `GoogleExt.response_modalities` | `["TEXT","IMAGE","AUDIO"]` | | `thinkingConfig` | `generationConfig.thinkingConfig` | GGL | `GoogleExt.thinking_config` | 思考预算(与 ANT thinking 不合并,枚举不对齐) | | `imageConfig` | `generationConfig.imageConfig` | GGL | `GoogleExt.image_config` | 图像生成配置 | | `routingConfig` | `generationConfig.routingConfig` | BAG↓pass | `VendorExtensions.passthrough_safe["routing_config"]` | auto/manual 路由 | | `modelSelectionConfig` | `generationConfig.modelSelectionConfig` | BAG↓pass | `VendorExtensions.passthrough_safe["model_selection_config"]` | | | `labels` | `labels` | BAG↓pass | `VendorExtensions.passthrough_safe["labels"]` | billing labels | | `mediaResolution` | `generationConfig.mediaResolution` | BAG↓pass | `VendorExtensions.passthrough_safe["media_resolution"]` | | | `speechConfig` | `generationConfig.speechConfig` | BAG↓pass | `VendorExtensions.passthrough_safe["speech_config"]` | | | `audioTimestamp` | `generationConfig.audioTimestamp` | BAG↓pass | `VendorExtensions.passthrough_safe["audio_timestamp"]` | | | `enableEnhancedCivicAnswers` | `generationConfig.enableEnhancedCivicAnswers` | BAG↓pass | `VendorExtensions.passthrough_safe["enable_enhanced_civic_answers"]` | | | `modelArmorConfig` | `modelArmorConfig` | BAG↓pass | `VendorExtensions.passthrough_safe["model_armor_config"]` | | | `serviceTier` | `generationConfig.serviceTier` | BAG↓pass | `VendorExtensions.passthrough_safe["service_tier"]` | | | `httpOptions` | — | DROP | — | SDK 层配置,不进 IR | | `abortSignal` | — | DROP | — | SDK 层,不进 IR | | `automaticFunctionCalling` | — | DROP | — | SDK 自动函数调用循环,不进 IR | ### Google Part → ContentBlock 映射 | Google `Part` 字段 | IR ContentBlock variant | |-------------------|------------------------| | `Part.text` (role=user/model) | `ContentBlock::Text` | | `Part.inlineData` (image/audio MIME) | `ContentBlock::Image` / `ContentBlock::Audio` | | `Part.fileData` | `ContentBlock::File` | | `Part.functionCall` | `ContentBlock::ToolUse` | | `Part.functionResponse` | `ContentBlock::ToolResult` | | `Part.thought=true` + `Part.text` | `ContentBlock::Thinking` | | `Part.thoughtSignature` | `ContentBlock::Thinking.signature` | | `Part.executableCode` | `ContentBlock::ExecutableCode` | | `Part.codeExecutionResult` | `ContentBlock::CodeExecutionResult` | | `Part.toolCall` (server-side) | `ContentBlock::ServerToolUse` | | `Part.toolResponse` (server-side) | `ContentBlock::ServerToolResult` | --- ## 7. ContentBlock 跨协议映射 `ContentBlock` 是 IR 的核心枚举,Decoder 负责将各协议的 content 结构归一化为该枚举。 | ContentBlock variant | OAI Chat | OAI Responses | Anthropic | Google | |---------------------|----------|---------------|-----------|--------| | `Text { text, cache_control? }` | `ContentPartText` | `ResponseInputText` / `ResponseOutputText` | `TextBlockParam` | `Part.text` | | `Image { source, cache_control? }` | `ContentPartImage` | `ResponseInputImage` | `ImageBlockParam` | `Part.inlineData`(img) | | `Audio { source }` | `ContentPartInputAudio` | `ResponseInputAudio` | — | `Part.inlineData`(audio) | | `File { source, detail? }` | `File` in content | `ResponseInputFile` | `DocumentBlockParam` | `Part.fileData` | | `Document { source, title?, context?, citations? }` | — | — | `DocumentBlockParam` (distinct from File) | — | | `SearchResult { content, source, title }` | — | — | `SearchResultBlockParam` | — | | `ToolUse { id, name, input, cache_control? }` | `tool_calls[n].function` | `ResponseFunctionToolCall` | `ToolUseBlockParam` | `Part.functionCall` | | `ToolResult { tool_use_id, content, is_error?, cache_control? }` | tool role msg | `ResponseFunctionToolCallOutput` | `ToolResultBlockParam` | `Part.functionResponse` | | `Thinking { thinking, signature }` | — | `ResponseReasoningItem` | `ThinkingBlockParam` | `Part{thought=true}` + text | | `RedactedThinking { data }` | — | `reasoning.encrypted_content` | `RedactedThinkingBlockParam` | — | | `ServerToolUse { id, name, input, server_type }` | (built-in calls) | built-in tool call items | `ServerToolUseBlockParam` | `Part.toolCall` | | `ServerToolResult { tool_use_id, content, server_type }` | (built-in outputs) | built-in tool output items | `WebSearch/WebFetch/CodeExec/...ResultBlockParam` | `Part.toolResponse` | | `Citation { source, location, cited_text }` | — | `ResponseOutputTextAnnotation` | TextBlock.citations | — | | `ContainerUpload { file_id, cache_control? }` | — | `ResponseInputFile`(container) | `ContainerUploadBlockParam` | — | | `ExecutableCode { code, language, id? }` | — | `ResponseCodeInterpreterCall` | (via server tool result) | `Part.executableCode` | | `CodeExecutionResult { return_code, stdout, stderr, id? }` | — | code interpreter outputs | `CodeExecutionResultBlockParam` | `Part.codeExecutionResult` | | `Refusal { refusal }` | `ContentPartRefusal` | `ResponseOutputRefusal` | stop_reason=refusal | — | **编解码方向说明**: - Decoder(ingress):协议原生格式 → `ContentBlock` enum - Encoder(egress):`ContentBlock` enum → 目标协议格式 - Parser(response):模型响应 → `ContentBlock` / `AiStreamDelta` --- ## 8. 响应字段归属 ### AiResponse 核心字段 | 语义 | OAI Chat | OAI Responses | Anthropic | Google | IR 字段 | |------|----------|---------------|-----------|--------|---------| | 响应 ID | `id` | `id` | `id` | `responseId` | `AiResponse.id` | | 使用模型 | `model` | `model` | `model` | `modelVersion` | `AiResponse.model` | | 停止原因 | `choices[].finish_reason` | `status` / item.status | `stop_reason` | `candidates[].finishReason` | `AiResponse.stop_reason` | | 输出内容 | `choices[].message.content` | `output[]` | `content[]` | `candidates[].content` | `AiResponse.content: Vec` | | 工具调用 | `choices[].message.tool_calls` | `output[].function_call` | `content[tool_use]` | `Part.functionCall` | 嵌入 `AiResponse.content` | | Prompt token | `usage.prompt_tokens` | `usage.input_tokens` | `usage.input_tokens` | `usageMetadata.promptTokenCount` | `AiResponse.usage.input_tokens` | | Completion token | `usage.completion_tokens` | `usage.output_tokens` | `usage.output_tokens` | `usageMetadata.candidatesTokenCount` | `AiResponse.usage.output_tokens` | | 缓存命中 tokens | `usage.prompt_tokens_details.cached_tokens` | `usage.input_tokens_details.cached_tokens` | `usage.cache_read_input_tokens` | `usageMetadata.cachedContentTokenCount` | `AiResponse.usage.cache_read_tokens` | | 推理 tokens | `usage.completion_tokens_details.reasoning_tokens` | `usage.output_tokens_details.reasoning_tokens` | (thinking tokens in input) | `usageMetadata.thoughtsTokenCount` | `AiResponse.usage.reasoning_tokens` | ### 响应中的 VendorBag 字段(透传回客户端) | 字段 | 协议 | VendorBag 路径 | |------|------|---------------| | `system_fingerprint` | OAI Chat | `VendorExtensions.passthrough_safe["system_fingerprint"]` | | `service_tier` | OAI Chat/Responses | `VendorExtensions.passthrough_safe["service_tier"]` | | `created` / `created_at` | OAI | `VendorExtensions.passthrough_safe["created"]` | | `conversation` (response) | OAI Responses | `VendorExtensions.passthrough_safe["conversation"]` | | `promptFeedback` | Google | `VendorExtensions.passthrough_safe["prompt_feedback"]` | | `modelStatus` | Google | `VendorExtensions.passthrough_safe["model_status"]` | --- ## 9. 错误规范化映射 `AiError` / `AiErrorKind` 是 IR 的错误公民,所有协议的错误均归一化到此类型。 ### AiErrorKind 枚举与协议映射 | AiErrorKind | OAI HTTP 状态/code | ANT HTTP 状态/type | GGL HTTP 状态/reason | is_retryable | |-------------|-------------------|-------------------|---------------------|-------------| | `AuthenticationError` | 401 | 401/authentication_error | 401 | false | | `AuthorizationError` | 403 | 403/permission_error | 403 | false | | `NotFoundError` | 404 | 404/not_found_error | 404 | false | | `RateLimitError` | 429 | 429/rate_limit_error | 429 | **true** | | `QuotaExceeded` | 429 (type=quota) | 529/overloaded_error | 429 (quotaExceeded) | false | | `InvalidRequest` | 400 | 400/invalid_request_error | 400 | false | | `ServerError` | 500 | 500/api_error | 500 | **true** | | `ServiceUnavailable` | 503 | 529/overloaded_error | 503 | **true** | | `Timeout` | 408/504 | 408 | 408 | **true** | | `ContentFiltered` | 400 (content_filter) | — | 200 with `promptFeedback.blockReason` | false | | `ContextLengthExceeded` | 400 (context_length) | 400/context_length_exceeded | 400 | false | | `ModelNotAvailable` | 404/503 | 404/not_found_error | 404 | **true** (临时) | | `StreamMidError` | data: {error:{...}} in SSE | event: error\ndata: {...} | `promptFeedback` in chunk or EOF | 按 kind 判断 | | `UnexpectedEof` | stream 被截断 | stream 被截断 | stream 被截断 | **true** | | `Unknown` | 其他 | 其他 | 其他 | false | ### 流式中途错误检测 | 协议 | 错误信号 | 检测位置 | |------|---------|---------| | OAI Chat | `data: {"error":{"message":"...","type":"...","code":"..."}}` | StreamParser: 每 SSE line | | OAI Responses | `event: error` + `data: {"code":"...","message":"..."}` 或 `response.failed` event | StreamParser: event type 检测 | | Anthropic | `event: error` + `data: {"type":"error","error":{...}}` | StreamParser: event type 检测 | | Google | stream body 中 `promptFeedback.blockReason` 或 EOF without `[DONE]` | StreamParser: 第一 chunk + EOF 检测 | --- ## 10. 缓存控制跨协议映射 ### 请求侧 CacheControl 注入 | 协议 | 方式 | 最小 token 阈值 | cache_control 挂载点 | |------|------|----------------|---------------------| | Anthropic | 每个 ContentBlock / ToolSpec 的 `cache_control` 字段 | 1024 tokens (ephemeral 5m) / 2048 tokens (ephemeral 1h) | `ContentBlock.cache_control` | | OAI Chat | 自动(prefix-based,client 无法显式标注) | 1024 tokens | 无字段;通过 `prompt_cache_key` + `prompt_cache_retention` 控制 | | OAI Responses | 同 OAI Chat | 1024 tokens | 同上 | | Google | `cachedContent` 引用名(服务端预缓存) | — | `GoogleExt.cached_content` | ### IR 中的 CacheControl 表示 ``` CacheControl { ttl: CacheTtl, // Ephemeral5m | Ephemeral1h | Auto breakpoint_priority: u8, // 多断点时的优先级排序,0=最低 } ``` **注入策略**(由 Encoder 实现): - Anthropic 出口:从 `ContentBlock.cache_control` 读取并写入 wire 字段 - OAI 出口:忽略 `ContentBlock.cache_control`,仅透传 `prompt_cache_key` / `prompt_cache_retention`(如 VendorBag 中有) - Google 出口:透传 `GoogleExt.cached_content` --- ## 11. 争议字段决策记录 以下字段有争议,决策已锁定,不在 PR review 重新讨论。 | 字段 | 争议 | 决策 | 理由 | |------|------|------|------| | `n` (OAI) vs `candidateCount` (GGL) | 语义相同,是否合并到 IR? | **不合并,各入 ProtocolExt** | 两者响应结构差异大(choices vs candidates),合并 IR 对 Parser 意义不大,会污染核心 | | `thinkingConfig` (GGL) vs `thinking` (ANT) vs `reasoning` (OAI) | 三协议思考配置,是否合并? | **ANT + OAI 合并到 `AiRequest.reasoning`;GGL 单独 `GoogleExt.thinking_config`** | ANT budget_tokens + OAI effort 可互操;GGL thinking 是实验性且结构不同 | | `service_tier` (OAI/ANT/GGL 都有) | 是否合并到 IR? | **不合并,各入 BAG↓pass(OAI/GGL)或 AnthropicExt(ANT)** | 各协议的 tier 枚举值完全不同,合并为 IR 字段没有语义意义 | | `tool_choice` (OAI 有 `allowed_tools`/MCP 变体) | 是否把所有 OAI 变体存 IR? | **IR 存规范化的 `ToolChoice` 枚举(none/auto/required/forced{name});OAI 特有变体(allowed_tools/MCP)存 `OpenAIResponsesExt.tool_choice_ext`** | 避免 IR 被 OAI 特有扩展污染 | | `prompt_cache_retention` 在 OAIChat 和 OAIResp 各出现一次 | 合并到 IR 还是各 Ext? | **各入对应 ProtocolExt** | 虽语义相同,但是否合并不影响跨协议互操作,保持 Ext 边界清晰 | | `disable_parallel_tool_use` (ANT ToolChoice) | 是否归 IR? | **归 IR `AiRequest.tool_config.disable_parallel`** | 与 OAI `parallel_tool_calls` 语义等价 | | `strict` (OAI FunctionDefinition, ANT Tool) | 归 IR ToolSpec 还是 Ext? | **归 IR `ToolSpec.strict: Option`** | ≥2 协议有,且 Encoder 需要读取 | | Google `responseSchema` vs `responseJsonSchema` | 两者并存时如何处理? | **`responseSchema` → `AiRequest.response_format`;`responseJsonSchema` → `GoogleExt.response_json_schema`;Encoder 优先 `response_json_schema` 如果存在** | responseJsonSchema 是 JSON Schema 标准格式,更推荐 | | ANT `top_k` | 归 IR 还是 ANT Ext? | **归 `AnthropicExt.top_k`** | 只有 Anthropic 支持,Google 虽有但在 GoogleExt | | Google `topK` | 归 IR 还是 GGL Ext? | **归 `GoogleExt.top_k`** | 与 ANT top_k 语义相同但不跨协议通用 |