¿Qué hace Flujo de Trabajo de Desarrollo de MMSP?
Agregar o actualizar el soporte de modelos sigue las etapas a continuación, en orden. Cuando una etapa dice detenerse y preguntar, pausa y pregunta al usuario; no continúes hasta que el problema esté resuelto y nunca llenes el vacío tú mismo.
Mapa de directorios
llmsdk_docs/<model_version>/ Captura instantánea de la documentación oficial, una carpeta por generación de modelo (README.md + docs/)
api_captures/<protocol>/ Capturas de API sin procesar, ignoradas por git: cargas útiles de solicitudes + eventos de streaming
src_py/mmsp/<client_type>/ Cliente Python, una carpeta por tipo de cliente: <vendor>_official/ o <protocol>/
src_py/mmsp/auto_client.py Crea el cliente para un tipo de cliente; la familia de un ID de modelo nombra su cliente oficial
src_ts/src/<client_type>/ Cliente TypeScript, refleja la carpeta de Python
src_ts/src/autoClient.ts Enrutamiento TypeScript, refleja auto_client.py
src_py/tests/test_client.py Pruebas e2e parametrizadas (AVAILABLE_MODELS limitados por entorno)
src_ts/tests/client.test.ts Lo mismo para TypeScript
changelog/unreleased/ Donde van los cambios no publicados; renombrado a la versión en el lanzamiento
changelog/<version>/ Resumen del lanzamiento (README.md) más un archivo de detalle por entrada
changelog/README.md El formato de entrada: bloque de metadatos, reglas del cuerpo, emparejamiento bilingüe
CHANGELOG.md Una línea breve por lanzamiento que enlaza a changelog/
src_py/mmsp/integration/ El playground, las páginas del tracer y el servidor MMSP, reflejado en src_ts/src/integration/
site/ El sitio Astro en mmsp.penguin.ooo
.agents/skills/mmsp-dev/kill-ai-slop/ Guía y escáner de AI-slop integrados para el trabajo de UI a continuación
<name>.zh.md Contraparte en chino, requerida para cada archivo en changelog/
Etapa 1 — Sincronizar documentos oficiales en llmsdk_docs/
- Las fuentes deben ser el sitio de documentación oficial del proveedor del modelo (por ejemplo, docs.anthropic.com, platform.openai.com, ai.google.dev). Nunca uses espejos de terceros, publicaciones de blog o memoria del modelo.
- Guarda la captura instantánea en
llmsdk_docs/<model_version>/siguiendo las convenciones de carpeta existentes y lista la carpeta enllmsdk_docs/README.md. Ejecutar este flujo de trabajo es la solicitud explícita que la regla del repositorio contra la edición dellmsdk_docs/solicita. - Cuando los documentos obtenidos difieran de una captura existente, los nuevos documentos oficiales ganan: actualiza los archivos antiguos en su lugar.
- La captura debe ser lo suficientemente completa para implementar: esquemas de solicitud/respuesta, secuencia de eventos de streaming, salida de pensamiento, llamadas a herramientas, campos de uso y respuestas de error.
- Detente y pregunta si la URL oficial es incierta o no se puede obtener una página. El usuario puede pegar el contenido manualmente.
Etapa 2 — Capturar un intercambio de API en vivo en api_captures/
- Puerta: la variable de entorno de la clave API del proveedor debe estar configurada y ser utilizable. Usa las mismas variables de entorno y URLs base que
src_py/tests/test_client.py(limitación deAVAILABLE_MODELSy_create_client). Detente y pregunta al usuario que proporcione la clave si falta; el flujo de trabajo no debe continuar sin ella. - Usando el SDK oficial del proveedor, o HTTP sin procesar exactamente como está documentado, ejecuta una solicitud de llamada a herramienta de streaming con pensamiento habilitado, luego envía el resultado de la herramienta de vuelta para que la captura también muestre cómo se reenvían los turnos del asistente.
- Guarda el intercambio completo sin modificar en
api_captures/<protocol>/(ignorado por git), por ejemplo,round1.request.jsonmásround1.stream.jsonlcon cada evento de streaming sin procesar en orden. Nunca guardes credenciales. - Cuando algo en el intercambio no se serialice a JSON (cargas útiles binarias, objetos SDK que contengan valores no JSON), guarda su forma
str()y analiza a partir de eso. Mantén el contenedor analizable: envuelve el texto en un objeto JSON para que el.jsonlmantenga un valor JSON por línea ({"unserializable_str": "<str(event)>"}, manteniendo el evento en orden de streaming), o pon todo el intercambio en un archivo.txthermano cuando nada de él se serialice. Una capturastr()sigue siendo el registro autorizado de lo que devolvió la API. Nunca descartes el evento, lo edites manualmente a JSON válido o recurras a la documentación porque la serialización falló. - Detente y pregunta ante cualquier error de API (clave no válida, cuota insuficiente, límite de velocidad). No simules la respuesta ni continúes solo con la documentación.
- La captura es la referencia de implementación principal y supera a la documentación: donde no coincidan, implementa lo que la API realmente devolvió.
- Luego, prueba la reproducción, porque la captura solo muestra lo que la API envía, no lo que necesita recibir. Reenvía el turno del asistente repetidamente, cada vez con un campo candidato eliminado (el id del elemento de razonamiento, su contenido, todo el elemento de razonamiento, cada campo que estuviste tentado a mantener) y registra qué eliminaciones la API aún acepta con el comportamiento del modelo intacto. Ese resultado, no la forma de la respuesta, decide lo que almacena la Etapa 3. Guarda los resultados de la prueba junto a la captura.
Etapa 3 — Implementar los clientes Python y TypeScript
- Una carpeta por tipo de cliente, nombrada después de él: un cliente oficial por proveedor (
openai_official/,anthropic_official/,google_official/,zai_official/,moonshot_official/,deepseek_official/,minimax_official/) habla la API propia del proveedor, conoce cada generación de los modelos del proveedor y lee la clave del proveedor del entorno; un cliente compatible por protocolo de red (openai_responses/,openai_chat/,openai_chat_vllm_adapter/,openai_embedding/,ant_messages/,google_genai/,mmsp/) habla ese protocolo para cualquier endpoint. La clase se nombra después de la carpeta (OpenAIOfficialClient,OpenaiChatClient), y el tipo de cliente es el nombre de la carpeta con guiones (openai-official,openai-chat).- Una nueva generación de modelos de un proveedor va al cliente oficial de ese proveedor. Diferencia el nuevo protocolo (captura + documentación) contra lo que el cliente envía hoy; donde una generación difiera, incluso por un solo nombre de clave, distingue las generaciones dentro del cliente por versión explícita (
"4-6" in self._model,"gpt-6" in self._model), nunca por una subcadena simple como"claude" in model, y mantén las generaciones anteriores funcionando. Nunca elimines una generación a menos que el usuario lo indique explícitamente. - Un nuevo proveedor obtiene una nueva carpeta de cliente oficial, un nuevo tipo de cliente
<vendor>-officialenauto_client.py/autoClient.tsy una nueva fila en la tabla de familias de modelos allí: la familia es el prefijo con el que comienza cada ID de los modelos del proveedor (glm-,kimi-), comparado sin distinción de mayúsculas y minúsculas. - Un nuevo protocolo de red al que atienden varios proveedores obtiene una carpeta de cliente compatible y un tipo de cliente compatible; un cliente compatible no lleva peculiaridades de proveedor.
- Una nueva generación de modelos de un proveedor va al cliente oficial de ese proveedor. Diferencia el nuevo protocolo (captura + documentación) contra lo que el cliente envía hoy; donde una generación difiera, incluso por un solo nombre de clave, distingue las generaciones dentro del cliente por versión explícita (
auto_client.py/autoClient.tscrean el cliente que nombra unclient_type, a partir de la única tabla de tipos de cliente. Sin unclient_type, la familia con la que comienza un ID de modelo nombra su cliente oficial; un ID de familia desconocida genera un error y pide una. El enrutamiento no tiene otra regla: sin coincidencia de subcadenas en los IDs de modelo, sin coincidencia de versión fuera de los clientes oficiales.- Una credencial va solo a donde fue dada. Este es el modelo de seguridad, y cada constructor de cliente lo sigue. Una clave pasada como
api_keyse usa tal cual. Un cliente que lee la clave genérica del entorno (OPENAI_API_KEYparaopenai_officialy los clientes compatibles con el protocolo OpenAI,ANTHROPIC_API_KEYparaanthropic_officialyant_messages,GEMINI_API_KEYparagoogle_genai) la lee solo cuando el endpoint también proviene del entorno (OPENAI_BASE_URL/ANTHROPIC_BASE_URL/GEMINI_BASE_URL, o el valor predeterminado del proveedor): dado unbase_urly sinapi_key, genera un error en la construcción en lugar de enviar esa clave a otro endpoint. Eso esresolve_credentials/resolveCredentialsenutils; llámalo, no lo reimplementes. Cualquier otro cliente oficial (deepseek_official,zai_official,moonshot_official,minimax_official,google_official) lee su propia variable (DEEPSEEK_API_KEY,ZAI_API_KEY,MOONSHOT_API_KEY,MINIMAX_API_KEY,GEMINI_API_KEY) sea cual sea la URL base que se le dé, y nunca la genérica; sin ella, genera un error<VAR> is required for <Client>.. Entrega cada credencial explícitamente al SDK del proveedor, incluyendo "ninguno" (authToken: nullen TypeScript, unauth_tokenlimpiado en Python), porque los SDKs llenan lo que no se les entrega de sus propias variables de entorno. Las pruebas sin conexión entests/env-credentials.test.ts/tests/test_env_credentials.pyconstruyen cada cliente bajo un entorno controlado y leen la credencial que contiene la instancia del SDK; agrega un nuevo cliente a ellas. - La reproducción debe ser mínima, no exhaustiva. Un turno de asistente reproducido debe ser aceptado por la API con el comportamiento del modelo intacto; no tiene que reproducir el elemento de red campo por campo.
fidelityes un canal de excepción, no un espejo de la carga útil: un campo se gana un lugar allí solo cuando la prueba de la Etapa 2 muestra que la solicitud falla o el modelo se degrada sin él. Los IDs generados por el proveedor que la API regenera o ignora (un ID de elemento de razonamiento, un ID de elemento de salida) se quedan fuera. Los campos que la API exige van: laphasede GPT-5.5, lasignaturede pensamiento de Claude, el nombre exacto del campo de razonamiento que requiere un upstream estricto. - Nunca copies en
fidelitylo que un campo universal ya contiene (el texto de pensamiento, el nombre o los argumentos analizados de una llamada a herramienta). Reconstruye el elemento de red a partir de los campos universales. tool_call_idno es opcional: siempre captura el ID de llamada del proveedor y reprodúcelo. Cuando el formato de red lleva tanto un ID de elemento como un ID de llamada, el ID de llamada es el que correlaciona un resultado con su llamada.- Sigue al cliente de referencia; no rediseñes.
openai_official/es la forma para protocolos estilo Responses,openai_chat/para Chat Completions: mismo orden de métodos, mismo flujo de control, mismos nombres. Un nuevo cliente debe leerse como una diferencia con respecto a su referencia, porque así es como se revisará. - Mantenlo legible de principio a fin. Un lector debe poder seguir una solicitud o respuesta directamente a través del archivo sin saltar entre definiciones, por lo que la inserción en línea es el valor predeterminado: lee los atributos del SDK directamente (
model_output.delta,model_output.item.call_id) y mantén el acceso a campos, la aritmética de uso y el texto de error donde se usan. Extrae un ayudante solo para un bloque genuinamente grande y autocontenido (del tipo que enterraría el flujo principal si estuviera en línea, como obtener y decodificar una imagen), nunca para unas pocas líneas. Refleja los propios métodos privados del cliente de referencia (_convert_thinking_level_*,_convert_tool_choice) en lugar de inventar una capa junto a ellos; un shim que acepta tanto diccionarios como objetos SDK nunca es uno de ellos, porque los eventos están tipificados con los tipos propios del SDK. - Los clientes producen solo deltas; la clase base cierra los elementos.
transform_model_output_to_uni_event/transformModelOutputToUniEventconvierte un evento de red en unUniEventcuyoscontent_itemsson los elementos.deltaque ese evento lleva, en orden de red, y nunca un elemento.done._streaming_response_internal/_streamingResponseInternalconstruye la solicitud, abre el streaming y produce un evento por evento de red: un bucle simple, sin contabilidad de elementos, sin acumulador, sin análisis de argumentos, sin fusión de uso y sin stop sintetizado.usage_metadata/finish_reasonviajan en eventosstopdonde sea que la red los informe (Anthropicmessage_startymessage_delta, los fragmentos de finalización y uso de Chat Completions,response.completed), fusionados campo por campo por la base; el eventostopde un cliente nunca termina el streaming público. Un evento de red sin nada universal es el evento delta vacío: sin elementos, uso nulo y motivo de finalización nulo. La clase base entrega cada delta aStreamItems(stream_items.py/streamItems.ts), que lo envía a medida que llega y cierra cada elemento con su elemento.donecuando comienza el siguiente elemento o termina el streaming del cliente; la base luego emite el único evento stop o generaEmptyResponseError. Los eventos que rompen el protocolo generanStreamProtocolErroren todos los modos, y las pruebas e2e compartidas verifican el streaming de cada cliente conassert_stream_grammar/assertStreamGrammar. - Los deltas de un elemento son contiguos y comparten un
fidelity.item_id. La salida del modelo es serial, por lo que un elemento se transmite a la vez y los eventos de fin de elemento del proveedor (content_block_stop,response.output_item.done,step.stop) se enumeran como no operaciones: una puerta de enlace que cierra sus elementos tarde o fuera de orden no cambia nada. Pasa el ID propio del proveedor, convertido a cadena, comofidelity.item_id: elindexde Anthropic block, elitem_iddel evento Responses oitem.id or item.call_id(None/undefineddonde una puerta de enlace no envía ninguno), elindexdel paso de Interactions, el campo de red de Chat Completions (reasoning_content,reasoning,content,tool_calls), el tipo de parte de generateContent (function_call,thought,inline_thinking,inline_data,text); los vectores de incrustación no llevan ninguno.StreamItemslo elimina, por lo que nunca llega a consumidores, trazas o historiales. StreamItemsdecide dónde termina un elemento; los clientes nunca lo hacen. Un delta pertenece al siguiente elemento cuando lleva otroitem_id, es de otro tipo (un paso de pensamiento de Interactions que va texto, imagen, texto son tres elementos bajo un índice), o comienza un elemento por sí mismo: untool_call.deltaque lleva un nombre (cada proveedor lo envía una vez, en el primer delta de la llamada, por lo que las llamadas de Chat Completions no necesitan IDs propios), una imagen (inline_thinking, oinline_datacon un tipo MIMEimage/*, mientras que el audio se transmite en fragmentos de un elemento), un vector de incrustación. De lo contrario, continúa el elemento que se está transmitiendo ahora: un delta sin un ID lo hace, y también los argumentos de una llamada, sea cual sea el ID que una puerta de enlace les ponga. La fidelidad enviada sola bajo el ID del elemento es de ese elemento, sea cual sea el tipo que lo lleve (unthought_signaturede Interactions después de un pensamiento de imagen). La tabla por tipo en ese archivo (el campo creciente, los campos de encabezado del primer delta, cómo se unen los fragmentos) es el único código por tipo en el streaming: el elemento hecho es el primer delta del elemento con el campo creciente reemplazado por la unión de sus fragmentos; los campos de encabezado se toman del primer delta; los encabezados posteriores se ignoran.
Etapa 4 — Ejecutar las pruebas de integración compartidas
src_py/tests/test_client.pyysrc_ts/tests/client.test.tsson el único lugar para las pruebas de clientes. Una prueba añadida allí se ejecuta contra todos los clientes; no hay pruebas separadas por cliente.AVAILABLE_MODELSentest_client.pyenumera cada combinación (modelo, tipo de cliente) que se ha probado con una clave activa; el resto se salta con pytest.skip.AVAILABLE_MODELSse actualiza aquí, no en el mapa de directorios.- La prueba principal ejecuta una solicitud de streaming con pensamiento y dos llamadas a herramienta, envía un resultado de herramienta de vuelta para un segundo turno y verifica que la respuesta se lea como uni_items.
assert_stream_grammar/assertStreamGrammarverifica cada streaming: que los elementos de contenido se abran y cierren, que los elementos.doneaparezcan solo una vez, que el eventostopllegue exactamente una vez y queusage_metadataesté completo. - Las pruebas de credenciales (
test_env_credentials.py) se ejecutan sin conexión en cada combinación de variable de entorno y construyen cada cliente bajo un entorno controlado para verificar que la instancia del SDK contenga la clave que esperamos;env-credentials.test.tses su contraparte. - La prueba de inserción en
tests/stream_items_test.py(test_stream_items) verifica internamente la división de deltas y la finalización de elementos.tests/test_vector_input.pyytests/test_image_input.pyverifican los extremos de entrada.tests/auto_client_test.pyprueba la tabla de tipos de cliente y sus enrutamientos. Los archivos de prueba de TypeScript reflejan la misma cobertura.
Etapa 5 — Cambiar las páginas de la interfaz de usuario (playground, tracer, sitio web)
src_py/mmsp/integration/ysrc_ts/src/integration/tienen la lógica para el playground interactivo, las páginas del tracer y el servidor proxy de MMSP que atiende al sitio Astro ensite/.- Cuando se añada un modelo, la UI debe mostrarlo en el selector de modelos del playground, representar correctamente nuevos tipos de contenido (elementos de pensamiento, nuevas formas de datos en línea, diferentes esquemas de herramientas) y la página del tracer debe poder ingerir y mostrar los eventos del nuevo modelo. Los cambios en el lado Python (por ejemplo, nuevos formatos de serialización para la página del tracer) deben coincidir con los cambios en el lado TypeScript.
- Antes de hacer cualquier cambio en la UI, ejecuta el escáner
kill-ai-slopdesde.agents/skills/mmsp-dev/kill-ai-slop/sobre los archivos que estás a punto de cambiar. El escáner señala patrones de UI AI-slop (sombras de caja excesivas, bordes redondeados sospechosos, degradados donde no se necesitan, desenfoques decorativos, escalas de grises, animaciones de salto, etc.). Corrige lo que señale antes de continuar. Si el escáner no existe o falla, detente y pregunta. - Al enviar commits de cambios en la UI, ejecuta el escáner contra los archivos modificados de nuevo. Los cambios en la UI que introduzcan AI-slop son rechazados; este es un workflow libre de slop.
Etapa 6 — Documentación de cambios (changelog)
- Cada entrada de changelog sigue el formato en
changelog/README.md: bloque de metadatos (fecha, tipo, ámbito, etc.), luego el cuerpo en español e inglés (el repositorio es bilingüe e inglés-chino en la práctica, así que produce siempre tanto<name>.mdcomo<name>.zh.md). - Los archivos van en
changelog/unreleased/hasta que se etiqueta un lanzamiento, momento en el que la carpeta completa se renombra a la versión. CHANGELOG.mden la raíz debe actualizarse con una línea que enlace a la carpeta del changelog del lanzamiento.- El propio resumen del lanzamiento (
changelog/<version>/README.md) enumera cada entrada, agrupada por tipo (añadido, cambiado, corregido, eliminado).
Reglas generales
- Nunca uses memoria del modelo en lugar de documentación oficial. Si surge una discrepancia, prevalece la documentación oficial.
- Las capturas de API tienen prioridad sobre la documentación. Si la API se comporta de manera diferente a los documentos, implementa el comportamiento real de la API.
- Ejecuta las pruebas e2e completas (
AVAILABLE_MODELScompleto o todo lo que se pueda probar) antes de marcar el trabajo como completo. - Pareamiento Python/TypeScript: cada cambio en la lógica de Python debe tener una contraparte exacta en TypeScript. Si una característica es solo para un lado, documéntalo explícitamente.
- No hay cambios de UI AI-slop. Ejecuta el escáner. No lo omitas.
- Reproducción de reproducción, no de campo. Mantén
fidelitymínima. Solo se reproducen los campos que la API exige absolutamente. - La seguridad de las credenciales es estricta. Cada cliente lee solo su clave designada, o ninguna, y nunca envía una clave a un endpoint equivocado.
- Mantenlo legible de arriba a abajo. Sin saltos de definición, sin capas de shim, sin código por tipo en el streaming del cliente.
- El flujo de trabajo es vinculante. No reorganices etapas, no las omitas y nunca rellenes un "detente y pregunta" con tu propia suposición.
Este flujo de trabajo está diseñado para entornos de desarrollo de agentes de IA (Claude Code, Cursor, Codex) y aplica un control estricto sobre la calidad de la integración de modelos, la seguridad y la fidelidad de la implementación.