MMSP 開發工作流程
新增或更新模型支援需按以下階段進行。當某階段標示為停止並詢問時,請暫停並詢問使用者;在問題解決前不要繼續,且切勿自行填補空白。
目錄結構
llmsdk_docs/<model_version>/ 官方文件快照,每個模型世代一個資料夾 (README.md + docs/)
api_captures/<protocol>/ 被 git 忽略的原始 API 捕獲:請求 payloads + 串流事件
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 條目格式:metadata 區塊、主體內文規則、雙語配對
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/ 中的檔案都需要對應的中文版本
第一階段 — 同步官方文件至 llmsdk_docs/
- 來源必須是模型供應商的官方文件網站(例如 docs.anthropic.com、platform.openai.com、ai.google.dev)。切勿使用第三方鏡像、部落格文章或模型記憶。
- 將快照儲存於
llmsdk_docs/<model_version>/下,遵循現有資料夾慣例,並在llmsdk_docs/README.md中列出該資料夾。執行此工作流程即為儲存庫規則要求編輯llmsdk_docs/的明確請求。 - 當取得的文件與現有快照不同時,以新的官方文件為準:就地更新舊檔案。
- 快照必須足夠完整以進行實作:請求/回應架構、串流事件順序、思考輸出、工具呼叫、用量欄位及錯誤回應。
- 停止並詢問如果官方 URL 不確定或無法取得頁面。使用者可手動貼上內容。
第二階段 — 將即時 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(二進位 payloads、持有非 JSON 值的 SDK 物件)時,儲存其
str()形式並從中進行分析。保持容器可解析:將文字包裝在 JSON 物件中,以便.jsonl保持每行一個 JSON 值({"unserializable_str": "<str(event)>"},保持事件在串流順序中),或當沒有任何東西可序列化時,將整個交換內容放入一個同級的.txt檔案中。str()捕獲仍然是 API 回傳內容的權威記錄。切勿丟棄事件、手動編輯使其成為有效 JSON,或因序列化失敗而回退使用文件。 - 停止並詢問任何 API 錯誤(無效金鑰、配額不足、速率限制)。不要模擬回應或僅從文件繼續。
- 捕獲是主要的實作參考,優先於文件:當兩者不一致時,實作 API 實際回傳的內容。
- 然後探測重播,因為捕獲只顯示 API 發送 的內容,而非其需要接收的內容。重複重新發送助理回合,每次移除一個候選欄位 — 推理項目的 id、其內容、整個推理項目、每個你想保留的欄位 — 並記錄 API 在模型行為不變的情況下仍接受哪些移除。該結果(而非回應的形狀)決定第三階段儲存的內容。將探測結果儲存在捕獲旁。
第三階段 — 實作 Python 和 TypeScript 客戶端
- 每個客戶端類型一個資料夾,以其名稱命名:官方客戶端(每個供應商一個,例如
openai_official/、anthropic_official/、google_official/、zai_official/、moonshot_official/、deepseek_official/、minimax_official/)使用供應商自己的 API,了解供應商每個世代的模型,並從環境變數讀取供應商的金鑰;相容客戶端(每個線路協定一個,例如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-),不區分大小寫匹配。 - 多個供應商使用的新線路協定應建立相容客戶端資料夾和客戶端類型;相容客戶端不攜帶任何供應商的特殊行為。
- 供應商模型的新世代應放入該供應商的官方客戶端。比較新協定(捕獲 + 文件)與客戶端目前發送的內容;當一個世代不同時,即使只是一個金鑰名稱不同,也應在客戶端內部使用明確的版本來區分世代(
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,包括「無」(TypeScript 中的authToken: null,Python 中清除的auth_token),因為 SDK 會從自己的環境變數中填充任何未傳遞的內容。tests/env-credentials.test.ts/tests/test_env_credentials.py中的離線測試在受控環境下建立每個客戶端,並讀取 SDK 實例持有的憑證;將新客戶端加入這些測試。 - 重播必須是最小化而非全面。重播的助理回合必須能被 API 接受且不改變模型行為;它不需要逐欄位複製線路專案的欄位。
fidelity是一個例外通道,而非 payload 的鏡像:僅在第二階段探測顯示請求失敗或模型降級時,欄位才有資格放入其中。提供者產生的 ID(API 重新產生或忽略的 — 推理項目 ID、輸出項目 ID)應排除在外。API 要求的欄位則需放入:GPT-5.5 的phase、Claude 的思考signature、嚴格上游要求的確切推理欄位名稱。 - 切勿將通用欄位已承載的內容複製到
fidelity中 — 思考文字、工具呼叫的名稱或已解析的參數。應從通用欄位重建線路專案。 tool_call_id不是可選的:始終捕獲提供者的呼叫 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 自己的類型進行型別化的。 - 客戶端僅產生 delta;基底類別關閉項目。
transform_model_output_to_uni_event/transformModelOutputToUniEvent將一個線路事件轉換為一個UniEvent,其content_items是該事件攜帶的.delta項目,按線路順序,且永遠不是.done項目。_streaming_response_internal/_streamingResponseInternal建構請求、開啟串流,並為每個線路事件產生一個事件:一個簡單的迴圈,沒有專案簿記、沒有累加器、沒有參數解析、沒有用量合併、沒有合成的停止。usage_metadata/finish_reason附加在stop事件上,只要線路報告它們 — Anthropic 的message_start和message_delta、Chat Completions 的 finish 和 usage chunks、response.completed— 由基底類別逐欄位合併;客戶端的stop事件永遠不會結束公開串流。一個沒有通用內容的線路事件是空的 delta 事件:沒有項目,null 的用量和 finish reason。基底類別將每個 delta 傳遞給StreamItems(stream_items.py/streamItems.ts),後者在 delta 到達時將其發送出去,並在下一個項目開始或客戶端串流結束時用其.done項目關閉每個項目;基底類別隨後發出一個 stop 事件或引發EmptyResponseError。破壞協定的事件在所有模式下都會引發StreamProtocolError,共享的端到端測試會使用assert_stream_grammar/assertStreamGrammar檢查每個客戶端的串流。 - 一個項目的 delta 是連續的,並共享一個
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 的線路欄位(reasoning_content、reasoning、content、tool_calls)、generateContent 部分種類(function_call、thought、inline_thinking、inline_data、text);嵌入向量不攜帶任何 ID。StreamItems會將其剝離,因此它永遠不會到達消費者、追蹤或歷史記錄。 StreamItems決定項目何時結束;客戶端永遠不這麼做。 當一個 delta 攜帶另一個item_id、是另一種種類(Interactions 的思考步驟從文字、圖像、文字到文字是同一索引下的三個項目),或自己開始一個項目時:一個攜帶名稱的tool_call.delta(每個提供者只在呼叫的第一個 delta 發送一次,因此 Chat Completions 呼叫不需要自己的 ID)、一個圖像(inline_thinking,或帶有image/*MIME 類型的inline_data,而音訊以一個項目的 chunks 串流)、一個嵌入向量。否則它會繼續當前正在串流的項目:沒有 ID 的 delta 會這樣做,無論閘道在其上放置什麼 ID,呼叫的參數也是如此。僅在項目的 ID 下單獨發送的 Fidelity 屬於該項目,無論承載它的種類為何(圖像思考後的 Interactionsthought_signature)。該檔案中的按種類表格 — 增長的欄位、第一個 delta 的標頭欄位、chunks 如何合併 — 是串流中唯一的按種類程式碼:done 項目是該項目的第一個 delta,其中增長的欄位被替換為所有 delta 中該欄位的合併。StreamItems會建立並擁有每個項目的生命週期。
…
[為翻譯而截斷]