首页
/ Headroom Phase C 解析:字节级 SSE 状态机与三大代理路由(/v1/messages、Chat Completions、Responses)的 Rust 化迁移

Headroom Phase C 解析:字节级 SSE 状态机与三大代理路由(/v1/messages、Chat Completions、Responses)的 Rust 化迁移

2026-09-05 15:15:37作者:段琳惟

本文基于仓库中的 Phase C 规划文档 REALIGNMENT/05-phase-C-rust-proxy.md 展开,讲解 Headroom 如何把剩余的代理接口(OpenAI Chat Completions 与 Responses,含 HTTP 与 SSE 流式)整体移植到 Rust:包括字节级 SSE 帧解析器、三套 provider 专属流式状态机、逐 item 类型的字节保真透传策略、Conversations API 感知,以及 Python responses_converter.py 的最终退役。读完你可以掌握"代理做压缩但绝不篡改透传字节"这一核心约束下,流式解析器与请求路径 handler 的完整设计,并能对照仓库源码验证每个 PR 的落地形态。

1. Phase C 的定位:目标、节奏与 PR 结构

Phase C 是 Headroom 整体"重对齐(Realignment)"九个阶段中的第三阶段,其目标(Goal)一句话概括:把剩余的代理接口移植到 Rust。Phase C 完成后,Rust 代理将完整承接 /v1/messages/v1/chat/completions/v1/responses(HTTP + 流式),并用一个字节级 SSE 状态机覆盖文档指南(guide §5)中列举的全部 wire-format 怪癖。

  • 时间盒:3 周,大部分顺序推进(每个 PR 都构建在前一个 PR 的 SSE 解析器之上);
  • 形态:5 个 PR(PR-C1 ~ PR-C5),风险等级从 HIGH 到 LOW 递减;
  • 前置依赖:PR-C1 依赖 Phase A 的 PR-A1(lockdown,先止血);PR-C2 还依赖 Phase B 的 PR-B3/B4(live-zone 压缩引擎);
  • 后置出口:PR-C3/C4/C5 共同阻塞 Phase H 的 PR-H1(Python 退役)。
PR 内容 风险 增量代码 分支
PR-C1 字节级 SSE 解析器 + 完整状态机 HIGH +1500 realign-C1-rust-sse-parser
PR-C2 /v1/chat/completions handler HIGH +1200 realign-C2-rust-chat-completions
PR-C3 /v1/responses HTTP handler HIGH +1500 realign-C3-rust-responses-http
PR-C4 /v1/responses 流式 + Conversations API 感知 MEDIUM-HIGH +800 realign-C4-rust-responses-streaming
PR-C5 删除 responses_converter.py LOW -267 / +20 realign-C5-retire-responses-converter

每个 PR 在 05-phase-C-rust-proxy.md 中都给出了独立的 worktree 目录(~/claude-projects/headroom-worktrees/realign-Cx-*)、明确的"Blocked by / Blocks / Rollback"三元组,且 Rollback 策略统一为 git revert——因为 Python 路径在 Phase H 之前始终保留作为回退通道,Rust 路径是"先影子、后接管"的渐进替换。

2. PR-C1:字节级 SSE 解析器与完整状态机

这是整个 Phase C 的地基(文档标注 Risk: HIGH,"foundational; many wire-format quirks; UTF-8 split-byte handling")。它一次性消灭六项已知缺陷:P1-8、P1-9、P1-14、P1-15、P1-17、P4-48,全部落在 crates/headroom-proxy/src/sse/ 下。

2.1 共同帧层(framing):为什么必须做在字节层面

05-phase-C-rust-proxy.mdframing.rs 的设计要求是:读取 bytes::Bytes 块,累积进 BytesMut 缓冲,在字节(而非字符串)里寻找 \n\n 事件分隔符,以 (event_name: Option<String>, data: Bytes) 形式产出完整事件;按完整事件解码 UTF-8,绝不按 chunk 解码;静默跳过 : ping 保活;识别 [DONE] 字面量。

对照仓库中已落地的 framing.rs,可以看清"按 chunk 解码"为何是致命伤:模块头注释直接指出,Python 代理曾用 errors="ignore" 逐个 TCP chunk 解码,任何跨块边界的 emoji 或非 ASCII 码点都会静默丢字节——生产遥测在 9 天内记录了 1946 次此类解析失败(即 P1-15)。Rust 侧的对应实现是:

  • SseFramer::push(chunk) 只做 extend_from_slice 追加,零长度 chunk 直接 no-op;
  • next_event() 调用 find_double_newline 在字节层面定位 \n\n(兼容 \r\n\r\n),用 BytesMut::split_to 做 O(1) 的零拷贝切分,注释明确说明"典型 SSE 事件小于 4KB,简单字节循环在缓存局部性上更优";
  • parse_event_block 按行解析 event: / data: 字段,data: 多行按 WHATWG 规范以 \n 拼接;data 负载以 Bytes 切片形式共享底层分配(零拷贝),UTF-8 解码推迟到状态机真正需要字符串时,且 SseEvent::data_str() 返回 Result 而不是 from_utf8_lossy——任何一处都不用 lossy 转换是项目的硬约束;
  • [DONE] 检测(is_done_sentinel)直接用字节比较 b"[DONE]",避免热路径上触碰 UTF-8 校验;done_seen 标记置位后仍容忍后续字节(某些 provider 会在 [DONE] 后追加尾随 \n\n)。

分层结构在 sse/mod.rs 中给出:[TCP bytes] → SseFramer → 三个状态机(AnthropicStreamState / ChunkState / ResponseState),帧层与 provider 无关,状态机只消费 SseEvent。该文件同时开列了本模块退役的 bug 清单(P1-8 缺 thinking_delta 分支、P1-9 缺 signature_delta、P1-14 缺 citations_delta、P1-15 UTF-8 跨块截断、P1-17 Responses item 按位置而非 id 键控、P4-48 OpenAI tool_call 的 id 在后续 chunk 被 None 覆盖),与文档 PR-C1 的 scope 完全对应。

2.2 Anthropic 流式状态机(anthropic.rs

规划文档给出了 AnthropicStreamState / BlockState 的设计草图;当前仓库的 anthropic.rs 与之逐字段吻合:

pub struct AnthropicStreamState {
    pub message_id: Option<String>,
    pub model: Option<String>,
    pub blocks: HashMap<usize, BlockState>,   // 按 index 键控
    pub current_block_index: Option<usize>,
    pub stop_reason: Option<String>,
    pub usage: UsageBuilder,
    pub status: StreamStatus,                 // Open | MessageStop | Errored
}

关键设计点(源码注释均可佐证):

  1. blocks 按 index 键控而非按到达顺序——规范并不保证单调递增到达,状态机容忍乱序交错(测试 interleaved_blocks_by_index);
  2. delta 类型全覆盖text_delta / thinking_delta 追加文本;signature_deltadelta.signature 逐字节保存(红acted thinking 的加密校验签名,代理不得改动);input_json_deltapartial_json 只累积字符串、到 content_block_stop 才一次性解析;citations_delta 追加 citation。未知 delta 类型打 tracing::warnevent=sse_unknown_event)而不是静默丢弃——这是项目"no-silent-fallbacks"约束的体现;
  3. 终态语义stop_reason 取自 message_delta 而非 message_stop(Anthropic 把最终 stop_reason 放在 delta 里);message_stop / error 为终态,终态后继续收事件只记日志不 panic。

文档为 PR-C1 规划的 Anthropic 侧测试(four_event_dance_text_blockthinking_delta_accumulatedsignature_delta_preserved_byte_equalinput_json_delta_concatenated_parsed_at_stopcitations_delta_accumulatedmessage_delta_finalizes_stop_reason_and_output_tokensmid_stream_error_event_handledinterleaved_blocks_by_index)在仓库中对应 tests/sse_anthropic.rs;帧层测试(utf8_split_emoji_across_chunks_preservedsingle_newline_does_not_emit_eventdouble_newline_emits_eventping_keepalive_skippeddone_sentinel_detectedtrailing_data_after_done_tolerated)对应 tests/sse_framing.rs

2.3 OpenAI 两套状态机(openai_chat.rs / openai_responses.rs

  • Chat Completionsopenai_chat.rs):ChunkState / ChoiceState / ToolCallState;处理 [DONE]stream_options.include_usage 的最终 usage chunk;tool_callid/name 只出现在首个 chunk,后续 chunk 只带 arguments 片段需累积拼接(对应 P4-48 的回归防护);
  • Responsesopenai_responses.rs):与 Chat Completions 不同,Responses 流使用命名事件(每事件一行 event:),需处理 response.createdoutput_item.added/donecontent_part.added/doneoutput_text.delta/donefunction_call_arguments.delta/donereasoning_summary.delta/doneresponse.completed/failed/incomplete

Responses 侧最核心的一条规则是 item 一律按 id(如 msg_abc123)键控,永不用位置:规范允许乱序完成(并行 reasoning + function_call 时 item 1 可能先于 item 0 完成),这正是 P1-17 的根因;测试 out_of_order_item_completion_by_id 专门守护此不变量。function_call_arguments 端到端保持字符串——代理从不把它再解析成 JSON("模型构造的字符串由模型解析")。当前实现的 ResponseState 还额外携带 service_tierincomplete_reason(从 response.completed 信封提取,供 proxy_service_tier_count_total / proxy_response_status_count_total 等 Phase G 指标使用)。

2.4 并行状态机:遥测不阻塞字节

PR-C1 对 crates/headroom-proxy/src/proxy.rs 的修改要求:转发流式响应时,状态机与字节透传并行运行——客户端立刻拿到原始字节,状态机只是填充遥测。sse/mod.rs 的模块注释明确确认:"None of these mutate the bytes flowing back to the client"。

2.5 PR-C1 验收标准

  • 全部新增测试通过;
  • 性质测试:proptest! { fn sse_parser_no_panic_on_arbitrary_bytes(bytes in any::<Vec<u8>>()) { let _ = parse(bytes); } } —— 10 万条随机字节序列绝不 panicframing.rsFramingError 文档也声明"framer 本身从不 panic;tests/sse_framing.rs 中的性质测试强制此约束");
  • 真实流量影子测试:把录制的生产 Anthropic 流同时喂给 Rust 与 Python 解析器,断言两者遥测的 usage 总量一致。

3. PR-C2:/v1/chat/completions handler 的 Rust 化

PR-C2(Risk: HIGH,+1200 LOC)把 /v1/chat/completions 加入 crates/headroom-proxy,范围覆盖请求体形状、live-zone 压缩分发、以及 PR-C1 的流式状态机,同时预埋 Phase E 的 PR-E1/E2 工具定义归一化 gate(在 Phase E 之前是 no-op)。

新增文件与职责(文档规划 → 仓库现状):

  • crates/headroom-proxy/src/handlers/chat_completions.rs(对应仓库 handlers/chat_completions.rs)——POST handler,规划签名:

    async fn handle_chat_completions(
        State(state): State<AppState>,
        headers: HeaderMap,
        body: Bytes,
    ) -> Result<Response>;
    
  • crates/headroom-proxy/src/compression/live_zone_openai.rs(对应仓库 live_zone_openai.rs)——OpenAI 的 live-zone 分发器。Chat Completions 的 live zone 只有两处:最新 tool role 消息的 content,以及最新 user 消息的文本内容;按类型感知的分发走与 Anthropic 完全相同的压缩器(复用,不另起炉灶)。这与 00-overview.md 确立的全局心智模型一致:透传神圣不可侵犯,只压 live zone,不动缓存热区(system prompt、tools、旧轮次、reasoning/thinking/compaction item)

修改lib.rs 中把 /v1/chat/completions(POST)路由到新 handler;compression/mod.rs 增加 OpenAI Chat 分发路径。

测试integration_chat_completions.rs,仓库中为 tests/integration_chat_completions.rs)共 7 个,覆盖:

测试 守护的不变量
passthrough_no_compression_byte_equal 压缩关闭时上游字节逐字节相等
tool_message_compressed tool role 消息进入 live zone 被压缩
n_greater_than_one_passthrough n > 1 时整体透传不改动
stream_options_include_usage_preserved usage chunk 原样保留
tool_choice_change_passthrough_no_mutation tool_choice 变更不改写请求
refusal_field_in_response_handled 响应中 refusal 字段被正确处理
streaming_tool_call_argument_accumulation delta.tool_calls[].function.arguments 流式累积

验收标准:压缩关闭时,真实 Chat Completions 请求经 Rust 代理产生的上游字节与原始请求逐字节相等;流式 tool_call 的参数累积对 delta.tool_calls[].function.arguments 模式有效。依赖与回滚:Blocked by PR-C1、PR-B3、PR-B4;Blocks PR-C3、PR-H1;回滚后 /v1/chat/completions 仍走 Python 代理("Phase H hasn't deleted it yet")。

4. PR-C3:/v1/responses HTTP handler 与逐 item 类型保真

PR-C3(Risk: HIGH,+1500 LOC)是 Phase C 中 wire-format 保真要求最高的一步:对每一种 Responses item 类型做 item-shape 透传保留——V4A patch、local_shell_call.action.command 的 argv 数组、Codex phase 字段、compaction、MCP item、computer_useimage_generation_call、服务端工具结果;同时只对 function_call_outputlocal_shell_call_outputapply_patch_call_output 做 live-zone 压缩,且规划文档规定仅在超过 2KB 时压缩

规划中的 ResponseItem 显式枚举(节选):

pub enum ResponseItem {
    Message { phase: Option<String>, .. },
    Reasoning { encrypted_content: Option<String>, .. },  // passthrough only
    FunctionCall { call_id: String, arguments: String, .. },  // arguments 保持字符串
    LocalShellCall { command: Vec<String>, .. },  // argv 数组原样保留
    ApplyPatchCall { operation: ApplyPatchOperation, .. },  // V4A diff 逐字
    Compaction { encrypted_content: String, .. },  // passthrough only
    McpCall { .. } | McpListTools { .. } | McpApprovalRequest { .. },  // passthrough
    ComputerCall { .. } | ComputerCallOutput { .. },
    WebSearchCall { .. } | FileSearchCall { .. } | CodeInterpreterCall { .. },
    ImageGenerationCall { .. },
    ToolSearchCall { .. },
    CustomToolCall { .. },
    Unknown { type_: String, raw: Box<RawValue> },  // 记警告;逐字保留
}

当前仓库的实现 responses_items.rs 遵循了同一套规则,且补充了规划中没有明说的 RawValue 双通道策略

  1. 不透明载荷(加密 reasoning、compaction blob、MCP/computer/web-search 等结果)一律透传,代理绝不重新序列化——重序列化可能破坏 provider 提示词缓存已经键控在其中的空白/键序/Unicode 转义不变量;
  2. 可压缩的只有三类 *_output item 的 output 字符串,且只压"每类中最新的一条",还要过 per-content-type 压缩器的 token 数下降校验;
  3. 未知 item 类型记 warn 日志并以 serde_json::value::RawValue 逐字节保留——"永远不删除我们不认识的 item"是 no-silent-fallbacks 契约:未来 OpenAI 新增 type 值时无需改代码即可继续流经代理;
  4. 序列化采用两遍式:先 &RawValue 持有原始字节切片(字节保真的真正载体),再对同一切片反序列化为类型化 ResponseItem(仅供遥测与决策,永不回写到线上字节);文档特别解释 serde(other) 只会存 tag 而丢数据,所以必须显式双通道。

两个值得注意的细节(均有源码注释佐证):

  • local_shell_call.action.command 必须保持 JSON 数组——拼成字符串会改变 shell 引用语义,与 Codex CLI 实际调起进程的方式不再等价;
  • apply_patch_call.operation.diffV4A patch 正文逐字量——重新序列化会改变缩进而导致 patch 无法应用;
  • function_call.arguments 是线上 JSON-encoded 字符串,"代理内部绝不当 JSON 解析"。

关于压缩下限阈值,文档规划写的是"only when >2KB",而当前仓库源码中的常量是 OUTPUT_ITEM_MIN_BYTES = 512;从源码结构看,实现采用的"output-item floor"比规划文档的 2KB 更保守(更早介入压缩),两者口径不一致这一点值得在实际接入时以代码为准并留意对应测试。

测试integration_responses.rs,仓库中为 tests/integration_responses.rs)共 15 个,逐一锁定各 item 类型的字节保真与压缩边界:v4a_patch_byte_equal_through_proxylocal_shell_call_command_argv_array_preservedcodex_phase_commentary_preservedcodex_phase_final_answer_preservedcompaction_item_byte_equalreasoning_encrypted_content_byte_equalfunction_call_arguments_string_preservedcall_id_referenced_not_idapply_patch_output_below_2kb_no_compressionapply_patch_output_above_2kb_compressedlocal_shell_output_compressedmcp_tool_call_byte_equalcomputer_call_byte_equalimage_generation_call_no_log_redaction_in_test_modeunknown_item_type_logged_warning_byte_equal

验收标准:一个包含 reasoning + function_call + local_shell + apply_patch + 自定义 item 的代表性 Responses 请求,往返后除 live-zone 压缩输出外逐字节相等。Blocked by PR-C1、PR-C2;Blocks PR-C4;回滚后 /v1/responses 仍走 Python。

5. PR-C4:/v1/responses 流式 + Conversations API 感知

PR-C4(Risk: MEDIUM-HIGH,+800 LOC)做两件事:

(1)Responses 流式。修改 handlers/responses.rs:当请求头为 Accept: text/event-stream 时走流式 handler,流式 handler 运行 PR-C1 的 OpenAIResponsesStreamState 状态机并与字节透传并行;同时在 sse/openai_responses.rs 中补充从 response.completed 提取 usage 的逻辑(即 openai_responses.rsResponseState.usage 的来源)。

(2)Conversations API 一等感知(P4-40)。新增 crates/headroom-proxy/src/conversations.rs(仓库中为 handlers/conversations.rs):当请求体中出现 conversation: {"id": "conv_..."} 时,说明本地视图不完整——历史在服务端而不在本请求里。文档规定的动作是:记一条警告,并对该请求禁用 live-zone 压缩(直到"Phase 4 跨请求共享缓存"落地),遥测计数器为 proxy_conversations_api_request_count_total。当前实现的 conversations handler 是显式逐路由绑定(创建/读/更新/删除、items 增删查)的"passthrough-with-instrumentation":不做正则 catch-all(项目 build constraints 明令禁止 regex 路由),不缓冲多 MB 的 items body(直接交给 forward_http 流式转发),每个请求在转发之前先发一条 event = "conversations_passthrough_pr_c4" 结构化日志(这样上游卡死也留有痕迹)。

测试integration_responses_streaming.rs + integration_conversations.rs,仓库中为 tests/integration_responses_streaming.rstests/integration_conversations.rs):reasoning_summary_streamed_correctlyfunction_call_arguments_streamed_byte_equalout_of_order_items_handled_by_idresponse_completed_usage_capturedresponse_failed_handledresponse_incomplete_with_max_output_tokens_reasonconversation_id_present_skips_compression_warns

验收标准:全部测试通过;Conversations API 警告以 INFO 级别出现在日志中并携带 conversation_id 字段。Blocked by PR-C3;Blocks PR-H1。

6. PR-C5:responses_converter.py 退役

这是收尾的"删除型" PR(Risk: LOW,-267/+20 LOC),删除 headroom/proxy/responses_converter.py——即 Anthropic ↔ OpenAI Responses ↔ Chat Completions 三方形状转换器,它曾错误处理 phase、多 text-part 重建等问题(responses_items.rs 头部注释也指出:每新增一种 OpenAI item 类型,这个转换器都会"静默坏掉"直到有人更新它)。

  • 删除headroom/proxy/responses_converter.py
  • 修改headroom/proxy/handlers/openai.py 移除对 responses_converter 的 import——原来"调用转换器把 Responses item 转成 Chat-Completions 消息再压缩"的分发路径整体消失,压缩由 Rust 原生处理;
  • 测试:删除 tests/test_responses_converter*.py 全部用例,该表面的覆盖由 crates/headroom-proxy/tests/integration_responses.rs 承接。

验收标准pytest -x 全绿;git grep responses_converter headroom/ 无输出。Blocked by PR-C3、PR-C4;Blocks PR-H1。回滚语义最特殊:Python 转换器可以恢复,与 Rust handler 短暂并存——"但 Rust 路径是 canonical"。

7. Phase C 验收总结与缺陷退役清单

5 个 PR 全部落地后的验收清单(原文照录):

  • ✅ 带完整状态机的字节级 SSE 解析器(UTF-8 跨块分裂、ping、[DONE]、所有 delta 类型、流中错误)
  • /v1/chat/completions 由 Rust 处理
  • /v1/responses HTTP 由 Rust 处理
  • /v1/responses 流式由 Rust 处理(乱序 item、全部事件类型)
  • ✅ Conversations API 感知(警告 + 跳过压缩)
  • ✅ 所有 Responses item 类型(V4A、local_shell、phase、compaction、MCP、computer、image_gen 等)逐字节保留
  • responses_converter.py 已删除

Phase C 共退役以下缺陷:P1-8 ~ P1-12、P1-14 ~ P1-17、P4-40、P4-42 ~ P4-44、P4-47、P4-48、P0-7(final)、P5-51。这些编号的完整定义见同目录的 REALIGNMENT/01-bug-list.md,全局动机("透传神圣,只压 live zone"的模型纠偏)见 REALIGNMENT/00-overview.md

8. 如何在仓库中验证 Phase C

Phase C 的实现集中在 crates/headroom-proxy 下,可按如下路径自查:

关注点 入口文件 对应测试
SSE 帧层 crates/headroom-proxy/src/sse/framing.rs tests/sse_framing.rstests/integration_sse.rs
Anthropic 状态机 crates/headroom-proxy/src/sse/anthropic.rs tests/sse_anthropic.rs
Chat 状态机 crates/headroom-proxy/src/sse/openai_chat.rs tests/sse_openai_chat.rs
Responses 状态机 crates/headroom-proxy/src/sse/openai_responses.rs tests/sse_openai_responses.rs
Chat Completions handler crates/headroom-proxy/src/handlers/chat_completions.rs tests/integration_chat_completions.rs
Responses handler / item 解析 crates/headroom-proxy/src/handlers/responses.rscrates/headroom-proxy/src/responses_items.rs tests/integration_responses.rs
流式 + Conversations crates/headroom-proxy/src/handlers/conversations.rs tests/integration_responses_streaming.rstests/integration_conversations.rs
live-zone 压缩分发 crates/headroom-proxy/src/compression/live_zone_openai.rscrates/headroom-proxy/src/compression/live_zone_responses.rscrates/headroom-proxy/src/compression/live_zone_anthropic.rs tests/integration_compression.rs

在本地运行 cargo test -p headroom-proxy 即可执行上述集成测试;Python 侧的对照回归则继续由 pytest 承接(PR-C5 的验收条件之一)。需要注意的适用前提:本文所有"当前实现"描述均基于仓库当前快照,且 Phase C 依赖 Phase A(lockdown)与 Phase B(live-zone 引擎)先行——若单独阅读本阶段,建议先读 03-phase-A-lockdown.md04-phase-B-live-zone.md 建立上下文。

9. 小结

Phase C 的技术价值不在"把 Python 换成 Rust",而在于它固化了一套可审计的代理约束体系:帧层只认字节、状态机只喂遥测、item 级逐字节保真、未知类型永不丢弃、压缩只碰 live zone 的最新输出且受 token 校验兜底。每一个约束都对应一个具名 bug 编号(P1-8~P1-17、P4-40、P4-48 等)和一个可回归的测试名(utf8_split_emoji_across_chunks_preservedsignature_delta_preserved_byte_equalout_of_order_item_completion_by_id……),使"代理改了什么、没改什么"成为可以在 CI 中机器验证的事实,而不是依赖代码评审的口头承诺。这套"规划文档给出 PR 级契约、源码注释回指 bug 编号、测试名一一对应验收项"的写法,也是 REALIGNMENT/ 全系列九个阶段文档的共性。

登录后查看全文
热门项目推荐
相关项目推荐