读 protocol.py 与 io_struct.py:公开协议怎样变成内部对象#

这章解决什么问题#

代码导读前面已经把 entrypoints/openai 的 handler 骨架讲出来了,但还有一层非常值得单独补:请求在协议层用的是 Pydantic request/response model,而进入 runtime 之后又会变成 dataclass 风格的内部对象。这条桥如果不讲清楚,读者会很容易在 protocol.pyio_struct.py 之间反复跳,却始终不确定“这两个文件到底是不是在描述同一条请求”。

这一章就是把这条桥讲透。

为什么这层对读仓库特别重要#

如果你只读 protocol.py,你会以为系统关心的是 OpenAI-compatible request schema;如果你只读 io_struct.py,你又会以为系统关心的是 manager 之间传递的运行时对象。真正的情况是:这两者分别站在同一条请求的外层和内层。

这也是为什么这章更适合放在代码导读而不是 API 章节里。它不是解释协议本身,而是解释“协议对象怎样真正走进运行时内部”。

一张图:协议对象与内部对象之间的桥#

这张图解决的理解障碍是:很多读者会把 ChatCompletionRequestGenerateReqInput 看成两套平行系统,而不是同一条变换链上的前后状态。

flowchart LR
    Proto["protocol.py Pydantic models"] --> Serve["serving_*._convert_to_internal_request()"]
    Serve --> IO["io_struct.py request objects"]
    IO --> Batch["tokenized / batch output structs"]
    Batch --> Runtime["scheduler / detokenizer / tokenizer manager"]

图比纯文字多解释的一点是:protocol.pyio_struct.py 不是重复定义,而是协议对象与运行时对象的两端。

protocol.py 更像什么#

entrypoints/openai/protocol.py 明确写着自己是 “Pydantic models for OpenAI API protocol”。这里的对象重点在于:

  • 对外请求/响应 schema
  • 字段默认值和兼容性
  • 请求参数归一化
  • 某些 surface-specific validator

例如 ChatCompletionRequest 里会同时包含:

  • OpenAI-compatible 字段
  • SGLang 扩展字段
  • response_format
  • chat_template_kwargs
  • routed_dp_rank
  • lora_path

这说明协议层不是纯 OpenAI 镜像,而是兼容层加扩展层。

io_struct.py 更像什么#

io_struct.py 的文件头直接说明:这里定义的是在 TokenizerManagerDetokenizerManagerScheduler 之间传递的对象。也就是说,它的关注点已经不是“客户端想表达什么”,而是“运行时各进程之间需要交换什么”。

从结构上看,这里至少包括:

  • GenerateReqInput
  • TokenizedGenerateReqInput
  • BatchTokenizedGenerateReqInput
  • EmbeddingReqInput
  • BatchTokenIDOutput
  • BatchStrOutput

这已经很清楚地说明,io_struct.py 不是协议层镜像,而是运行时消息对象层。

ChatCompletionRequest -> GenerateReqInput 这条桥的阅读价值#

serving_chat.py::_convert_to_internal_request(...) 正是最值得盯住的桥:

  1. 先处理 reasoning_effort、message preprocessing、tool call constraint。
  2. request.to_sampling_params(...)
  3. 最后构造 GenerateReqInput(...),把 prompt、多模态数据、sampling params、LoRA、routing、priority 等统一塞进内部请求对象。

这说明最有价值的不是背字段名,而是看“协议字段在这一层被解释成了什么内部语义”。

为什么 protocol.py 里会有这么多“看起来像运行时字段”的东西#

比如:

  • top_k
  • min_p
  • regex
  • ebnf
  • custom_logit_processor
  • routed_dp_rank

这些字段在协议层出现,不代表协议层真的理解它们的运行时后果。更准确的说法是:协议层负责允许调用方表达这些意图,真正的运行时含义在 _convert_to_internal_request(...)io_struct.py 里才落地。

这正是这章最值得技术书强调的地方:字段出现的位置,不等于语义真正生效的位置。

io_struct.py 里最值得优先记住的几个对象#

GenerateReqInput#

这是请求进入 runtime 前最重要的统一对象。文本、多模态、sampling、LoRA、routing、session、priority 都在这里收口。

TokenizedGenerateReqInput#

这一步意味着请求已经跨过“解释/规范化”边界,进入更接近 tokenized runtime 的状态。

BatchTokenIDOutput / BatchStrOutput#

这两个对象则站在请求回程链上,分别代表 token 级输出和文本级输出。

把这几个对象抓住,你就能把请求的“外层表示”和“内层表示”连接起来。

为什么这条桥能显著提升读仓库效率#

因为很多读者读大型系统时都会被一种假象拖住:以为不同目录里出现的是不同系统。protocol.pyio_struct.py 正好是典型例子。只要把这条桥看清,很多阅读动作都会变简单:

  • 看到请求字段时,知道去哪里找它的运行时落点。
  • 看到内部对象时,知道它最初来自哪个公开 surface。
  • 排障时,知道问题更可能出在 schema、转换还是运行时对象推进。

这一层最容易出现的误判#

1. 把 protocol.py 当成“完整语义定义”#

它定义的是对外 shape,不是全部运行时语义。

2. 把 io_struct.py 当成“纯内部细节”#

实际上很多公开功能是否成立,都要在这里找到真正落点。

3. 看到字段重复出现就以为设计重复#

很多时候这是对象跨层演化,而不是重复定义。

如果你要顺着一条请求往里追,先怎么查#

建议按这个顺序:

  1. 先在 protocol.py 找公开字段和 validator。
  2. 再在 serving_*._convert_to_internal_request(...) 看这些字段被怎样翻译。
  3. 再到 io_struct.py 看内部对象的真正承载形态。
  4. 最后才进入 tokenizer / scheduler / detokenizer 路径。

小结#

这一章真正想补齐的,是代码导读里经常缺失的数据模型桥:

  • protocol.py 描述请求对外怎样说。
  • io_struct.py 描述运行时内部怎样接住它。
  • _convert_to_internal_request(...) 则是两者之间最值得读的桥。

到这里,代码导读部分就不仅在讲“哪些目录重要”,也开始讲“同一条请求在不同层到底长成什么样”。