Headroom Phase C 解析:字节级 SSE 状态机与三大代理路由(/v1/messages、Chat Completions、Responses)的 Rust 化迁移
本文基于仓库中的 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.md 对 framing.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
}
关键设计点(源码注释均可佐证):
- blocks 按
index键控而非按到达顺序——规范并不保证单调递增到达,状态机容忍乱序交错(测试interleaved_blocks_by_index); - delta 类型全覆盖:
text_delta/thinking_delta追加文本;signature_delta的delta.signature逐字节保存(红acted thinking 的加密校验签名,代理不得改动);input_json_delta的partial_json只累积字符串、到content_block_stop才一次性解析;citations_delta追加 citation。未知 delta 类型打tracing::warn(event=sse_unknown_event)而不是静默丢弃——这是项目"no-silent-fallbacks"约束的体现; - 终态语义:
stop_reason取自message_delta而非message_stop(Anthropic 把最终 stop_reason 放在 delta 里);message_stop/error为终态,终态后继续收事件只记日志不 panic。
文档为 PR-C1 规划的 Anthropic 侧测试(four_event_dance_text_block、thinking_delta_accumulated、signature_delta_preserved_byte_equal、input_json_delta_concatenated_parsed_at_stop、citations_delta_accumulated、message_delta_finalizes_stop_reason_and_output_tokens、mid_stream_error_event_handled、interleaved_blocks_by_index)在仓库中对应 tests/sse_anthropic.rs;帧层测试(utf8_split_emoji_across_chunks_preserved、single_newline_does_not_emit_event、double_newline_emits_event、ping_keepalive_skipped、done_sentinel_detected、trailing_data_after_done_tolerated)对应 tests/sse_framing.rs。
2.3 OpenAI 两套状态机(openai_chat.rs / openai_responses.rs)
- Chat Completions(openai_chat.rs):
ChunkState/ChoiceState/ToolCallState;处理[DONE]与stream_options.include_usage的最终 usage chunk;tool_call的id/name只出现在首个 chunk,后续 chunk 只带arguments片段需累积拼接(对应 P4-48 的回归防护); - Responses(openai_responses.rs):与 Chat Completions 不同,Responses 流使用命名事件(每事件一行
event:),需处理response.created、output_item.added/done、content_part.added/done、output_text.delta/done、function_call_arguments.delta/done、reasoning_summary.delta/done、response.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_tier 与 incomplete_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 万条随机字节序列绝不 panic(framing.rs 的FramingError文档也声明"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 只有两处:最新toolrole 消息的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_use、image_generation_call、服务端工具结果;同时只对 function_call_output、local_shell_call_output、apply_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 双通道策略:
- 不透明载荷(加密 reasoning、compaction blob、MCP/computer/web-search 等结果)一律透传,代理绝不重新序列化——重序列化可能破坏 provider 提示词缓存已经键控在其中的空白/键序/Unicode 转义不变量;
- 可压缩的只有三类
*_outputitem 的output字符串,且只压"每类中最新的一条",还要过 per-content-type 压缩器的 token 数下降校验; - 未知 item 类型记 warn 日志并以
serde_json::value::RawValue逐字节保留——"永远不删除我们不认识的 item"是 no-silent-fallbacks 契约:未来 OpenAI 新增type值时无需改代码即可继续流经代理; - 序列化采用两遍式:先
&RawValue持有原始字节切片(字节保真的真正载体),再对同一切片反序列化为类型化ResponseItem(仅供遥测与决策,永不回写到线上字节);文档特别解释serde(other)只会存 tag 而丢数据,所以必须显式双通道。
两个值得注意的细节(均有源码注释佐证):
local_shell_call.action.command必须保持 JSON 数组——拼成字符串会改变 shell 引用语义,与 Codex CLI 实际调起进程的方式不再等价;apply_patch_call.operation.diff是 V4A 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_proxy、local_shell_call_command_argv_array_preserved、codex_phase_commentary_preserved、codex_phase_final_answer_preserved、compaction_item_byte_equal、reasoning_encrypted_content_byte_equal、function_call_arguments_string_preserved、call_id_referenced_not_id、apply_patch_output_below_2kb_no_compression、apply_patch_output_above_2kb_compressed、local_shell_output_compressed、mcp_tool_call_byte_equal、computer_call_byte_equal、image_generation_call_no_log_redaction_in_test_mode、unknown_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.rs 中 ResponseState.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.rs 与 tests/integration_conversations.rs):reasoning_summary_streamed_correctly、function_call_arguments_streamed_byte_equal、out_of_order_items_handled_by_id、response_completed_usage_captured、response_failed_handled、response_incomplete_with_max_output_tokens_reason、conversation_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/responsesHTTP 由 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 下,可按如下路径自查:
在本地运行 cargo test -p headroom-proxy 即可执行上述集成测试;Python 侧的对照回归则继续由 pytest 承接(PR-C5 的验收条件之一)。需要注意的适用前提:本文所有"当前实现"描述均基于仓库当前快照,且 Phase C 依赖 Phase A(lockdown)与 Phase B(live-zone 引擎)先行——若单独阅读本阶段,建议先读 03-phase-A-lockdown.md 与 04-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_preserved、signature_delta_preserved_byte_equal、out_of_order_item_completion_by_id……),使"代理改了什么、没改什么"成为可以在 CI 中机器验证的事实,而不是依赖代码评审的口头承诺。这套"规划文档给出 PR 级契约、源码注释回指 bug 编号、测试名一一对应验收项"的写法,也是 REALIGNMENT/ 全系列九个阶段文档的共性。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00