Prism-Shadow/model-message-stream-protocol

Workflow fixe pour développer le MMSP lui-même — ajout ou mise à jour de la prise en charge des modèles et modification de ses pages. Utilisez-le lorsqu'on vous demande de prendre en charge un nouveau modèle ou une version de protocole dans ce dépôt, de synchroniser llmsdk_docs, d'implémenter un client fournisseur, ou de modifier le playground, le traceur ou le site. Couvre la synchronisation de la documentation, la capture d'API en direct, l'implémentation appairée Python/TypeScript, les tests e2e limités aux modèles, et les règles d'interface pour les trois pages.

Qu'est-ce que model-message-stream-protocol ?

Ce skill définit le workflow de développement complet pour le protocole MMSP. Il est conçu pour être utilisé par des développeurs et des agents IA travaillant sur le dépôt `model-message-stream-protocol`. L'objectif principal est de guider l'ajout ou la mise à jour de la prise en charge d'un modèle d'IA, ce qui implique la synchronisation de la documentation officielle du fournisseur, la capture d'échanges d'API en direct, l'implémentation de clients en Python et TypeScript, et l'exécution de tests de bout en bout. Le workflow insiste sur l'utilisation de sources officielles, la capture d'API réelles comme référence principale (qui prévaut sur la documentation), et le respect d'un modèle de sécurité strict pour les identifiants. Il inclut également des directives pour le développement de l'interface utilisateur, notamment le playground, le traceur, et le site Astro. Le skill est applicable à divers outils d'agent IA et est essentiel pour maintenir le protocole de manière cohérente et fiable.

Compatible avec✓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 Productivité et collaboration skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Workflow de développement MMSP

L'ajout ou la mise à jour de la prise en charge d'un modèle suit les étapes ci-dessous, dans l'ordre. Lorsqu'une étape indique arrêtez-vous et demandez, faites une pause et demandez à l'utilisateur ; ne continuez pas tant que le problème n'est pas résolu et ne comblez jamais le vide vous-même.

Plan du répertoire

llmsdk_docs/<model_version>/      Instantané de la documentation officielle, un dossier par génération de modèle (README.md + docs/)
api_captures/<protocol>/          Captures d'API brutes ignorées par git : charges utiles des requêtes + événements de flux
src_py/mmsp/<client_type>/        Client Python, un dossier par type de client : <vendor>_official/ ou <protocol>/
src_py/mmsp/auto_client.py        Crée le client qu'un nom de type de client spécifie ; la famille d'un ID de modèle nomme son client officiel
src_ts/src/<client_type>/         Client TypeScript, miroir du dossier Python
src_ts/src/autoClient.ts          Routage TypeScript, miroir de auto_client.py
src_py/tests/test_client.py       Tests e2e paramétrés (AVAILABLE_MODELS avec vérification d'environnement)
src_ts/tests/client.test.ts       Même chose pour TypeScript
changelog/unreleased/             Où vont les modifications non publiées ; renommé en version lors de la publication
changelog/<version>/              Résumé de la version (README.md) plus un fichier de détail par entrée
changelog/README.md               Le format de l'entrée : bloc de métadonnées, règles du corps, appairage bilingue
CHANGELOG.md                      Une ligne brève par version pointant vers changelog/
src_py/mmsp/integration/          Le playground, les pages du traceur et le serveur MMSP, miroir dans src_ts/src/integration/
site/                             Le site Astro à mmsp.penguin.ooo
.agents/skills/mmsp-dev/kill-ai-slop/   Guide et scanner contre le 'slop' IA achetés pour le travail d'interface ci-dessous
<name>.zh.md                      Contrepartie chinoise, obligatoire pour chaque fichier dans changelog/

Étape 1 — Synchroniser la documentation officielle dans llmsdk_docs/

  • Les sources doivent être le site de documentation officiel du fournisseur du modèle (par exemple docs.anthropic.com, platform.openai.com, ai.google.dev). N'utilisez jamais de miroirs tiers, de billets de blog ou de mémoire de modèle.
  • Enregistrez l'instantané sous llmsdk_docs/<model_version>/ en suivant les conventions de dossier existantes et listez le dossier dans llmsdk_docs/README.md. L'exécution de ce workflow est la demande explicite que la règle du dépôt contre l'édition de llmsdk_docs/ demande.
  • Lorsque la documentation récupérée diffère d'un instantané existant, la nouvelle documentation officielle l'emporte : mettez à jour les anciens fichiers sur place.
  • L'instantané doit être suffisamment complet pour implémenter à partir de celui-ci : schémas de requête/réponse, séquence d'événements de flux, sortie de raisonnement, appel d'outils, champs d'utilisation et réponses d'erreur.
  • Arrêtez-vous et demandez si l'URL officielle est incertaine ou si une page ne peut pas être récupérée. L'utilisateur peut coller le contenu manuellement.

Étape 2 — Capturer un échange d'API en direct dans api_captures/

  • Condition préalable : la variable d'environnement de la clé API du fournisseur doit être définie et utilisable. Utilisez les mêmes variables d'environnement et URL de base que src_py/tests/test_client.py (condition AVAILABLE_MODELS et _create_client). Arrêtez-vous et demandez à l'utilisateur de fournir la clé si elle est manquante ; le workflow ne doit pas continuer sans elle.
  • En utilisant le SDK officiel du fournisseur, ou HTTP brut exactement comme documenté, exécutez une requête d'appel d'outil en streaming avec le raisonnement activé, puis renvoyez le résultat de l'outil pour que la capture montre également comment les tours de l'assistant sont renvoyés.
  • Enregistrez l'échange complet non modifié sous api_captures/<protocol>/ (ignoré par git), par exemple round1.request.json plus round1.stream.jsonl avec chaque événement de flux brut dans l'ordre. N'enregistrez jamais d'identifiants.
  • Lorsque quelque chose dans l'échange ne peut pas être sérialisé en JSON (charges utiles binaires, objets SDK contenant des valeurs non JSON), enregistrez sa représentation str() et analysez à partir de celle-ci. Gardez le conteneur analysable : enveloppez le texte dans un objet JSON pour que le .jsonl reste une valeur JSON par ligne ({"unserializable_str": "<str(event)>"}, en gardant l'événement dans l'ordre du flux), ou mettez tout l'échange dans un fichier .txt frère lorsque rien de celui-ci ne se sérialise. Une capture str() est toujours l'enregistrement faisant autorité de ce que l'API a renvoyé. Ne supprimez jamais l'événement, ne le modifiez pas à la main pour qu'il soit du JSON valide, et ne revenez pas à la documentation parce que la sérialisation a échoué.
  • Arrêtez-vous et demandez en cas d'erreur d'API (clé invalide, quota insuffisant, limite de débit). Ne simulez pas la réponse et ne continuez pas uniquement à partir de la documentation.
  • La capture est la référence d'implémentation principale et prévaut sur la documentation : là où elles divergent, implémentez ce que l'API a réellement renvoyé.
  • Sondez ensuite la relecture, car la capture montre seulement ce que l'API envoie, pas ce qu'elle doit recevoir en retour. Renvoyez le tour de l'assistant à plusieurs reprises, en supprimant à chaque fois un champ candidat — l'ID de l'élément de raisonnement, son contenu, l'élément de raisonnement entier, chaque champ que vous seriez tenté de conserver — et enregistrez quelles suppressions l'API accepte toujours avec le comportement du modèle intact. Ce résultat, et non la forme de la réponse, décide ce que l'étape 3 stocke. Enregistrez les résultats de la sonde à côté de la capture.

Étape 3 — Implémenter les clients Python et TypeScript

  • Un dossier par type de client, nommé d'après celui-ci : un client officiel par fournisseur (openai_official/, anthropic_official/, google_official/, zai_official/, moonshot_official/, deepseek_official/, minimax_official/) parle l'API propre du fournisseur, connaît chaque génération des modèles du fournisseur et lit la clé du fournisseur depuis l'environnement ; un client compatible par protocole filaire (openai_responses/, openai_chat/, openai_chat_vllm_adapter/, openai_embedding/, ant_messages/, google_genai/, mmsp/) parle ce protocole pour n'importe quel point de terminaison. La classe est nommée d'après le dossier (OpenAIOfficialClient, OpenaiChatClient), et le type de client est le nom du dossier avec des traits d'union (openai-official, openai-chat).
    • Une nouvelle génération des modèles d'un fournisseur va dans le client officiel du fournisseur. Différenciez le nouveau protocole (capture + documentation) par rapport à ce que le client envoie aujourd'hui ; là où une génération diffère, même par un seul nom de clé, distinguez les générations à l'intérieur du client par une version explicite ("4-6" in self._model, "gpt-6" in self._model), jamais par une sous-chaîne nue comme "claude" in model, et gardez les générations plus anciennes fonctionnelles. Ne supprimez jamais une génération à moins que l'utilisateur ne l'ordonne explicitement.
    • Un nouveau fournisseur obtient un nouveau dossier de client officiel, un nouveau type de client <vendor>-official dans auto_client.py / autoClient.ts, et une nouvelle ligne dans le tableau des familles de modèles là-bas : la famille est le préfixe par lequel chaque ID des modèles du fournisseur commence (glm-, kimi-), comparé sans tenir compte de la casse.
    • Un nouveau protocole filaire que plusieurs fournisseurs desservent obtient un dossier de client compatible et un type de client ; un client compatible ne porte aucune particularité de fournisseur.
  • auto_client.py / autoClient.ts créent le client qu'un client_type nomme, à partir de la table unique des types de client. Sans client_type, la famille par laquelle un ID de modèle commence nomme son client officiel ; un ID sans famille connue lève une exception et demande une famille. Le routage n'a aucune autre règle : pas de correspondance de sous-chaîne sur les ID de modèle, pas de correspondance de version en dehors des clients officiels.
  • Un identifiant va seulement là où il a été donné. C'est le modèle de sécurité, et chaque constructeur de client le suit. Une clé passée comme api_key est utilisée telle quelle. Un client qui lit la clé générique de l'environnement (OPENAI_API_KEY pour openai_official et les clients compatibles avec le protocole OpenAI, ANTHROPIC_API_KEY pour anthropic_official et ant_messages, GEMINI_API_KEY pour google_genai) ne la lit que lorsque le point de terminaison vient aussi de l'environnement (OPENAI_BASE_URL / ANTHROPIC_BASE_URL / GEMINI_BASE_URL, ou la valeur par défaut du fournisseur) : donné un base_url et sans api_key, il lève une erreur à la construction au lieu d'envoyer cette clé à un autre point de terminaison. C'est resolve_credentials / resolveCredentials dans utils ; appelez-la, ne la réimplémentez pas. Tout autre client officiel (deepseek_official, zai_official, moonshot_official, minimax_official, google_official) lit sa propre variable (DEEPSEEK_API_KEY, ZAI_API_KEY, MOONSHOT_API_KEY, MINIMAX_API_KEY, GEMINI_API_KEY) quelle que soit l'URL de base qui lui est donnée, et jamais la générique ; sans l'une ou l'autre, il lève <VAR> is required for <Client>.. Remettez chaque identifiant au SDK du fournisseur explicitement, y compris "aucun" (authToken: null en TypeScript, un auth_token effacé en Python), car les SDK remplissent ce qui ne leur est pas remis à partir de leurs propres variables d'environnement. Les tests hors ligne dans tests/env-credentials.test.ts / tests/test_env_credentials.py construisent chaque client dans un environnement contrôlé et lisent l'identifiant que l'instance du SDK détient ; ajoutez-leur un nouveau client.
  • La relecture doit être minimale, pas exhaustive. Un tour d'assistant relu doit être accepté par l'API avec le comportement du modèle intact ; il n'a pas à reproduire le champ de l'élément filaire champ pour champ. fidelity est un canal d'exception, pas un miroir de la charge utile : un champ y gagne une place uniquement lorsque la sonde de l'étape 2 montre que la requête échoue ou que le modèle se dégrade sans lui. Les ID générés par le fournisseur que l'API régénère ou ignore — un ID d'élément de raisonnement, un ID d'élément de sortie — restent en dehors. Les champs que l'API exige vont dedans : le phase de GPT-5.5, la signature de raisonnement de Claude, le nom exact du champ de raisonnement qu'un amont strict exige.
  • Ne copiez jamais dans fidelity ce qu'un champ universel transporte déjà — le texte de raisonnement, le nom d'un appel d'outil ou ses arguments analysés. Reconstruisez plutôt l'élément filaire à partir des champs universels.
  • tool_call_id n'est pas optionnel : capturez toujours l'ID d'appel du fournisseur et rejouez-le. Lorsque le format filaire porte à la fois un ID d'élément et un ID d'appel, l'ID d'appel est celui qui corrèle un résultat à son appel.
  • Suivez le client de référence ; ne reconcevez pas. openai_official/ est la forme pour les protocoles de style Responses, openai_chat/ pour les Chat Completions : même ordre de méthodes, même flux de contrôle, mêmes noms. Un nouveau client doit se lire comme une différence par rapport à sa référence, car c'est ainsi qu'il sera examiné.
  • Gardez-le lisible de haut en bas. Un lecteur doit suivre une requête ou une réponse directement à travers le fichier sans sauter entre les définitions, donc l'inlining est la valeur par défaut : lisez les attributs du SDK directement (model_output.delta, model_output.item.call_id) et gardez l'accès aux champs, l'arithmétique d'utilisation et le texte d'erreur là où ils sont utilisés. Extrayez une fonction d'aide uniquement pour un bloc vraiment volumineux et autonome — le genre qui enterrerait le flux principal s'il était inline, comme la récupération et le décodage d'une image — jamais pour quelques lignes. Miroir des méthodes privées propres du client de référence (_convert_thinking_level_*, _convert_tool_choice) au lieu d'inventer une couche à côté d'elles ; une couche d'adaptation qui accepte à la fois des dictionnaires et des objets SDK n'en fait jamais partie, car les événements sont typés avec les types propres du SDK.
  • Les clients produisent uniquement des deltas ; la classe de base ferme les éléments. transform_model_output_to_uni_event / transformModelOutputToUniEvent transforme un événement filaire en un UniEvent dont les content_items sont les éléments .delta que cet événement transporte, dans l'ordre filaire, et jamais un élément .done. _streaming_response_internal / _streamingResponseInternal construit la requête, ouvre le flux et génère un événement par événement filaire : une boucle simple, sans comptabilité d'élément, sans accumulateur, sans analyse d'argument, sans fusion d'utilisation et sans arrêt synthétisé. usage_metadata / finish_reason chevauchent les événements stop là où le filaire les rapporte — message_start et message_delta d'Anthropic, les morceaux de fin et d'utilisation des Chat Completions, response.completed — fusionnés champ par champ par la base ; l'événement stop d'un client ne termine jamais le flux public. Un événement filaire sans rien d'universel est l'événement delta vide : pas d'éléments, utilisation et raison de fin nulles. La classe de base remet chaque delta à StreamItems (stream_items.py / streamItems.ts), qui l'envoie dès qu'il arrive et ferme chaque élément avec son élément .done lorsque l'élément suivant commence ou que le flux du client se termine ; la base émet ensuite l'événement stop unique ou lève EmptyResponseError. Les événements qui violent le protocole lèvent StreamProtocolError dans tous les modes, et les tests e2e partagés vérifient le flux de chaque client avec assert_stream_grammar / assertStreamGrammar.
  • Les deltas d'un même élément sont contigus et partagent un seul fidelity.item_id. La sortie du modèle est séquentielle, donc un élément coule à la fois et les événements de fin d'élément du fournisseur (content_block_stop, response.output_item.done, step.stop) sont listés comme des non-opérations : une passerelle qui ferme ses éléments en retard ou dans le désordre ne change rien. Passez l'ID propre du fournisseur, converti en chaîne, comme fidelity.item_id : l'index du bloc Anthropic, le item_id de l'événement Responses ou item.id or item.call_id (None / undefined là où une passerelle n'en envoie pas), l'index de l'étape Interactions, le champ filaire des Chat Completions (reasoning_content, reasoning, content, tool_calls), le type de partie generateContent (function_call, thought, inline_thinking, inline_data, text) ; les vecteurs d'intégration n'en portent aucun. StreamItems le supprime, donc il n'atteint jamais les consommateurs, les traces ou les historiques.
  • StreamItems décide où un élément se termine ; les clients ne le font jamais. Un delta appartient à l'élément suivant lorsqu'il porte un autre item_id, est d'un autre type (une étape de pensée Interactions passant texte, image, texte correspond à trois éléments sous un seul index), ou commence un élément par lui-même : un tool_call.delta portant un nom (chaque fournisseur l'envoie une fois, sur le premier delta de l'appel, donc les appels Chat Completions n'ont pas besoin de leurs propres ID), une image (inline_thinking, ou inline_data avec un type MIME image/*, tandis que l'audio coule en morceaux d'un élément), un vecteur d'intégration. Sinon, il continue l'élément qui coule maintenant : un delta sans ID le fait, de même que les arguments d'un appel quel que soit l'ID qu'une passerelle leur met. La fidélité envoyée seule sous l'ID de l'élément est celle de cet élément, quel que soit le type qui la porte (un thought_signature Interactions après une pensée image). La table par type dans ce fichier — le champ croissant, les champs d'en-tête du premier delta, comment les morceaux se joignent — est le seul code par type dans le flux : l'élément fait est le premier delta de l'élément avec le champ croissant remplacé par la jointure de

... [tronqué pour la traduction]

Skills associés