Community生产力与协作github.com

Prism-Shadow/model-message-stream-protocol

用于开发和维护MMSP自身的固定工作流——添加或更新模型支持、修改页面。当需要在本仓库中支持新模型或协议版本、同步llmsdk_docs、实现供应商客户端、或修改playground、tracer或站点时使用。涵盖文档同步、实时API捕获、Python/TypeScript配对实现、模型范围的端到端测试,以及三个页面的UI规则。

model-message-stream-protocol 是什么?

该技能为MMSP(Model Message Stream Protocol)项目提供了一套严格的开发工作流,适用于开发者需要添加新模型支持、更新现有模型、同步官方文档、实现供应商客户端、或修改playground/tracer/站点等场景。工作流分为四个阶段:首先从模型供应商的官方文档站点同步文档到llmsdk_docs/目录;然后使用供应商API密钥实时捕获API交互(包括请求和流事件)到api_captures/,并探测重放所需的最小字段集;接着根据捕获结果和文档实现Python和TypeScript客户端,遵循引用客户端模式并保持最小重放原则;最后进行端到端测试和UI规则检查。该技能支持Claude Code、Cursor等AI编码代理,通过.agents/skills/mmsp-dev/目录下的规则文件定义操作约束,确保所有修改都从头开始,避免产生“AI垃圾代码”。

兼容平台✓Claude Code✓Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/Prism-Shadow/model-message-stream-protocol/tree/HEAD/.agents/skills/mmsp-dev

Installed? Explore more 生产力与协作 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

MMSP 开发工作流

添加或更新模型支持需按以下阶段顺序进行。当某个阶段提示暂停并询问时,请暂停并向用户提问;在问题解决前不要继续,切勿自行填补空白。

目录映射

llmsdk_docs/<model_version>/      官方文档快照,每个模型代系一个文件夹 (README.md + docs/)
api_captures/<protocol>/           Git 忽略的原始 API 捕获数据:请求载荷 + 流事件
src_py/mmsp/<client_type>/        Python 客户端,每个客户端类型一个文件夹:<vendor>_official/ 或 <protocol>/
src_py/mmsp/auto_client.py        根据 client_type 名称创建客户端;模型 ID 的家族名称为其官方客户端
src_ts/src/<client_type>/         TypeScript 客户端,与 Python 文件夹镜像
src_ts/src/autoClient.ts          TypeScript 路由,镜像 auto_client.py
src_py/tests/test_client.py       参数化的端到端测试 (环境变量门控的 AVAILABLE_MODELS)
src_ts/tests/client.test.ts       相同功能的 TypeScript 测试
changelog/unreleased/              存放未发布的变更;发布时重命名为版本号
changelog/<version>/              发布摘要 (README.md) 加每个条目的详情文件
changelog/README.md                条目格式:元数据块、正文规则、双语配对
CHANGELOG.md                       每个版本一行简要说明,链接至 changelog/
src_py/mmsp/integration/          playground、tracer 页面和 MMSP 服务器,镜像于 src_ts/src/integration/
site/                             位于 mmsp.penguin.ooo 的 Astro 站点
.agents/skills/mmsp-dev/kill-ai-slop/   针对下方 UI 工作引入的 AI 垃圾代码指南和扫描工具
<name>.zh.md                       changelog/ 中每个文件对应的中文版本

阶段 1 — 将官方文档同步到 llmsdk_docs/

  • 来源必须是模型供应商的官方文档站点(例如 docs.anthropic.com, platform.openai.com, ai.google.dev)。禁止使用第三方镜像、博客文章或模型记忆。
  • 将快照保存到 llmsdk_docs/<model_version>/ 下,遵循现有文件夹约定,并在 llmsdk_docs/README.md 中列出该文件夹。运行此工作流即是对仓库规则(禁止编辑 llmsdk_docs/)的明确请求。
  • 当获取的文档与现有快照不同时,以新官方文档为准:原地更新旧文件。
  • 快照必须足够完整以支持实现:请求/响应模式、流事件序列、思考输出、工具调用、用量字段和错误响应。
  • 暂停并询问如果官方 URL 不确定或某个页面无法获取。用户可手动粘贴内容。

阶段 2 — 将实时 API 交换捕获到 api_captures/

  • 前提条件:提供商的 API 密钥环境变量必须已设置且可用。使用与 src_py/tests/test_client.py 相同的环境变量和基础 URL(AVAILABLE_MODELS 门控和 _create_client)。暂停并询问用户如果密钥缺失;工作流不得在没有密钥的情况下继续。
  • 使用提供商的官方 SDK,或完全按文档记录的原始 HTTP,运行一次启用思考功能的流式工具调用请求,然后发送工具结果,使捕获也显示助手轮次如何重新发送。
  • 将完整交换(未修改)保存到 api_captures/<protocol>/ 下(Git 忽略),例如 round1.request.json 加 round1.stream.jsonl,其中每个原始流事件按顺序排列。切勿保存凭据。
  • 当交换中的某些内容无法序列化为 JSON 时(二进制载荷、包含非 JSON 值的 SDK 对象),保存其 str() 形式并据此分析。保持容器可解析:将文本包装在 JSON 对象中,使 .jsonl 保持每行一个 JSON 值({"unserializable_str": "<str(event)>"},保持事件在流中的顺序),或者当没有任何内容可序列化时,将整个交换放入同级 .txt 文件中。str() 捕获仍然是 API 返回内容的权威记录。切勿丢弃事件、手动编辑以使其成为有效 JSON,或由于序列化失败而回退到文档。
  • 暂停并询问任何 API 错误(无效密钥、配额不足、速率限制)。不要模拟响应或仅凭文档继续。
  • 捕获是主要的实现参考,优先级高于文档:当两者不一致时,以 API 实际返回的内容为准。
  • 然后探测重放,因为捕获仅显示 API 发送的内容,而非其需要接收的内容。重复发送助手轮次,每次移除一个候选字段——推理项目的 id、其内容、整个推理项目、每个你曾想保留的字段——并记录哪些移除后 API 仍接受且模型行为保持不变。该结果(而非响应格式)决定了阶段 3 存储的内容。将探测结果保存在捕获旁边。

阶段 3 — 实现 Python 和 TypeScript 客户端

  • 每个客户端类型一个文件夹,以其命名:官方客户端(每个供应商一个 openai_official/, anthropic_official/, google_official/, zai_official/, moonshot_official/, deepseek_official/, minimax_official/)使用供应商自己的 API,了解该供应商所有代系的模型,并从环境变量中读取该供应商的密钥;兼容客户端(每个 Wire 协议一个 openai_responses/, openai_chat/, openai_chat_vllm_adapter/, openai_embedding/, ant_messages/, google_genai/, mmsp/)针对任何端点使用该协议。类名与文件夹相同(OpenAIOfficialClient, OpenaiChatClient),客户端类型为文件夹名称(带短横线 openai-official, openai-chat)。
    • 供应商模型的新代系应放入该供应商的官方客户端。比较新协议(捕获 + 文档)与客户端当前发送的内容;如果代系之间存在差异,即使只是单个键名,也应在客户端内通过显式版本区分("4-6" in self._model, "gpt-6" in self._model),切勿使用裸子字符串(如 "claude" in model),并保持旧代系正常工作。除非用户明确指示,否则绝不能丢弃代系。
    • 新供应商应有新的官方客户端文件夹、auto_client.py/autoClient.ts 中新的 <vendor>-official 客户端类型,以及模型族表中对应的新行:族是供应商所有模型 ID 的开头前缀(glm-, kimi-),不区分大小写匹配。
    • 多个供应商使用的新 Wire 协议应获得一个兼容客户端文件夹和客户端类型;兼容客户端不携带任何供应商的特殊行为。
  • auto_client.py/autoClient.ts 根据客户端类型表创建 client_type 命名的客户端。若未指定 client_type,则模型 ID 开头的族名称将命名其官方客户端;未知族的 ID 将引发错误并要求提供。路由没有其他规则:禁止对模型 ID 进行子字符串匹配,禁止在官方客户端外进行版本匹配。
  • 凭据仅用于其被给予的端点。 这是安全模型,每个客户端构造函数都遵循此规则。作为 api_key 传递的密钥原样使用。从环境变量读取通用密钥的客户端(openai_official 和 OpenAI 协议兼容客户端使用 OPENAI_API_KEY、anthropic_official 和 ant_messages 使用 ANTHROPIC_API_KEY、google_genai 使用 GEMINI_API_KEY)仅当端点也来自环境变量时才读取(OPENAI_BASE_URL/ANTHROPIC_BASE_URL/GEMINI_BASE_URL,或提供商的默认值):如果提供了 base_url 但没有 api_key,则在构造时引发错误,而不是将该密钥发送到另一个端点。这是 utils 中的 resolve_credentials/resolveCredentials;调用它,不要重新实现。任何其他官方客户端(deepseek_official, zai_official, moonshot_official, minimax_official, google_official)读取自己的变量(DEEPSEEK_API_KEY, ZAI_API_KEY, MOONSHOT_API_KEY, MINIMAX_API_KEY, GEMINI_API_KEY)而不管给定的基础 URL,且从不读取通用密钥;如果两者都缺失,则引发 <VAR> is required for <Client>.。将每个凭据显式传递给供应商 SDK,包括 "none"(TypeScript 中的 authToken: null,Python 中的已清除 auth_token),因为 SDK 会填充所有未从自身环境变量接收的内容。tests/env-credentials.test.ts/tests/test_env_credentials.py 中的离线测试在受控环境下构建每个客户端,并读取 SDK 实例持有的凭据;为新客户端添加测试。
  • 重放必须最小化,而非穷举。重放的助手轮次必须能被 API 接受且模型行为保持不变;它不需要逐字段重现 Wire 项。fidelity 是一个异常通道,而非载荷的镜像:只有当阶段 2 的探测显示没有该字段请求会失败或模型会降级时,字段才有资格进入 fidelity。提供者生成且 API 会重新生成或忽略的 ID(推理项目 ID、输出项 ID)应被排除。API 要求的字段必须包含:GPT-5.5 的 phase、Claude 的思考 signature、严格上游要求的精确推理字段名称。
  • 切勿将通用字段已包含的内容复制到 fidelity 中——思考文本、工具调用的名称或已解析参数。应改为从通用字段重建 Wire 项。
  • tool_call_id 不可省略:始终捕获提供者的调用 ID 并重放它。当 Wire 格式同时包含项目 ID 和调用 ID 时,调用 ID 是将结果与调用关联的 ID。
  • 遵循参考客户端;不要重新设计。 openai_official/ 是 Responses 风格协议的模式,openai_chat/ 是 Chat Completions 的模式:相同的方法顺序、相同的控制流、相同的命名。新客户端应看起来像是针对其参考的差异,因为这就是审查方式。
  • 保持从上到下的可读性。 读者应能顺畅地跟踪单个请求或响应穿过文件,而无需在定义之间跳转,因此内联是默认方式:直接读取 SDK 的属性(model_output.delta, model_output.item.call_id),并将字段访问、用量计算和错误文本保留在使用它们的位置。仅当确实存在一个大型、自包含的块时才提取辅助函数——那种内联会掩埋主流程的块,例如获取和解码图像——绝不为了几行代码。镜像参考客户端自身的私有方法(_convert_thinking_level_*, _convert_tool_choice),而不是在旁边发明一层;同时接受字典和 SDK 对象的适配器绝不是其中之一,因为事件使用 SDK 自身的类型进行类型化。
  • 客户端仅产生增量;基类负责关闭项目。 transform_model_output_to_uni_event/transformModelOutputToUniEvent 将一个 Wire 事件转换为一个 UniEvent,其 content_items 是该事件携带的 .delta 项,按 Wire 顺序排列,且从不包含 .done 项。_streaming_response_internal/_streamingResponseInternal 构建请求、打开流,并逐个 Wire 事件产生一个事件:一个简单的循环,没有项目记账、累加器、参数解析、用量合并或合成停止。usage_metadata/finish_reason 随 Wire 报告它们的 stop 事件一起传递——Anthropic 的 message_start 和 message_delta、Chat Completions 的 finish 和 usage 块、response.completed——由基类逐字段合并;客户端的 stop 事件永不结束公共流。没有通用内容的 Wire 事件是空增量事件:没有项目,null 用量和 finish reason。基类将每个增量传递给 StreamItems(stream_items.py/streamItems.ts),后者在接收到增量时立即发送出去,并在下一个项目开始或客户端的流结束时关闭每个项目的 .done 项;然后基类发出一个 stop 事件或引发 EmptyResponseError。破坏协议的事件在每种模式下都引发 StreamProtocolError,共享的端到端测试使用 assert_stream_grammar/assertStreamGrammar 检查每个客户端的流。
  • 一个项目的增量是连续的,共享同一个 fidelity.item_id。 模型输出是串行的,因此一次流式传输一个项目,提供者的项目结束事件(content_block_stop, response.output_item.done, step.stop)被列为无操作:延迟或乱序关闭项目的网关不会改变任何内容。将提供者自身的 ID(字符串化)作为 fidelity.item_id 传递:Anthropic 的块 index、Responses 事件的 item_id 或 item.id or item.call_id(当网关未发送时为 None/undefined)、Interactions 步骤的 index、Chat Completions 的 Wire 字段(reasoning_content, reasoning, content, tool_calls)、generateContent 的部分类型(function_call, thought, inline_thinking, inline_data, text);嵌入向量不携带任何 ID。StreamItems 会剥离该 ID,因此它永远不会到达消费者、追踪或历史记录。
  • StreamItems 决定项目何时结束;客户端从不这样做。 当增量携带另一个 item_id、属于另一种类型(Interactions 的 thought 步骤变为文本、图像、文本,在一个索引下是三个项目)、或自身开始一个项目时(携带名称的 tool_call.delta——每个提供者只在调用的第一个增量上发送一次,因此 Chat Completions 调用不需要自己的 ID;图像——inline_thinking,或带有 image/* MIME 类型的 inline_data,而音频以单个项目的块流式传输;嵌入向量),该增量属于下一个项目。否则它继续当前正在流式传输的项目:没有 ID 的增量属于当前项目,并且无论网关在调用参数上放置什么 ID,它们都属于当前项目。在项目 ID 下单独发送的 Fidelity 属于该项目,无论携带它的类型如何(Interactions 在图像思考后的 thought_signature)。该文件中的按类型表——增长的字段、第一个增量的头字段、块如何连接——是流中唯一的按类型代码:done 项目是该项目的第一个增量,其中增长字段被替换为所有增量的连接……

相关技能