MMSP-RFC-0003 消息请求
- Feature Name:
message_requests - Start Date: 2026-10-24
- RFC PR: #301
- Kind: spec
Abstract
消息请求是调用模型的唯一方式:由 streaming_response 或 streaming_response_stateful 产出的一条 UniEvent 异步流,只有一套语法,由基类收尾,先是若干 delta 事件,每个携带一个内容项,然后恰好一个 stop 事件,携带用量和结束原因。本文规定这种调用、流的语法、流怎样结束,以及请求参数怎样映射:每个思考等级在每个客户端上都能用,其他每个 UniConfig 值要么按明确的映射发送,要么在请求发出之前被拒绝。
1. Introduction
同时服务许多对话的宿主程序,绝不能因为某个服务商而阻塞自己的事件循环;调用方也绝不能把被截断的回答当成完整的回答。在第二版流式协议(#223)之前,每个客户端自己为内容项收尾,所以针对某个服务商的流的修复(#218、#221)一次只能落到一个客户端。2026-10-22,CI 中一条 GLM-5.3-Flash 的流在携带用量的数据块到达之前就结束了(run 39841678668);探测表明服务商的最后一个数据块每次都同时携带结束原因和用量,所以那条流是中途断开的,而这正是调用方想要重试的情况(#272)。
调用方只设一次 ThinkingLevel,换模型只换名字;如果客户端因为某个级别抛出异常,调用方就又得按模型分支。#166 按每个 Gemini 模型限制了级别,#176 按厂商映射了 max,#293 补上了 none 什么也不发送、Claude Opus 5 仍在思考的缺口,#294 让 none 在 Kimi K3 系列上关闭思考。靠模糊匹配构建的请求,会把一代模型的参数发给另一代;#289 背后的审查发现配置页在大多数键上写得不对,于是依据两种语言的转换重写了它。
项目负责人的原话:支持全部思考等级;模型请求参数不模糊匹配;对本文则是:要求异步和流式,任何思考等级不能报错,请求参数尽量精确。这些内容最初在 #265 中写作 MMSP-RFC-0003、MMSP-RFC-0006、MMSP-RFC-0010 以及 MMSP-RFC-0004 中的版本规则,在 #298 中写作 MMSP-RFC-0003、MMSP-RFC-0005(stop 事件部分)、MMSP-RFC-0010 与 MMSP-RFC-0011。
2. Conventions and Terminology
本文中的关键词“必须”“不得”“应当”“不应”“可以”依次对应 BCP 14(RFC 2119、RFC 8174)中的 MUST、MUST NOT、SHOULD、SHOULD NOT 与 MAY,按其定义解释;本文只在这一含义上使用这五个词。
- 客户端流:客户端的
_streaming_response_internal/_streamingResponseInternal产出的事件。公开流:基类产出给调用方的事件。 - 片段、完整内容项:定义见 MMSP-RFC-0002 §2。增长字段:某类内容项的片段不断延长的那一个字段(
text、thinking、arguments、data、embedding)。 - 官方客户端、兼容客户端:定义见 MMSP-RFC-0005 §2。
- 代:一个厂商接受同一种请求的那些模型(
"4-6"、"gpt-6")。
3. 调用
3.1 两个入口
调用模型必须只经由 streaming_response 与 streaming_response_stateful(TypeScript 中为 streamingResponse 与 streamingResponseStateful),它们都是产出 UniEvent 的异步生成器。没有同步调用,也没有一次返回整个响应的调用。客户端向端点发出的另一种请求只有 list_models(MMSP-RFC-0005 §5)。
3.2 全部走流式
服务商提供流式端点时,客户端必须读取流式端点。embedding 端点不是流式的:它的向量作为内容项流出,然后是一个 stop,以 prompt_tokens 携带端点报告的输入计数,finish_reason 为 stop。openai-embedding 按向量的 index 把每个向量与它的输入对应起来。
3.3 请求之前
在发送任何内容之前,streaming_response 给每条缺少 created_at 的消息打上时间戳(MMSP-RFC-0004 §6.3),转换旧的内容项类型(MMSP-RFC-0009 §7),并检查每条消息都有可发送的内容。没有内容项的消息,或者客户端的消息转换无法发送的消息(只有思考),必须在发往服务商的请求发出之前抛出带有 message_index 的 InvalidMessageError(MMSP-RFC-0007 §3.1)。
4. 流的语法
4.1 客户端产出什么
- 服务商的流中每来一个事件,客户端流必须产出一个
UniEvent:只产出 delta,从不产出完整内容项。它的stop事件在线上报告用量和结束原因的地方携带它们,有时分几次携带;delta事件两者都不携带。 - 不含任何通用内容的线上事件就是空的 delta 事件。表示内容项结束的事件不为内容项收尾:它只产出自己新增的东西,比如一个已完成的推理内容项的 fidelity。客户端不认识的事件必须跳过,因此网关自己插入的帧无法终止一次生成;设置
MMSP_DEBUG后改为抛出UnknownEventError。 - 从线上读取完整内容项的客户端(MiniMax 的工具调用)把它作为单个片段流出,不得再同时流出它的 delta。
4.2 基类做什么
- 基类把每个 delta 交给
StreamItems(stream_items.py/streamItems.ts),由它判断每个内容项在哪里结束,并在下一个内容项开始或客户端流结束时,以完整内容项为它收尾。 - 基类把客户端各个
stop事件上的用量和结束原因逐字段合并进唯一的公开stop:后到的计数替换先到的,0 仍然是 0,服务商不报告的计数为None,不要求任何计数是数字。
4.3 内容项
- 每个内容项都以连续的片段流出,随后是它唯一的完整内容项。内容项之间不得交错。
- 完整内容项只由
StreamItems用已经流出的片段构建:增长字段是各片段该字段的拼接;工具调用的参数由拼接后的 JSON 解析而来;其余字段取自第一个片段;fidelity是片段携带的那一份。一个内容项最多只有一个片段携带它的 fidelity,完整内容项会再带一次。
4.4 助手消息
按顺序排列的完整内容项,加上 stop 事件的 usage_metadata 与 finish_reason,就是助手消息。concat_uni_events_to_uni_message、有状态的历史与 tracer 都把这两个字段复制到助手消息上。
5. 流怎样结束
- 一条流必须要么以它的
stop事件结束,要么以异常结束,不会两者兼有。 stop事件必须携带一个usage_metadata对象和一个有值的finish_reason。服务商的流缺少其中之一就结束时,在它之前的事件之后、原本stop的位置抛出IncompleteStreamError,因此调用方不会看到缺少这两项的stop,历史和 trace 也不会记录这一回合。这个错误指明客户端与缺少的一个或多个字段,并携带已经到达的用量或结束原因,好让这些 token 仍能计入。- 除了思考没有别的内容项、或者根本没有内容项的响应,在原本
stop的位置抛出EmptyResponseError,因为没有服务商接受它回传(MMSP-RFC-0004 §5)。 - 客户端产出违反这套语法的内容时,在任何模式下都抛出
StreamProtocolError:完整内容项、带用量的 delta 事件、同一内容项上两份不同的 fidelity、第一个片段缺少名字或调用 id 的工具调用。这是客户端的缺陷,不得修补。IncompleteStreamError不属于这种情况(是服务商截断了流),EmptyResponseError也不属于(是没有内容的响应)。 - SDK 在一条已打开的流中抛出的异常原样传出(MMSP-RFC-0007 §4);Interactions API 的
error事件两个 Interactions SDK 都不会抛出,由客户端抛出ProviderStreamError。中止按 MMSP-RFC-0008 的规定结束这条流。
6. 请求参数
6.1 明确匹配
- 客户端为某个模型发送什么,必须由明确的匹配决定,绝不靠猜。在官方客户端内部,各代模型靠 id 中的版本区分(
"4-6";"gpt-6"但不是"gpt-6-luna";"k2.";"m3.1";"glm-5.3"),写在有名字的元组或直白的条件里;先匹配上的那一行生效。表示档位或模态的标记(-pro、-image、tts、embedding)也以同样的方式选定模型的类别。"claude"、"gpt"这类只有系列名的子串不得决定任何事。新的一代模型加一行或一个条件,绝不放宽匹配。 - 兼容客户端只在发送的内容因模型而异时才读取模型 id,而且同样按明确的名字:vLLM 适配器从自己的表中选出聊天模板的思考开关,找不到时退回 Qwen3 的开关;
openai-responses在 Luna 以外的 GPT-6 上把none变成low;openai-chat和openai-responses对 GPT-5.6 原本会拒绝的图片请求detail: "high"。 - 模型 id 由哪个客户端处理,见 MMSP-RFC-0005。
6.2 没有对应设置的值
客户端无法为该模型发送的 UniConfig 值,必须在请求发出之前抛出 UnsupportedParameterError,指明客户端与参数。客户端用不上的键会被忽略。thinking_level 是例外(§6.3)。
6.3 思考等级
- 客户端不得拒绝任何
ThinkingLevel:无论什么模型,都没有客户端为thinking_level抛出UnsupportedParameterError。 - 每个官方客户端按一张固定的表,把每个级别映射到该模型这一代的某个设置上,不发出警告:模型有这个级别时,就用它本身;模型没有时,用模型接受的另一个级别(Claude 4.6 以
high运行xhigh;Gemini 以high运行xhigh和max);对于none,模型有自己的关闭开关时就用它(Claude Sonnet 5 上的disabled,GPT-6 Luna 和 Kimi K3 系列上的none);对于没有关闭开关的模型上的none,用它允许的最少思考(Claude Opus 5.5 上的最低 effort,Claude Sonnet 5.5 上的between_tools,Gemini 上的minimal或low)。 - 经由兼容客户端时,由端点决定某个级别实际如何运行,端点可以拒绝这个值:
ant-messages对none发送disabled;openai-responses把级别作为它的 effort 发送,在 Luna 以外的 GPT-6 上把none发成low;openai-chat不发送级别;它的 vLLM 适配器只发送聊天模板的思考开关;mmsp客户端把级别转发给它的服务端。
6.4 有另一种形式的值
协议对某个值有另一种形式时,映射写明用哪一种:工具名列表在 openai-chat 上是 allowed_tools;只有一个名字的列表在 ant-messages 上是 {type: "tool", name};没有设置的 max_tokens 在 Anthropic 客户端上是 64000。
6.5 配置页
本文陈述规范;site/src/docs/configuration.zh.md 写明每个客户端对每个键发送、忽略还是拒绝什么,思考等级的每一行在两个包的映射测试中都有对应的用例。其中的一行必须和改动它的代码在同一个 PR 中修改。
7. mmsp 客户端
mmsp 客户端有自己的 streaming_response:它打上 created_at,把消息和配置原样发给它的服务端,不转换旧的内容项类型,也不检查每条消息是否有可发送的内容;§3.3 和 §6 的其余部分由服务端的客户端完成。它原样转发服务端的公开流,包括其中的 stop。服务端的流在 stop 之前结束时,它自己抛出 IncompleteStreamError;服务端的 IncompleteStreamError 以 UpstreamError 的形式到达它,带有该 error_type 以及这个错误的各个字段(MMSP-RFC-0007 §4)。
8. Conformance
src_py/mmsp/base_client.py、src_py/mmsp/stream_items.py、src_ts/src/baseClient.ts、src_ts/src/streamItems.ts;errors.py/errors.ts中的IncompleteStreamError,以及它在wire.py/wire.ts中的分支。src_py/tests/stream_grammar.py/src_ts/tests/streamGrammar.ts,共享的 e2e 测试用它检查每一条流,包括每个完整内容项等于其片段的拼接。test_stream_protocol.py、test_stream_items.py、test_unknown_events.py、test_empty_response.py、test_message_order.py(把一条空消息和一条只有思考的消息交给各客户端的消息转换)及其对应的 Jest 测试;test_mmsp_server.py/mmsp-server.test.ts,检查经过服务端的流与上游的流相同。src_py/mmsp/<client>/client.py和src_ts/src/<client>/client.ts中的配置转换(transform_uni_config_to_model_config/transformUniConfigToModelConfig);test_thinking_level_mapping.py/thinking-level-mapping.test.ts,为每一代模型的那一行固定用例;test_request_body.py/request-body.test.ts。site/src/docs/configuration.en.md/configuration.zh.md;UnsupportedParameterError的文档字符串;对照.agents/skills/mmsp-dev/SKILL.md评审,它要求在编写不显然的映射之前先停下来询问。