02 协议、分帧与传输分离 本节摘要:上一节讲了 Route 如何组合「一个部署」,但部署确定后,「请求体怎么构造、流式响应怎么解析」还是一团乱麻——每家厂商的请求体格式不同、流式 chunk 格式不同。OpenCode 的解法是把这件事再拆成三层:协议(Protocol)负责构造请求体与解析流式响应,分帧(Framing)管字节流如何切成事件,传输(Transport)管 HTTP 连接细节。本节讲清这三层各自的职责,以及为什么这种分离让「支持一个新协议」变得 mostly 是写一个协议模块。 一、为什么还要再拆 上一节的 Route 把「一个部署」抽象成了字段组合,其中有个字段叫 。
本节摘要:上一节讲了 Route 如何组合「一个部署」,但部署确定后,「请求体怎么构造、流式响应怎么解析」还是一团乱麻——每家厂商的请求体格式不同、流式 chunk 格式不同。OpenCode 的解法是把这件事再拆成三层:**协议(Protocol)**负责构造请求体与解析流式响应,**分帧(Framing)**管字节流如何切成事件,**传输(Transport)**管 HTTP 连接细节。本节讲清这三层各自的职责,以及为什么这种分离让「支持一个新协议」变得 mostly 是写一个协议模块。
上一节的 Route 把「一个部署」抽象成了字段组合,其中有个字段叫 protocol。但协议本身还是个复杂的东西——它要知道:
如果把这部分也塞进 Route,Route 会变得臃肿。所以 OpenCode 把它单独拎出来,并且进一步拆成协议、分帧、传输三层,各管一件事。
协议层是最核心的一层。它的职责是翻译:
provider 中立请求 厂商请求体 { system, messages, tools } ──►协议──► { 厂商专用格式 } 厂商流式响应 统一事件 data: { 厂商chunk } ──►协议──► LLMEvent(text-delta / tool-call / ...)
OpenCode 内置了一批协议,每个对应一种厂商 API 风格:
| 协议 | 对应风格 |
|---|---|
| anthropic-messages | Anthropic 的 Messages API |
| openai-chat | OpenAI 的 Chat Completions |
| openai-responses | OpenAI 的 Responses API(新) |
| openai-compatible-chat | 任何 OpenAI 兼容端点 |
| gemini | Google Gemini |
| bedrock-converse | Bedrock 的 Converse API |
| bedrock-event-stream | Bedrock 的事件流 |
注意:协议 ≠ 厂商。一个厂商可能用多个协议(OpenAI 有 chat 和 responses 两种),多个厂商可能共用一个协议(很多家用 openai-compatible-chat)。这种「协议与厂商解耦」正是灵活性的来源。
💡 支持一个新协议,mostly 是写一个协议模块:它要实现「中立请求 → 厂商请求体」和「厂商流 → 统一事件」两组翻译。协议模块之外的东西(传输、分帧、Route)都能复用。这就是分层的好处。
协议管的是「事件级的翻译」,但流式响应在网络上是一串字节,怎么切成一个个可解析的单元?这是分帧的事。
网络字节流: b'{"type":"text","delta":"你好"}\n{"type":"text","delta":"世界"}\n...' │ ▼ 分帧 帧1: '{"type":"text","delta":"你好"}' 帧2: '{"type":"text","delta":"世界"}' │ ▼ 协议解析每个帧 LLMEvent(text-delta "你好"), LLMEvent(text-delta "世界")
不同厂商的分帧方式不同:
data: 前缀 + 空行分隔。分帧层把这些差异吸收,给协议层提供「一个一个的帧」,协议层不用关心字节流是怎么切的。
最底层是传输,管 HTTP 连接细节:怎么发请求、怎么处理超时、怎么重试、怎么处理流式连接的建立与断开。这一层把 HTTP 的脏活累活扛下来,协议和分帧不用操心。
传输层:HTTP 请求/响应/流连接/超时/重试 ▲ │ 提供字节流 分帧层:把字节流切成帧 ▲ │ 提供帧 协议层:把帧解析成统一事件 / 把请求翻译成厂商格式
把协议、分帧、传输分开,换来三样东西:
传输层几乎对所有厂商都一样(HTTP 就是 HTTP),分帧层按几种标准(SSE/JSONL/二进制)分类,真正厂商专属的只有协议层。所以支持新厂商时,传输和分帧大概率复用,只写协议。
三层可以单独测:协议层用 mock 的帧测翻译正确性,分帧层用 mock 字节流测切帧,传输层测连接管理。每层测试又快又聚焦。
想换一个 HTTP 库?只动传输层。想支持一种新的分帧方式?只动分帧层。协议和传输互不干扰。
把三层串起来,看一次完整请求:
provider 中立请求 │ ▼ 协议层:翻译成厂商请求体 厂商请求体 JSON │ ▼ 传输层:发 HTTP 请求(建立流连接) │ ▼ 厂商返回字节流 │ ▼ 分帧层:切成帧 帧1, 帧2, 帧3, ... │ ▼ 协议层:每个帧解析成统一事件 LLMEvent, LLMEvent, LLMEvent, ... │ ▼ 给上层(会话核心)
注意:请求出去时,协议在前(翻译),传输在后(发);响应回来时,分帧在前(切),协议在后(解析)。三层各司其职,数据流畅通。
协议把厂商流解析成了「统一事件」,但「统一事件」到底长什么样?这就是下一节的 LLMEvent。