# IR 演进日志(CHANGELOG) > 记录每次 IR 结构变更:新增字段/变体、语义变更、删除字段、重命名。 > **格式规范**:每个 PR 合并后在此追加条目,格式参照下方模板。 > 阅读顺序:最新条目在上方。 --- ## [PR-C] 6 个 codec trait 统一重命名 — 2026-05-16 ### 重命名 / 变更 **`protocol/mod.rs` — 6 个 trait 重命名** | 旧名 | 新名 | |------|------| | `IngressDecoder` | `RequestDecoder` | | `EgressEncoder` | `RequestEncoder` | | `ResponseParser` | `ResponseDecoder` | | `ResponseFormatter` | `ResponseEncoder` | | `StreamParser` | `StreamResponseDecoder` | | `StreamFormatter` | `StreamResponseEncoder` | **`protocol/traits.rs` — `EndpointHandler` 工厂方法重命名** | 旧名 | 新名 | |------|------| | `make_decoder` | `make_request_decoder` | | `make_encoder` | `make_request_encoder` | | `make_response_parser` | `make_response_decoder` | | `make_response_formatter` | `make_response_encoder` | | `make_stream_parser` | `make_stream_response_decoder` | | `make_stream_formatter` | `make_stream_response_encoder` | ### 删除 - `protocol/traits.rs`:移除 `pub use EndpointHandler as ProtocolHandler;` 别名 ### 清理 - `openai_compatible/embeddings.rs`:doc comment 中 `ProtocolHandler` → `EndpointHandler` - 全部 4 个 codec `EndpointHandler impl`、~50 处调用点、所有 use 语句同步替换 ### 完成态 - 数据流向一目了然:`RequestDecoder → AiRequest → RequestEncoder → wire → Provider → wire → ResponseDecoder → AiResponse`,流式路径 `StreamResponseDecoder → Vec → StreamResponseEncoder` - 无任何旧名残留,编译器负责全量校验 --- ## [PR-B] Stream Parser 直产 AiStreamDelta + 删除遗留文件 — 2026-05-16 ### 重命名 / 变更 **4 个 codec stream parser(`openai_compatible/stream.rs`、`anthropic_messages/stream.rs`、`google_generative/stream.rs`、`openai_responses/parser.rs`)** - 内部状态机从产出 `types::StreamDelta` 改为直产 `ir::AiStreamDelta`,消除 `old_stream_delta_to_new` 转换层 - `StreamDelta::ReasoningDelta` → `AiStreamDelta::ThinkingDelta` - `StreamDelta::ReasoningSignature` → `AiStreamDelta::ThinkingSignature` - `StreamDelta::RawEvent { event_type, data }` → `AiStreamDelta::Unknown { raw }` (格式:`"event: TYPE\ndata: DATA"`) **4 个 codec stream formatter(`openai_compatible/stream.rs`、`anthropic_messages/stream.rs`、`google_generative/stream.rs`、`openai_responses/stream.rs`)** - `format_deltas` 直接 match `AiStreamDelta` 变体,消除 `ai_stream_delta_to_old` 转换层 **`parse_response` 中的 `types::ToolCall` 临时变量**(`openai_compatible`、`anthropic_messages`、`google_generative`、`openai_responses`):改为直接构造 `ir::request::ToolCall`,去除 `.map(|tc| ir::request::ToolCall {...})` 转换 ### 删除 - `crates/nyro-core/src/protocol/types.rs` — 整文件删除(`StreamDelta`、`ToolCall` 已无消费方) - `crates/nyro-core/src/protocol/ir/compat.rs` — 整文件删除(`old_stream_delta_to_new`、`ai_stream_delta_to_old` 不再被调用) - `crates/nyro-core/src/provider/stream.rs` — 整文件删除(`ProviderStreamParser` trait、`LegacyStreamParserAdapter`) ### 清理 - `protocol/mod.rs`:移除 `pub mod types;` - `protocol/ir/mod.rs`:移除 `pub mod compat;`,更新模块 doc comment - `provider/mod.rs`:移除 `pub mod stream;` 及 `LegacyStreamParserAdapter`/`ProviderStreamParser` re-export - `provider/vendor.rs`:从 `Vendor` trait 移除 `fn stream_parser()` - `provider/common/pipeline.rs`:移除 `stream_parser()` 工厂函数及相关 doc comment - 全部 13 个 vendor `mod.rs`:移除 `stream_parser()` impl 及 `ProviderStreamParser` import - `tests/passthrough_fidelity.rs`:移除 `ProviderStreamParser` import 及两处 `unreachable!()` stub - `integrations/mod.rs`:更新 doc comment 中的废弃类型引用 ### 完成态 - 仓内不再有 `protocol/types.rs` / `protocol/ir/compat.rs` / `provider/stream.rs` - 不再有 `LegacyStreamParserAdapter` 或 `ProviderStreamParser` - IR 路径全栈端到端:`Decoder → AiRequest → Encoder → wire → Provider → wire → Parser → AiResponse / AiStreamDelta`,无任何中间老类型 - 五个文件净删除,约 1600 行遗留代码清零 --- ## [PR-A] TokenUsage 重塑 + Encoder 直吃 IR — 2026-05-15 ### 新增 **`ir/usage.rs`**(新文件) - `Usage` struct(字段:`prompt_tokens`, `completion_tokens`, `total_tokens`, `cache_read_tokens?`, `cache_creation_tokens?`, `server_tool_use?`) - `ServerToolUsage` struct(移自 `types.rs`) - `ir::mod.rs` 新增 `pub mod usage` 并 re-export `Usage`, `ServerToolUsage` ### 重命名 / 变更 **`ir::Usage`(原 `types::TokenUsage`)字段重命名** - `input_tokens` → `prompt_tokens` - `output_tokens` → `completion_tokens` - `cache_read_input_tokens` → `cache_read_tokens`(改为 `Option`) - `cache_creation_input_tokens` → `cache_creation_tokens`(改为 `Option`) **`ir/response.rs`**:`AiResponse.usage: TokenUsage` → `ir::Usage` **`ir/stream.rs`**:`AiStreamDelta::Usage(TokenUsage)` → `AiStreamDelta::Usage(ir::Usage)` **`cache/entry.rs`**:`CacheEntry.usage: TokenUsage` → `ir::Usage` **`cache/key.rs`**:`CODEC_SCHEMA_VERSION` 3 → 4(缓存 key 格式变更) **`logging/mod.rs`**:`LogEntry.usage: TokenUsage` → `ir::Usage` **`proxy/observability.rs`**:`emit_log` 参数 `TokenUsage` → `ir::Usage` **`proxy/stream.rs`**:`StreamBridge.final_usage` 及相关方法 → `ir::Usage` **`proxy/dispatcher/mod.rs`**:`LogBuilder.usage` → `ir::Usage` **`proxy/dispatcher/accumulator.rs`**:`StreamResponseAccumulator.usage` → `ir::Usage` **`protocol/mod.rs`**:`StreamFormatter::usage()` 返回类型 → `ir::Usage` **4 个 codec `stream.rs`/`parser.rs`**(anthropic、openai_compatible、google_generative、openai_responses): - 内部 usage 累加器字段改为 `ir::Usage` - `extract_*_usage` 函数返回 `ir::Usage`,JSON wire key 保持原样(`input_tokens`→`prompt_tokens` 仅限 IR 侧) - `storage/postgres/mod.rs`、`storage/sqlite/mod.rs`:bind 改用 `prompt_tokens`/`completion_tokens` **4 个 encoder 直接消费 IR 类型**(`anthropic_messages`、`openai_compatible`、`google_generative`、`openai_responses`): - 移除 `compat::ai_msg_to_old_ref`、`ai_tool_choice_to_value`、`ai_tool_spec_to_old_ref` 调用 - 直接接受 `&ir::Message`、`&ir::ContentBlock`、`&ir::ToolSpec` - `ToolChoice::Tool(String)` → `ToolChoice::Named { name: String }` ### 删除 **`types.rs`** - `InternalMessage`, `Role`, `MessageContent`, `ContentBlock`, `ImageSource`, `ToolDef`, `ResponseItem`, `TokenUsage`, `ServerToolUsage`(仅保留 `ToolCall`、`StreamDelta`,PR-B 再删) **`compat.rs`**(by-ref encoder helpers) - `ai_msg_to_old_ref` - `ai_tool_choice_to_value` - `ai_tool_spec_to_old_ref` **`tests/protocol_conversion.rs`** - 删除本地 `InternalRequest`/`InternalResponse` shim 及所有 `From` 实现、`ir_msg_from_old`、`ir_block_from_old` 辅助函数 - ~30 个测试全部改为直接构造 `AiRequest`/`IrAiResponse`,不再经过 shim 转换 --- ## [PR-6] 删除 InternalRequest / InternalResponse + 清理 compat.rs — 2026-05-15 ### 删除 **`protocol/types.rs`** - `InternalRequest` struct — 全流程已用 `AiRequest` 替代 - `InternalResponse` struct — 全流程已用 `AiResponse` 替代 **`ir/compat.rs`** - `From for AiRequest` / `From for InternalRequest` - `From for AiResponse` / `From for InternalResponse` - 与 `InternalRequest`/`InternalResponse` 相关的所有 by-value 转换函数 - Round-trip 测试(`round_trip_internal_request`) ### 变更 **保留**(仍用于 codec 内部辅助) - `types.rs` 内:`InternalMessage`, `Role`, `MessageContent`, `ContentBlock`, `ImageSource`, `ToolCall`, `ToolDef`, `ResponseItem`, `StreamDelta`, `TokenUsage`, `ServerToolUsage` - `compat.rs` 内:by-ref helpers (`ai_msg_to_old_ref`, `ai_tool_choice_to_value`, `ai_tool_spec_to_old_ref`) + StreamDelta 双向转换函数 **`codec/reasoning.rs`**:改为接受 `&mut AiResponse` **`codec/tool_correlation.rs`**:完全重写,使用 `AiRequest`/`ir::Message` **`pipeline.rs`**:移除 compat 圈子转换,`normalize_tool_results` 和 `reasoning` 直接调用 **4 个 ResponseFormatter**:移除 `let resp: InternalResponse = resp.clone().into()` 内部转换 **4 个 ResponseParser**:直接构造 `AiResponse`,移除 `AiResponse::from(InternalResponse {...})` **`tests/protocol_conversion.rs`** - 删除 `ir_compat_preserves_per_message_reasoning_extra`(测试已删除的 compat round-trip) - 添加本地 `InternalRequest`/`InternalResponse` shim + `From` 实现,其余 36 个测试零改动 --- ## [PR-5] Dispatcher / Provider Adapter / Cache 全切到新 IR — 2026-05-15 ### 变更 **`CacheEntry`** (`cache/entry.rs`) - `internal_response: Option` → `Option` **`cache/key.rs`** - `build_cache_key` 参数 `&InternalRequest` → `&AiRequest` - `CODEC_SCHEMA_VERSION` v2 → v3(字段映射变更,清空旧缓存) **Integration Hooks** (`integrations/hooks.rs`) - `RequestHook::on_request`: `&mut InternalRequest` → `&mut AiRequest` - `ResponseHook::on_response`: `&mut InternalResponse` → `&mut AiResponse` **Vendor 层** - `Vendor::build_request`: `&mut InternalRequest` → `&mut AiRequest` - `Vendor::parse_response`: return `InternalResponse` → `AiResponse` - Hook 方法 (`pre_encode`, `post_parse`, `pre_request`, `on_stream_delta`) 全部切换新类型 - `VendorExtension` 及其 blanket impl 同步更新 - Ollama `pre_request`:`req.extra.remove(...)` 改为 `req.meta.vendor.ingress.remove(...)` **`pipeline.rs`** - `build_request` 参数 `&mut InternalRequest` → `&mut AiRequest`;移除 `ai_req = req.clone().into()` 中转 - `parse_response` 返回 `AiResponse`;移除 `ai_resp.into()` 中转 - `egress_path` 传参 `req.stream` → `req.stream.enabled` **11 个 vendor mod.rs(批量)** - `build_request` / `parse_response` 签名同步切换 **`StreamResponseAccumulator`** (`dispatcher/accumulator.rs`) - 全面切换 `AiStreamDelta`;`into_internal_response()` 改为 `into_ai_response()` 返回 `AiResponse` **Dispatcher** (`dispatcher/mod.rs`) - 移除 `let mut internal: InternalRequest = request.into()`;全程直接使用 `request: AiRequest` - `replay_cached_stream` / `split_text_deltas` / 新增 `ai_response_to_deltas` 切换为 `AiStreamDelta` **`dispatcher/non_stream.rs` + `stream.rs`** - 移除所有 `AiResponse::from(...)` 和 `ai_stream_delta_to_old(...)` 包装 - Accumulator 直接调用 `.apply_all(&ai_deltas)` / `.into_ai_response()` - Cache entry `internal_response` 直接存 `AiResponse` **`dispatcher/util.rs`** - `request_has_image_input` / `extract_semantic_embedding_input` 改为 `&AiRequest` ### 不变 - `compat.rs` 本体保留(供测试和外部使用) - 老 `InternalRequest`/`InternalResponse` 类型仍在 `types.rs`(PR-6 删除) - Parser/Formatter/StreamParser/StreamFormatter 已在 PR-3/PR-4 完成切换 --- ## [PR-4] Codec Parser + Formatter 全切换到 AiResponse / AiStreamDelta — 2026-05-15 ### 变更 **4 个 trait 签名更新** - `ResponseParser::parse_response` 返回 `AiResponse`(原 `InternalResponse`) - `ResponseFormatter::format_response` 参数改为 `&AiResponse` - `StreamParser::parse_chunk` / `finish` 返回 `Vec` - `StreamFormatter::format_deltas` 参数改为 `&[AiStreamDelta]` **`compat.rs` 新增 StreamDelta 双向转换** - `old_stream_delta_to_new` — `&OldStreamDelta → AiStreamDelta` - `ai_stream_delta_to_old` — `&AiStreamDelta → OldStreamDelta` **4 套 Codec 实现(stream.rs + parser.rs + formatter.rs)** - Parsers:内部仍构造 `InternalResponse`,边界 `Ok(AiResponse::from(...))` 转换 - Formatters:入口 `let resp: InternalResponse = resp.clone().into();` 转换 - StreamParsers:内部构造 `Vec`,出口 `.map(old_stream_delta_to_new)` 转换 - StreamFormatters:入口 `let old: Vec = deltas.iter().map(ai_stream_delta_to_old).collect();` 转换 - Embeddings 存根签名同步更新 **Dispatcher 适配(stream.rs + non_stream.rs + mod.rs)** - 所有 `format_response(&internal)` 改为 `format_response(&AiResponse::from(internal.clone()))` - 所有 `accumulator.apply_all(&deltas)` 在 `Vec` 上加 `ai_stream_delta_to_old` 转换 - `replay_cached_stream` 中旧 `StreamDelta` 先转新再传给 formatter **Provider 层适配** - `LegacyStreamParserAdapter`:`parse_chunk` / `finish` 将 `Vec` 转换回 `Vec` 供 ProviderStreamParser 调用方 - `pipeline.rs::parse_response`:codec 返回 `AiResponse` 后 `.into()` 转回 `InternalResponse` ### 不变 - `StreamResponseAccumulator` 仍用 `StreamDelta`(PR-5 会迁移) - Provider Vendor trait 签名不变(PR-5 迁移) --- ## [PR-3] Codec Encoder 全切换到 AiRequest — 2026-05-15 ### 变更 **`EgressEncoder` trait** - `encode_request` 参数类型由 `&InternalRequest` → `&AiRequest` **4 大 Encoder 重写** - `OpenAIEncoder` — 直接读取 `AiRequest`;`req.extra` → `req.meta.vendor.ingress`;标量字段改用 `req.generation.*` - `ResponsesEncoder` — 同上;消息先通过 `ai_msg_to_old_ref` 转换后复用原有逻辑 - `AnthropicEncoder` — 同上;`__anthropic_raw_*` 字段从 `ingress` 读取 - `GoogleEncoder` — 同上;`__google_*` 字段从 `ingress` 读取 **`EmbeddingsEncoder` 更新** - 参数同步改为 `&AiRequest`;`req.extra` → `req.meta.vendor.ingress` **`compat.rs` 新增 by-ref 辅助** - `ai_msg_to_old_ref` / `ai_tool_choice_to_value` / `ai_tool_spec_to_old_ref` **`pipeline.rs` 适配** - `build_request` 步骤 4 加一行:`let ai_req = req.clone().into();` 后调用 `encoder.encode_request(&ai_req)` **集成测试更新** - `protocol_registry.rs`:直接使用 decoder 返回的 `AiRequest` 传给 encoder(移除 `.into()` 中转) - `protocol_conversion.rs`:`InternalRequest` 构造的测试改为 `.encode_request(&req.clone().into())` ### 不变 - Parser / StreamParser / Formatter / StreamFormatter 均未修改 - `compat.rs` 核心双向转换逻辑不变 --- ## [PR-2] Codec Decoder 全切换到 AiRequest — 2026-05-15 ### 变更 **`IngressDecoder` trait** - `decode_request` 返回类型由 `InternalRequest` → `AiRequest` **`GenerationConfig` 清理** - 移除临时字段 `logit_bias`、`n`、`top_k`(已归属 `ProtocolExt`) **4 大 Decoder 重写** - `OpenAIDecoder` — 直出 `AiRequest`;`ProtocolExt::OpenAiChat(OpenAIChatExt)` - `audio / modalities / logit_bias / n / prediction / stream_options` 进 `OpenAIChatExt` - `service_tier / user` 进 `meta.vendor.ingress`(老 encoder 向后兼容) - `reasoning_effort` → `ReasoningConfig`;`stop` → `GenerationConfig.stop` - `ResponsesDecoder` — 直出 `AiRequest`;`ProtocolExt::OpenAiResponses(OpenAIResponsesExt)` - `background / previous_response_id / truncation / include` 进 `OpenAIResponsesExt` - `reasoning` 字段 → `ReasoningConfig`;`reasoning_content` 附加到 `Message.meta` - `AnthropicDecoder` — 直出 `AiRequest`;`ProtocolExt::Anthropic(AnthropicExt)` - `ContentBlock` 全面升级:`Thinking`、`Document`、`Audio`、`cache_control` 原生支持 - 内置工具进 `AnthropicExt.server_tools`;用户工具进 `AiRequest.tools`(带 `cache_control`) - `thinking` → `ReasoningConfig`;`stop_sequences` → `GenerationConfig.stop` - 原始 wire JSON 保留在 `meta.vendor.ingress`(`__anthropic_raw_*`,兼容旧 encoder) - `GoogleDecoder` — 直出 `AiRequest`;`ProtocolExt::Google(GoogleExt)` - `decode_with_model` 签名同步更新(model + stream 由 URL 路径注入) - `executableCode / codeExecutionResult` → `ContentBlock::ExecutableCode / CodeExecutionResult` - `thought=true` Part → `ContentBlock::Thinking` - `generationConfig` 扩展字段进 `GoogleExt`;`safety_settings` → `AiRequest.safety_settings` - `__google_*` 原始字段保留在 `meta.vendor.ingress`(兼容旧 encoder) **`EmbeddingsDecoder` 更新** - 返回类型同步改为 `AiRequest`;`__emb_*` 键保留在 `meta.vendor.ingress` **5 个 Ingress Handler + Dispatcher** - 移除 `let request: AiRequest = internal.into()` 一行(decoder 直出 `AiRequest`) **`compat.rs` 修复** - `block_to_old` 补充 `MediaSource::Url` / `MediaSource::FileId` → `OldContentBlock::Image` 映射 ### 不变 - Encoder / Parser / Formatter 均未修改;通过 `AiRequest → InternalRequest`(compat.rs)继续工作 - `compat.rs` 双向转换逻辑核心不变 --- ## 模板 ``` ## [PR-N] <标题> — YYYY-MM-DD ### 新增 - `TypeName::field_name: Type` — 说明 ### 变更(语义或类型改动) - `TypeName::field_name`: `OldType` → `NewType` — 原因 ### 删除 - `TypeName::field_name` — 已被 X 替代 ### 重命名 - `OldName` → `NewName` — 原因 ``` --- ## [PR-0] 设计文档骨架 — 2026-05-14 ### 新增(文档) - `docs/design/ir/FIELD_HOMING.md` — 字段归属决策表(4 协议全字段 × 归属/依据) - `docs/design/ir/CHANGELOG.md` — 本文件 - `docs/design/ir/README.md` — 目录导航与 IR 设计概览 --- ## [PR-1] IR 类型重塑 + 流式事件分层 + Schema 抽象 — 2026-05-15 ### 新增 **新模块** - `ir/cache.rs` — `CacheControl { ttl: CacheTtl, breakpoint_priority: u8 }` / `CacheTtl { Ephemeral5m, Ephemeral1h }` - `ir/error.rs` — `AiError { kind, message, status_code, raw }` / `AiErrorKind` (15 变体) + `is_retryable()` - `ir/ext.rs` — `ProtocolExt` 枚举 + `OpenAIChatExt` / `OpenAIResponsesExt` / `AnthropicExt` / `GoogleExt` - `ir/schema.rs` — `SchemaObject` (JSON Schema 归一化,`to_google_wire()` 大写转换) **ContentBlock 新变体** - `ContentBlock::Thinking { thinking, signature? }` ← 重命名自 `Reasoning`(字段 `text` → `thinking`) - `ContentBlock::RedactedThinking { data }` — Anthropic redacted thinking - `ContentBlock::Audio { source: MediaSource }` — 音频内容块 - `ContentBlock::File { source: MediaSource }` — 文件内容块 - `ContentBlock::Document { source, title?, context?, cache_control? }` — Anthropic DocumentBlockParam - `ContentBlock::SearchResult { content, source, title, cache_control? }` — Anthropic SearchResultBlockParam - `ContentBlock::ServerToolUse { id, name, input, server_type?, cache_control? }` — 服务端工具调用 - `ContentBlock::ServerToolResult { tool_use_id, content, server_type?, cache_control? }` — 服务端工具结果 - `ContentBlock::Citation { cited_text, source }` — 引用块 - `ContentBlock::ExecutableCode { code, language?, id? }` — Google executableCode - `ContentBlock::CodeExecutionResult { return_code, stdout, stderr, id? }` — 代码执行结果 - `ContentBlock::ContainerUpload { file_id, cache_control? }` — Anthropic 容器上传 - `ContentBlock::Refusal { refusal }` — 模型拒绝 **ContentBlock 已有变体扩展** - `ContentBlock::Image`: `media_type/data` → `source: MediaSource` + `cache_control?` - `ContentBlock::Text`: 新增 `cache_control?` - `ContentBlock::ToolUse`: 新增 `cache_control?` - `ContentBlock::ToolResult`: 新增 `is_error?` + `cache_control?` **新类型** - `MediaSource { Base64 { media_type, data }, Url(String), FileId { file_id, detail? } }` - `DocumentSource { Base64Pdf, PlainText, Url, Blocks }` - `ReasoningEffort { None, Minimal, Low, Medium, High, Xhigh, Budget(u32) }` **AiRequest 新字段** - `disable_parallel_tool_calls: Option` — 与 ANT `disable_parallel_tool_use` 对应 - `ext: Option` — 协议域 Ext 载体 **ToolSpec 新字段** - `strict: Option` — OAI + ANT strict schema 校验 - `cache_control: Option` — ANT 工具级别缓存断点 **ReasoningConfig 扩展** - `effort: Option` 类型从 `Option` 改为强类型 enum - `display: Option` — ANT thinking display 模式 **AiResponse 新字段** - `error: Option` — 规范化错误(非 2xx 或内容过滤时填充) **AiStreamDelta 新变体** - `StreamDelta::ThinkingDelta(String)` ← 重命名自 `ReasoningDelta` - `StreamDelta::ThinkingSignature(String)` ← 重命名自 `ReasoningSignature` - `StreamDelta::StreamError { error: AiError }` — 流式中途错误 - `StreamDelta::UnexpectedEof` — 流被截断 ### 变更(语义) - `ContentBlock::Reasoning { text, signature }` → `ContentBlock::Thinking { thinking, signature }` — 字段名 `text` 改为 `thinking`;compat.rs 已更新做透明桥接 - `ResponseItem::Reasoning { text }` → `ResponseItem::Thinking { text }` - `GenerationConfig`: 标注 `logit_bias` / `n` / `top_k` 为 TODO(PR-2) 待迁移到 ProtocolExt ### 删除 - `AiRequest::modalities` 字段 — 已移入 `OpenAIChatExt.modalities`