MMSP 開発ワークフロー
モデルサポートの追加または更新は、以下のステージを順に実行します。各ステージで停止して尋ねると記載されている場合は、一時停止してユーザーに確認し、問題が解決するまで続行せず、自分でギャップを埋めてはいけません。
ディレクトリマップ
llmsdk_docs/<model_version>/ 公式ドキュメントのスナップショット、モデル世代ごとに1フォルダ(README.md + docs/)
api_captures/<protocol>/ Git管理外の生APIキャプチャ:リクエストペイロード + ストリームイベント
src_py/mmsp/<client_type>/ Pythonクライアント、クライアントタイプごとに1フォルダ:<vendor>_official/ または <protocol>/
src_py/mmsp/auto_client.py クライアントタイプ名からクライアントを作成します。モデルIDのファミリーがその公式クライアントを指定します
src_ts/src/<client_type>/ TypeScriptクライアント。Pythonフォルダをミラーリングします
src_ts/src/autoClient.ts TypeScriptルーティング。auto_client.py をミラーリングします
src_py/tests/test_client.py パラメータ化されたE2Eテスト(環境変数でゲートされる AVAILABLE_MODELS)
src_ts/tests/client.test.ts TypeScript向けの同上
changelog/unreleased/ 未リリースの変更が保存される場所。リリース時にバージョン名にリネーム
changelog/<version>/ リリースサマリー(README.md)とエントリごとの詳細ファイル1つ
changelog/README.md エントリ形式:メタデータブロック、本文ルール、バイリンガルペアリング
CHANGELOG.md リリースごとに1行の簡潔なリンクで changelog/ にリンク
src_py/mmsp/integration/ プレイグラウンド、トレーサーページ、MMSPサーバー。src_ts/src/integration/ にミラーリング
site/ mmsp.penguin.ooo のAstroサイト
.agents/skills/mmsp-dev/kill-ai-slop/ 以下のUI作業用のベンダー版AIスロップガイドとスキャナー
<name>.zh.md Chinese counterpart, required for every file in 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(AVAILABLE_MODELSゲーティングと_create_client)と同じ環境変数とベースURLを使用します。キーが不足している場合は停止して尋ねるようにユーザーに依頼します。キーなしではワークフローを続行してはいけません。 - プロバイダの公式SDK、またはドキュメントに正確に従った生のHTTPを使用して、思考機能を有効にした1つのストリーミングツールコールリクエストを実行し、ツール結果を送り返して、アシスタントターンがどのように再送信されるかをキャプチャに示します。
- 完全な交換を修正せずに
api_captures/<protocol>/以下に保存します(Git管理外)。例:round1.request.jsonとround1.stream.jsonlにすべての生ストリームイベントを順に含めます。決して認証情報を保存しないでください。 - 交換の一部がJSONにシリアル化できない場合(バイナリペイロード、非JSON値を保持するSDKオブジェクト)、その
str()形式を保存し、そこから分析します。コンテナはパース可能に保ちます:テキストをJSONオブジェクトでラップして.jsonlが1行に1つのJSON値を保つようにします({"unserializable_str": "<str(event)>"}、イベントをストリーム順に保持)、または交換全体を何もシリアル化できない場合は兄弟の.txtに置きます。str()キャプチャはAPIが返したものの信頼できる記録です。イベントを削除したり、有効なJSONに手動編集したり、シリアル化に失敗したからといってドキュメントにフォールバックしたりしないでください。 - 停止して尋ねる:APIエラー(無効なキー、クォータ不足、レート制限)が発生した場合。レスポンスをモックしたり、ドキュメントだけで続行したりしないでください。
- キャプチャは主要な実装リファレンスであり、ドキュメントよりも優先されます:両者が矛盾する場合は、APIが実際に返したものを実装します。
- 次にリプレイを調査します。キャプチャはAPIが送信するものだけを示し、受け取る必要があるものは示さないからです。アシスタントターンを繰り返し再送信し、毎回1つの候補フィールド(推論項目のid、そのコンテンツ、推論項目全体、保持したくなる各フィールド)を削除し、どの削除がモデルの動作を維持したままAPIにまだ受け入れられるかを記録します。その結果が、ステージ3が保存するものを決定します。調査結果をキャプチャの隣に保存します。
ステージ 3 — PythonおよびTypeScriptクライアントを実装する
- クライアントタイプごとに1フォルダ、そのタイプにちなんで命名します:ベンダーごとの公式クライアント(
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)。- ベンダーのモデルの新しい世代は、ベンダーの公式クライアントに入ります。新しいプロトコル(キャプチャ + ドキュメント)をクライアントが現在送信しているものと比較します。世代が異なる場合(1つのキー名でも)、クライアント内で明示的なバージョン(
"4-6" in self._model、"gpt-6" in self._model)で世代を区別し、決して"claude" in modelのような生のサブストリングで区別せず、古い世代も動作し続けるようにします。ユーザーが明示的に指示しない限り、世代を削除しないでください。 - 新しいベンダーには、新しい公式クライアントフォルダ、
auto_client.py/autoClient.tsの新しい<vendor>-officialクライアントタイプ、およびモデルファミリーテーブルの新しい行が必要です。ファミリーは、ベンダーのモデルのすべてのIDが始まるプレフィックス(glm-、kimi-)で、大文字小文字を区別せずに一致します。 - 複数のベンダーが提供する新しいワイヤープロトコルには、互換クライアントフォルダとクライアントタイプが必要です。互換クライアントはベンダーの癖を一切持ちません。
- ベンダーのモデルの新しい世代は、ベンダーの公式クライアントに入ります。新しいプロトコル(キャプチャ + ドキュメント)をクライアントが現在送信しているものと比較します。世代が異なる場合(1つのキー名でも)、クライアント内で明示的なバージョン(
auto_client.py/autoClient.tsは、クライアントタイプの1つのテーブルから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)は、与えられたベースURLに関係なく自身の変数(DEEPSEEK_API_KEY、ZAI_API_KEY、MOONSHOT_API_KEY、MINIMAX_API_KEY、GEMINI_API_KEY)を読み取り、汎用のものは決して読み取りません。どちらもない場合は<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に受け入れられなければなりません。ワイヤーアイテムをフィールドごとに再現する必要はありません。
fidelityは例外チャネルであり、ペイロードのミラーではありません:フィールドがそこに場所を得るのは、ステージ2の調査で、それがなければリクエストが失敗するか、モデルが劣化することを示した場合のみです。APIが再生成または無視するプロバイダ生成ID(推論項目id、出力項目id)は含めないでください。APIが要求するフィールドは含めます:GPT-5.5のphase、Claudeの思考signature、厳格な上流が要求する正確な推論フィールド名。 - ユニバーサルフィールドがすでに持っているもの(思考テキスト、ツールコールの名前やパース済み引数)を決して
fidelityにコピーしないでください。代わりにユニバーサルフィールドからワイヤーアイテムを再構築します。 tool_call_idはオプションではありません:常にプロバイダのコールIDをキャプチャしてリプレイします。ワイヤーフォーマットが項目IDとコールIDの両方を運ぶ場合、コールIDは結果をそのコールに関連付けるものです。- リファレンスクライアントに従い、再設計しないでください。
openai_official/はResponsesスタイルのプロトコルの形状であり、openai_chat/はChat Completionsの形状です:同じメソッド順序、同じ制御フロー、同じ名前。新しいクライアントはリファレンスに対する差分として読めるべきであり、それがレビューの方法だからです。 - 上から下まで読みやすく保ちます。 読者は定義間を行き来することなく、1つのリクエストまたはレスポンスをファイルを通してまっすぐ追えるべきです。したがって、インライン化がデフォルトです:SDKの属性を直接読み取り(
model_output.delta、model_output.item.call_id)、フィールドアクセス、使用量演算、エラーテキストを使用場所に保持します。ヘルパーを抽出するのは、本当に大きく自己完結したブロック(インライン化するとメインフローを埋もれさせる種類、例えば画像の取得とデコード)に対してのみであり、数行のために抽出してはいけません。リファレンスクライアント自身のプライベートメソッド(_convert_thinking_level_*、_convert_tool_choice)をミラーリングし、それらの隣に別のレイヤーを発明しないでください。dictとSDKオブジェクトの両方を受け入れるシムは決してその1つではありません。イベントはSDK自身の型で型付けされているからです。 - クライアントはデルタのみを生成します。ベースクラスがアイテムをクローズします。
transform_model_output_to_uni_event/transformModelOutputToUniEventは1つのワイヤーイベントを1つのUniEventに変換し、そのcontent_itemsはそのイベントが運ぶ.deltaアイテムをワイヤー順に含み、決して.doneアイテムを含みません。_streaming_response_internal/_streamingResponseInternalはリクエストを構築し、ストリームを開き、ワイヤーイベントごとに1つのイベントを生成します:プレーンなループで、アイテムの簿記、アキュムレータ、引数パース、使用量マージ、合成された停止はありません。usage_metadata/finish_reasonは、ワイヤーがそれらを報告する場所(Anthropicのmessage_startとmessage_delta、Chat Completionsの終了と使用量チャンク、response.completed)でstopイベントに乗せられ、ベースクラスによってフィールドごとにマージされます。クライアントのstopイベントは決してパブリックストリームを終了しません。ユニバーサルなものがないワイヤーイベントは空のデルタイベントです:アイテムなし、nullの使用量と終了理由。ベースクラスはすべてのデルタをStreamItems(stream_items.py/streamItems.ts)に渡し、それらは到着時に送信し、次のアイテムが開始するかクライアントのストリームが終了したときに各アイテムをその.doneアイテムでクローズします。その後ベースクラスは1つの停止イベントを発行するか、EmptyResponseErrorを発生させます。プロトコルを破るイベントはすべてのモードでStreamProtocolErrorを発生させ、共有のE2Eテストはassert_stream_grammar/assertStreamGrammarですべてのクライアントのストリームをチェックします。 - 1つのアイテムのデルタは連続しており、1つの
fidelity.item_idを共有します。 モデル出力はシリアルなので、一度に1つのアイテムがストリーミングされ、プロバイダのアイテム終了イベント(content_block_stop、response.output_item.done、step.stop)はno-opとしてリストされます:アイテムを遅くまたは順序不同でクローズするゲートウェイは何も変えません。プロバイダ自身の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);埋め込みベクトルは何も持ちません。StreamItemsはそれを取り除くため、コンシューマー、トレース、履歴に到達することはありません。 StreamItemsがアイテムの終了を決定します。クライアントは決して決定しません。 デルタは、別のitem_idを持つ場合、別の種類である場合(Interactionsの思考ステップがテキスト、画像、テキストと進むのは1つのインデックスの下で3つのアイテム)、またはアイテムをそれ自体で開始する場合(名前を運ぶtool_call.delta(すべてのプロバイダがコールの最初のデルタで1回送信するため、Chat Completionsのコールは独自のIDを必要としません)、画像(inline_thinking、またはimage/*MIMEタイプのinline_data。一方、オーディオは1つのアイテムのチャンクでストリーミングされます)、埋め込みベクトル)に次のアイテムに属します。それ以外の場合は、現在ストリーミング中のアイテムを継続します:IDのないデルタもそうであり、ゲートウェイがそれらに付けるIDにかかわらずコールの引数もそうです。アイテムのIDの下で単独で送信されたフィデリティは、その種類が何であれそのアイテムのものです(画像思考の後のInteractionsのthought_signature)。そのファイルの種類ごとのテーブル(成長するフィールド、最初のデルタのヘッダーフィールド、チャンクの結合方法)がストリーム内の種類ごとの唯一のコードです:完了アイテムは、アイテムの最初のデルタで、成長するフィールドをすべてのチャンクの結合に置き換えたものです。