MMSP-Entwicklungsworkflow
Das Hinzufügen oder Aktualisieren von Modellunterstützung erfolgt in den unten aufgeführten Phasen in der angegebenen Reihenfolge. Wenn in einer Phase stop and ask (anhalten und fragen) steht, pausieren Sie und fragen Sie den Benutzer; fahren Sie nicht fort, bis das Problem gelöst ist, und füllen Sie die Lücke niemals selbst.
Verzeichnisstruktur
llmsdk_docs/<model_version>/ Offizieller Docs-Snapshot, ein Ordner pro Modellgeneration (README.md + docs/)
api_captures/<protocol>/ Von Git ignorierte, rohe API-Erfassungen: Anfrage-Payloads + Stream-Events
src_py/mmsp/<client_type>/ Python-Client, ein Ordner pro Client-Typ: <vendor>_official/ oder <protocol>/
src_py/mmsp/auto_client.py Erstellt den Client, den ein client_type benennt; die Familie einer Modell-ID benennt ihren offiziellen Client
src_ts/src/<client_type>/ TypeScript-Client, spiegelt den Python-Ordner wider
src_ts/src/autoClient.ts TypeScript-Routing, spiegelt auto_client.py wider
src_py/tests/test_client.py Parametrisierte E2E-Tests (umgebungsgesteuert durch AVAILABLE_MODELS)
src_ts/tests/client.test.ts Gleiches für TypeScript
changelog/unreleased/ Wo noch nicht veröffentlichte Änderungen abgelegt werden; bei Veröffentlichung in die Version umbenannt
changelog/<version>/ Release-Zusammenfassung (README.md) plus eine Detaildatei pro Eintrag
changelog/README.md Das Format der Einträge: Metadatenblock, Textregeln, zweisprachige Paarung
CHANGELOG.md Eine kurze Zeile pro Release mit Link zu changelog/
src_py/mmsp/integration/ Das Playground, die Tracer-Seiten und der MMSP-Server, gespiegelt in src_ts/src/integration/
site/ Die Astro-Website unter mmsp.penguin.ooo
.agents/skills/mmsp-dev/kill-ai-slop/ Eingekaufter AI-Slop-Leitfaden und Scanner für die UI-Arbeit unten
<name>.zh.md Chinesisches Gegenstück, erforderlich für jede Datei in changelog/
Phase 1 – Offizielle Docs in llmsdk_docs/ synchronisieren
- Quellen müssen die offizielle Dokumentationsseite des Modellanbieters sein (z. B. docs.anthropic.com, platform.openai.com, ai.google.dev). Verwenden Sie niemals Drittanbieter-Spiegel, Blogbeiträge oder Modellgedächtnis.
- Speichern Sie den Snapshot unter
llmsdk_docs/<model_version>/gemäß den bestehenden Ordnerkonventionen und listen Sie den Ordner inllmsdk_docs/README.mdauf. Das Ausführen dieses Workflows ist die explizite Anfrage, die die Repository-Regel gegen das Bearbeiten vonllmsdk_docs/verlangt. - Wenn sich die abgerufenen Docs von einem vorhandenen Snapshot unterscheiden, gewinnen die neuen offiziellen Docs: Aktualisieren Sie die alten Dateien direkt.
- Der Snapshot muss vollständig genug für die Implementierung sein: Request/Response-Schemas, Streaming-Event-Sequenz, Thinking-Ausgabe, Tool-Aufrufe, Usage-Felder und Fehlerantworten.
- Stop and ask (Anhalten und fragen), wenn die offizielle URL unsicher ist oder eine Seite nicht abgerufen werden kann. Der Benutzer kann den Inhalt manuell einfügen.
Phase 2 – Live-API-Austausch in api_captures/ erfassen
- Voraussetzung: Die API-Key-Umgebungsvariable des Anbieters muss gesetzt und verwendbar sein. Verwenden Sie dieselben Umgebungsvariablen und Basis-URLs wie
src_py/tests/test_client.py(AVAILABLE_MODELS-Steuerung und_create_client). Stop and ask (Anhalten und fragen) Sie den Benutzer, den Schlüssel bereitzustellen, wenn er fehlt; der Workflow darf ohne ihn nicht fortgesetzt werden. - Führen Sie unter Verwendung des offiziellen SDKs des Anbieters oder von rohem HTTP genau wie dokumentiert eine Streaming-Tool-Call-Anfrage mit aktiviertem Thinking aus, senden Sie dann das Tool-Ergebnis zurück, sodass die Erfassung auch zeigt, wie Assistant-Turns erneut gesendet werden.
- Speichern Sie den vollständigen Austausch unverändert unter
api_captures/<protocol>/(von Git ignoriert), z. B.round1.request.jsonplusround1.stream.jsonlmit jedem rohen Stream-Event in der richtigen Reihenfolge. Speichern Sie niemals Anmeldedaten. - Wenn etwas im Austausch nicht in JSON serialisiert werden kann (binäre Payloads, SDK-Objekte mit Nicht-JSON-Werten), speichern Sie seine
str()-Form und analysieren Sie daraus. Halten Sie den Container analysierbar: Wickeln Sie den Text in ein JSON-Objekt, sodass die.jsonleinen JSON-Wert pro Zeile enthält ({"unserializable_str": "<str(event)>"}, wobei das Event in der Stream-Reihenfolge bleibt), oder legen Sie den gesamten Austausch in eine nebengeordnete.txt-Datei, wenn nichts daran serialisierbar ist. Einestr()-Erfassung ist immer noch die maßgebliche Aufzeichnung dessen, was die API zurückgegeben hat. Lassen Sie das Event niemals weg, bearbeiten Sie es nicht zu gültigem JSON oder greifen Sie nicht auf die Docs zurück, weil die Serialisierung fehlgeschlagen ist. - Stop and ask (Anhalten und fragen) bei jedem API-Fehler (ungültiger Schlüssel, unzureichendes Kontingent, Ratenbegrenzung). Simulieren Sie die Antwort nicht und fahren Sie nicht allein mit den Docs fort.
- Die Erfassung ist die primäre Implementierungsreferenz und hat Vorrang vor den Docs: Wenn sie sich unterscheiden, implementieren Sie, was die API tatsächlich zurückgegeben hat.
- Testen Sie dann das Replay, da die Erfassung nur zeigt, was die API sendet, nicht was sie zurück erwartet. Senden Sie den Assistant-Turn wiederholt, jedes Mal mit einem entfernten Kandidatenfeld – der ID des Reasoning-Items, seinem Inhalt, dem gesamten Reasoning-Item, jedem Feld, das Sie behalten wollten – und notieren Sie, welche Entfernungen die API noch mit intaktem Modellverhalten akzeptiert. Dieses Ergebnis, nicht die Form der Antwort, entscheidet, was Phase 3 speichert. Speichern Sie die Testergebnisse neben der Erfassung.
Phase 3 – Python- und TypeScript-Clients implementieren
- Ein Ordner pro Client-Typ, benannt nach ihm: Ein offizieller Client pro Anbieter (
openai_official/,anthropic_official/,google_official/,zai_official/,moonshot_official/,deepseek_official/,minimax_official/) spricht die eigene API des Anbieters, kennt jede Generation der Modelle des Anbieters und liest den Schlüssel des Anbieters aus der Umgebung; ein kompatibler Client pro Drahtprotokoll (openai_responses/,openai_chat/,openai_chat_vllm_adapter/,openai_embedding/,ant_messages/,google_genai/,mmsp/) spricht dieses Protokoll für jeden Endpunkt. Die Klasse wird nach dem Ordner benannt (OpenAIOfficialClient,OpenaiChatClient), und der Client-Typ ist der Ordnername mit Bindestrichen (openai-official,openai-chat).- Eine neue Generation der Modelle eines Anbieters kommt in den offiziellen Client des Anbieters. Vergleichen Sie das neue Protokoll (Erfassung + Docs) mit dem, was der Client heute sendet; wenn sich eine Generation unterscheidet, selbst um einen einzelnen Schlüsselnamen, unterscheiden Sie die Generationen innerhalb des Clients durch explizite Version (
"4-6" in self._model,"gpt-6" in self._model), niemals durch einen bloßen Teilstring wie"claude" in model, und lassen Sie die älteren Generationen funktionsfähig. Entfernen Sie niemals eine Generation, es sei denn, der Benutzer weist explizit dazu an. - Ein neuer Anbieter erhält einen neuen offiziellen Client-Ordner, einen neuen
<vendor>-officialClient-Typ inauto_client.py/autoClient.tsund eine neue Zeile in der Modellfamilientabelle dort: Die Familie ist das Präfix, mit dem jede ID der Modelle des Anbieters beginnt (glm-,kimi-), groß-/kleinschreibungsunabhängig abgeglichen. - Ein neues Drahtprotokoll, das von mehreren Anbietern bedient wird, erhält einen kompatiblen Client-Ordner und Client-Typ; ein kompatibler Client trägt keine Besonderheiten des Anbieters.
- Eine neue Generation der Modelle eines Anbieters kommt in den offiziellen Client des Anbieters. Vergleichen Sie das neue Protokoll (Erfassung + Docs) mit dem, was der Client heute sendet; wenn sich eine Generation unterscheidet, selbst um einen einzelnen Schlüsselnamen, unterscheiden Sie die Generationen innerhalb des Clients durch explizite Version (
auto_client.py/autoClient.tserstellen den Client, den einclient_typebenennt, aus der einen Tabelle der Client-Typen. Ohneclient_typebenennt die Familie, mit der eine Modell-ID beginnt, ihren offiziellen Client; eine ID ohne bekannte Familie löst einen Fehler aus und fragt nach einer. Das Routing hat keine andere Regel: keine Teilstring-Übereinstimmung bei Modell-IDs, keine Versionsübereinstimmung außerhalb der offiziellen Clients.- Eine Anmeldeinformation wird nur dorthin gegeben, wofür sie gegeben wurde. Dies ist das Sicherheitsmodell, und jeder Client-Konstruktor befolgt es. Ein als
api_keyübergebener Schlüssel wird so verwendet, wie er ist. Ein Client, der den generischen Schlüssel der Umgebung liest (OPENAI_API_KEYfüropenai_officialund die OpenAI-Protokoll-kompatiblen Clients,ANTHROPIC_API_KEYfüranthropic_officialundant_messages,GEMINI_API_KEYfürgoogle_genai), liest ihn nur, wenn der Endpunkt ebenfalls aus der Umgebung kommt (OPENAI_BASE_URL/ANTHROPIC_BASE_URL/GEMINI_BASE_URL, oder die eigene Voreinstellung des Anbieters): Bei einerbase_urlund ohneapi_keylöst er beim Bauen einen Fehler aus, anstatt diesen Schlüssel an einen anderen Endpunkt zu senden. Das istresolve_credentials/resolveCredentialsin den Hilfsprogrammen; rufen Sie es auf, implementieren Sie es nicht neu. Jeder andere offizielle Client (deepseek_official,zai_official,moonshot_official,minimax_official,google_official) liest seine eigene Variable (DEEPSEEK_API_KEY,ZAI_API_KEY,MOONSHOT_API_KEY,MINIMAX_API_KEY,GEMINI_API_KEY) unabhängig von der übergebenen Basis-URL und niemals die generische; ohne eine von beiden löst er<VAR> is required for <Client>.aus. Übergeben Sie jede Anmeldeinformation explizit an das Anbieter-SDK, einschließlich "none" (authToken: nullin TypeScript, ein gelöschtesauth_tokenin Python), da die SDKs alles ausfüllen, was sie nicht erhalten, aus ihren eigenen Umgebungsvariablen. Die Offline-Tests intests/env-credentials.test.ts/tests/test_env_credentials.pybauen jeden Client unter einer kontrollierten Umgebung auf und lesen die Anmeldeinformation, die die SDK-Instanz hält; fügen Sie einen neuen Client zu ihnen hinzu. - Das Replay muss minimal, nicht erschöpfend sein. Ein wiederholter Assistant-Turn muss von der API mit intaktem Modellverhalten akzeptiert werden; er muss das Wire-Item nicht Feld für Feld reproduzieren.
fidelityist ein Ausnahmekanal, kein Spiegel der Payload: Ein Feld verdient dort nur einen Platz, wenn die Phase-2-Untersuchung zeigt, dass die Anfrage fehlschlägt oder das Modell ohne es degradiert. Von Anbietern generierte IDs, die die API neu generiert oder ignoriert – eine Reasoning-Item-ID, eine Output-Item-ID – bleiben außen vor. Felder, die die API verlangt, kommen hinein: GPT-5.5'sphase, Claudes Thinking-signature, der genaue Reasoning-Feldname, den eine strenge vorgeschaltete Stelle erfordert. - Kopieren Sie niemals in
fidelity, was ein universelles Feld bereits trägt – den Thinking-Text, den Namen eines Tool-Aufrufs oder seine analysierten Argumente. Bauen Sie das Wire-Item stattdessen aus den universellen Feldern neu auf. tool_call_idist nicht optional: Erfassen Sie immer die Aufruf-ID des Anbieters und spielen Sie sie erneut ab. Wenn das Wire-Format sowohl eine Item-ID als auch eine Aufruf-ID trägt, ist die Aufruf-ID diejenige, die ein Ergebnis mit seinem Aufruf korreliert.- Folgen Sie dem Referenzclient; entwerfen Sie nicht neu.
openai_official/ist die Form für Responses-ähnliche Protokolle,openai_chat/für Chat Completions: gleiche Methodenreihenfolge, gleicher Steuerfluss, gleiche Namen. Ein neuer Client sollte wie ein Diff gegen seinen Referenzclient lesbar sein, denn so wird er überprüft. - Halten Sie es von oben nach unten lesbar. Ein Leser sollte eine Anfrage oder Antwort direkt durch die Datei verfolgen können, ohne zwischen Definitionen springen zu müssen. Daher ist das Inlining die Voreinstellung: Lesen Sie die Attribute des SDKs direkt (
model_output.delta,model_output.item.call_id) und halten Sie Feldzugriff, Usage-Arithmetik und Fehlertexte dort, wo sie verwendet werden. Extrahieren Sie einen Helfer nur für einen wirklich großen, in sich geschlossenen Block – die Art, die den Hauptfluss begraben würde, wenn sie inline wäre, wie das Abrufen und Dekodieren eines Bildes – niemals für ein paar Zeilen. Spiegeln Sie die eigenen privaten Methoden des Referenzclients wider (_convert_thinking_level_*,_convert_tool_choice), anstatt eine Ebene daneben zu erfinden; ein Shim, der sowohl Diktate als auch SDK-Objekte akzeptiert, ist niemals einer von ihnen, da die Events mit den eigenen Typen des SDKs typisiert sind. - Clients geben nur Deltas ab; die Basisklasse schließt die Items.
transform_model_output_to_uni_event/transformModelOutputToUniEventwandelt ein Wire-Event in einUniEventum, dessencontent_itemsdie.delta-Items sind, die dieses Event trägt, in Wire-Reihenfolge, und niemals ein.done-Item._streaming_response_internal/_streamingResponseInternalbaut die Anfrage, öffnet den Stream und gibt ein Event pro Wire-Event aus: eine einfache Schleife, ohne Item-Buchhaltung, ohne Akkumulator, ohne Argument-Parsing, ohne Usage-Zusammenführung und ohne synthetisiertem Stopp.usage_metadata/finish_reasonreiten aufstop-Events, wo das Wire sie meldet – Anthropicsmessage_startundmessage_delta, die Chat-Completions-Finish- und Usage-Chunks,response.completed– Feld für Feld von der Basis zusammengeführt; einstop-Event eines Clients beendet niemals den öffentlichen Stream. Ein Wire-Event ohne etwas Universelles ist das leere Delta-Event: keine Items, null Usage und Finish Reason. Die Basisklasse übergibt jedes Delta anStreamItems(stream_items.py/streamItems.ts), das es sofort ausgibt und jedes Item mit seinem.done-Item schließt, wenn das nächste Item beginnt oder der Client-Stream endet; die Basis gibt dann das eine Stop-Event aus oder löstEmptyResponseErroraus. Events, die das Protokoll verletzen, lösen in jedem ModusStreamProtocolErroraus, und die gemeinsamen E2E-Tests überprüfen jeden Client-Stream mitassert_stream_grammar/assertStreamGrammar. - Die Deltas eines Items sind zusammenhängend und teilen sich eine
fidelity.item_id. Die Modellausgabe ist seriell, sodass jeweils ein Item streamt und die End-of-Item-Events des Anbieters (content_block_stop,response.output_item.done,step.stop) als No-Ops aufgeführt werden: Ein Gateway, das seine Items spät oder in falscher Reihenfolge schließt, ändert nichts. Übergeben Sie die eigene ID des Anbieters, als Zeichenkette, alsfidelity.item_id: den Anthropic-Block-index, das Responses-Eventitem_idoderitem.id or item.call_id(None/undefined, wo ein Gateway keine sendet), den Interactions-Schritt-index, das Chat-Completions-Wire-Feld (reasoning_content,reasoning,content,tool_calls), die generateContent-Teil-Art (function_call,thought,inline_thinking,inline_data,text); Einbettungsvektoren tragen keine.StreamItemsentfernt sie, sodass sie niemals Verbraucher, Ablaufverfolgungen oder Verläufe erreicht. StreamItemsentscheidet, wo ein Item endet; Clients tun das nie. Ein Delta gehört zum nächsten Item, wenn es eine andereitem_idträgt, von einer anderen Art ist (ein Interactions-Thought-Schritt, der Text, Bild, Text wird, sind drei Items unter einem Index), oder ein Item für sich beginnt: eintool_call.delta, das einen Namen trägt (jeder Anbieter sendet ihn einmal, beim ersten Delta des Aufrufs, daher benötigen Chat-Completions-Aufrufe keine eigenen IDs), ein Bild (inline_thinking, oderinline_datamit einemimage/*-MIME-Typ, während Audio in Chunks eines Items streamt), ein Einbettungsvektor. Andernfalls setzt es das jetzt streamende Item fort: Ein Delta ohne ID tut das, ebenso wie die Argumente eines Aufrufs, unabhängig von der ID, die ein Gateway darauf setzt. Fidelity allein unter der ID des Items gesendet, gehört zu diesem Item, unabhängig von der Art, die es trägt (ein Interactionsthought_signaturenach einem Bild-thought). Die tabelle pro Art in dieser Datei – das wachsende Feld, die Kopffelder des ersten Deltas, wie die Chunks zusammengefügt werden – ist der einzige Pro-Art-Code im Stream: Das Done-Item ist das erste Delta des Items, wobei das wachsende Feld durch die Verbindung der
…
[gekürzt für die Übersetzung]