Fluxo de Trabalho de Desenvolvimento do MMSP
Adicionar ou atualizar o suporte a um modelo segue os estágios abaixo, em ordem. Onde um estágio disser pare e pergunte, pare e pergunte ao usuário; não continue até que o problema seja resolvido e nunca preencha a lacuna você mesmo.
Mapa de diretórios
llmsdk_docs/<versão_do_modelo>/ Snapshot da documentação oficial, uma pasta por geração de modelo (README.md + docs/)
api_captures/<protocolo>/ Capturas de API brutas ignoradas pelo Git: payloads de requisição + eventos de stream
src_py/mmsp/<tipo_cliente>/ Cliente Python, uma pasta por tipo de cliente: <fornecedor>_official/ ou <protocolo>/
src_py/mmsp/auto_client.py Cria o cliente que um tipo_cliente nomeia; a família de um id de modelo nomeia seu cliente oficial
src_ts/src/<tipo_cliente>/ Cliente TypeScript, espelha a pasta Python
src_ts/src/autoClient.ts Roteamento TypeScript, espelha auto_client.py
src_py/tests/test_client.py Testes e2e parametrizados (protegidos por variável de ambiente AVAILABLE_MODELS)
src_ts/tests/client.test.ts O mesmo para TypeScript
changelog/unreleased/ Onde as alterações não lançadas vão; renomeado para a versão no lançamento
changelog/<versão>/ Resumo do lançamento (README.md) mais um arquivo de detalhe por entrada
changelog/README.md O formato da entrada: bloco de metadados, regras do corpo, pareamento bilíngue
CHANGELOG.md Uma linha breve por lançamento ligando a changelog/
src_py/mmsp/integration/ O playground, as páginas do tracer e o servidor MMSP, espelhado em src_ts/src/integration/
site/ O site Astro em mmsp.penguin.ooo
.agents/skills/mmsp-dev/kill-ai-slop/ Guia de AI-slop e scanner embutidos para o trabalho de UI abaixo
<nome>.zh.md Contraparte em Chinês, obrigatório para todo arquivo em changelog/
Estágio 1 — Sincronizar documentação oficial em llmsdk_docs/
- As fontes devem ser o site de documentação oficial do fornecedor do modelo (ex.: docs.anthropic.com, platform.openai.com, ai.google.dev). Nunca use mirrors de terceiros, postagens de blog ou memória de modelo.
- Salve o snapshot em
llmsdk_docs/<versão_do_modelo>/seguindo as convenções de pasta existentes e liste a pasta emllmsdk_docs/README.md. Executar este fluxo de trabalho é a solicitação explícita que a regra do repositório contra editarllmsdk_docs/pede. - Quando a documentação buscada diferir de um snapshot existente, a nova documentação oficial vence: atualize os arquivos antigos no lugar.
- O snapshot deve ser completo o suficiente para implementar a partir dele: esquemas de requisição/resposta, sequência de eventos de stream, saída de pensamento, chamada de ferramenta, campos de uso e respostas de erro.
- Pare e pergunte se a URL oficial for incerta ou se uma página não puder ser buscada. O usuário pode colar o conteúdo manualmente.
Estágio 2 — Capturar uma troca de API ao vivo em api_captures/
- Portão: a variável de ambiente da chave de API do provedor deve estar definida e utilizável. Use as mesmas variáveis de ambiente e URLs base que
src_py/tests/test_client.py(proteçãoAVAILABLE_MODELSe_create_client). Pare e pergunte ao usuário para fornecer a chave se ela estiver faltando; o fluxo de trabalho não deve continuar sem ela. - Usando o SDK oficial do provedor, ou HTTP puro exatamente como documentado, execute uma requisição de chamada de ferramenta em stream com pensamento habilitado, então envie o resultado da ferramenta de volta para que a captura também mostre como as rodadas do assistente são reenviadas.
- Salve a troca completa sem modificações em
api_captures/<protocolo>/(ignorada pelo git), ex.:round1.request.jsonmaisround1.stream.jsonlcom cada evento de stream bruto em ordem. Nunca salve credenciais. - Quando algo na troca não serializar para JSON (payloads binários, objetos SDK contendo valores não JSON), salve sua forma
str()e analise a partir disso. Mantenha o contêiner analisável: envolva o texto em um objeto JSON para que o.jsonlpermaneça um valor JSON por linha ({"unserializable_str": "<str(evento)>"}, mantendo o evento em ordem de stream), ou coloque toda a troca em um arquivo.txtirmão quando nada sobre ela serializar. Uma capturastr()ainda é o registro autoritativo do que a API retornou. Nunca descarte o evento, edite-o manualmente para JSON válido ou recorra à documentação porque a serialização falhou. - Pare e pergunte em qualquer erro de API (chave inválida, cota insuficiente, limite de taxa). Não simule a resposta ou continue apenas com a documentação.
- A captura é a referência primária de implementação e supera a documentação: onde discordarem, implemente o que a API realmente retornou.
- Em seguida, investigue a repetição, porque a captura só mostra o que a API envia, não o que ela precisa de volta. Reenvie a rodada do assistente repetidamente, cada vez com um campo candidato removido — o id do item de raciocínio, seu conteúdo, todo o item de raciocínio, cada campo que você foi tentado a manter — e registre quais remoções a API ainda aceita com o comportamento do modelo intacto. Esse resultado, não a forma da resposta, decide o que o Estágio 3 armazena. Salve os resultados da investigação ao lado da captura.
Estágio 3 — Implementar os clientes Python e TypeScript
- Uma pasta por tipo de cliente, nomeada após ele: um cliente oficial por fornecedor (
openai_official/,anthropic_official/,google_official/,zai_official/,moonshot_official/,deepseek_official/,minimax_official/) fala a própria API do fornecedor, conhece todas as gerações dos modelos do fornecedor e lê a chave do fornecedor do ambiente; um cliente compatível por protocolo de rede (openai_responses/,openai_chat/,openai_chat_vllm_adapter/,openai_embedding/,ant_messages/,google_genai/,mmsp/) fala esse protocolo para qualquer endpoint. A classe é nomeada após a pasta (OpenAIOfficialClient,OpenaiChatClient), e o tipo de cliente é o nome da pasta com hífens (openai-official,openai-chat).- Uma nova geração de modelos de um fornecedor vai para o cliente oficial do fornecedor. Diferencie o novo protocolo (captura + documentação) do que o cliente envia hoje; onde uma geração difere, mesmo que por um único nome de chave, diferencie as gerações dentro do cliente por versão explícita (
"4-6" in self._model,"gpt-6" in self._model), nunca por uma substring simples como"claude" in model, e mantenha as gerações mais antigas funcionando. Nunca descarte uma geração a menos que o usuário instrua explicitamente. - Um novo fornecedor recebe uma nova pasta de cliente oficial, um novo tipo de cliente
<fornecedor>-officialemauto_client.py/autoClient.tse uma nova linha na tabela de famílias de modelos lá: a família é o prefixo com o qual todo id dos modelos do fornecedor começa (glm-,kimi-), correspondido sem distinção de maiúsculas/minúsculas. - Um novo protocolo de rede que vários fornecedores atendem recebe uma pasta de cliente compatível e tipo de cliente compatível; um cliente compatível não carrega peculiaridades de nenhum fornecedor.
- Uma nova geração de modelos de um fornecedor vai para o cliente oficial do fornecedor. Diferencie o novo protocolo (captura + documentação) do que o cliente envia hoje; onde uma geração difere, mesmo que por um único nome de chave, diferencie as gerações dentro do cliente por versão explícita (
auto_client.py/autoClient.tscriam o cliente que umclient_typenomeia, a partir da única tabela de tipos de cliente. Sem umclient_type, a família com a qual um id de modelo começa nomeia seu cliente oficial; um id de uma família desconhecida levanta e pede por uma. O roteamento não tem outra regra: sem correspondência de substring em ids de modelo, sem correspondência de versão fora dos clientes oficiais.- Uma credencial vai apenas onde foi fornecida. Este é o modelo de segurança, e todo construtor de cliente o segue. Uma chave passada como
api_keyé usada como está. Um cliente que lê a chave genérica do ambiente (OPENAI_API_KEYparaopenai_officiale os clientes compatíveis com o protocolo OpenAI,ANTHROPIC_API_KEYparaanthropic_officialeant_messages,GEMINI_API_KEYparagoogle_genai) a lê apenas quando o endpoint também vem do ambiente (OPENAI_BASE_URL/ANTHROPIC_BASE_URL/GEMINI_BASE_URL, ou o padrão do próprio provedor): dado umbase_urle nenhumapi_key, ele levanta na construção em vez de enviar essa chave para outro endpoint. Isso éresolve_credentials/resolveCredentialsemutils; chame-o, não o reimplemente. Qualquer outro cliente oficial (deepseek_official,zai_official,moonshot_official,minimax_official,google_official) lê sua própria variável (DEEPSEEK_API_KEY,ZAI_API_KEY,MOONSHOT_API_KEY,MINIMAX_API_KEY,GEMINI_API_KEY) qualquer que seja a URL base fornecida, e nunca a genérica; sem uma delas, levanta<VAR> is required for <Client>.. Entregue toda credencial ao SDK do fornecedor explicitamente, incluindo "nenhuma" (authToken: nullem TypeScript, umauth_tokenlimpo em Python), porque os SDKs preenchem o que não recebem de suas próprias variáveis de ambiente. Os testes offline emtests/env-credentials.test.ts/tests/test_env_credentials.pyconstroem cada cliente sob um ambiente controlado e leem a credencial que a instância do SDK mantém; adicione um novo cliente a eles. - A repetição deve ser mínima, não exaustiva. Uma rodada do assistente repetida tem que ser aceita pela API com o comportamento do modelo intacto; ela não tem que reproduzir o campo do item de rede campo por campo.
fidelityé um canal de exceção, não um espelho do payload: um campo ganha um lugar lá apenas quando a investigação do Estágio 2 mostra que a requisição falha ou o modelo degrada sem ele. Ids gerados pelo provedor que a API regenera ou ignora — um id de item de raciocínio, um id de item de saída — ficam de fora. Campos que a API exige vão:phasedo GPT-5.5,signaturedo pensamento do Claude, o nome exato do campo de raciocínio que um upstream estrito requer. - Nunca copie para
fidelityo que um campo universal já carrega — o texto de pensamento, o nome ou argumentos analisados de uma chamada de ferramenta. Reconstrua o item de rede a partir dos campos universais. tool_call_idnão é opcional: sempre capture o id de chamada do provedor e repita-o. Onde o formato de rede carrega tanto um id de item quanto um id de chamada, o id de chamada é aquele que correlaciona um resultado à sua chamada.- Siga o cliente de referência; não redesenhe.
openai_official/é a forma para protocolos estilo Responses,openai_chat/para Chat Completions: mesma ordem de métodos, mesmo fluxo de controle, mesmos nomes. Um novo cliente deve ser lido como uma diferença contra sua referência, porque é assim que será revisado. - Mantenha legível de cima a baixo. Um leitor deve seguir uma requisição ou resposta diretamente através do arquivo sem pular entre definições, então a inlining é o padrão: leia os atributos do SDK diretamente (
model_output.delta,model_output.item.call_id) e mantenha acesso a campos, aritmética de uso e texto de erro onde são usados. Extraia um auxiliar apenas para um bloco genuinamente grande e autocontido — o tipo que enterraria o fluxo principal se fosse inlinado, como buscar e decodificar uma imagem — nunca para algumas linhas. Espelhe os métodos privados do cliente de referência (_convert_thinking_level_*,_convert_tool_choice) em vez de inventar uma camada ao lado deles; uma adaptação que aceita tanto dicts quanto objetos SDK nunca é uma delas, porque os eventos são tipados com os próprios tipos do SDK. - Os clientes geram apenas deltas; a classe base fecha os itens.
transform_model_output_to_uni_event/transformModelOutputToUniEventtransforma um evento de rede em umUniEventcujoscontent_itemssão os itens.deltaque aquele evento carrega, em ordem de rede, e nunca um item.done._streaming_response_internal/_streamingResponseInternalconstrói a requisição, abre o stream e gera um evento por evento de rede: um loop simples, sem contabilidade de itens, sem acumulador, sem análise de argumentos, sem mesclagem de uso e sem síntese de parada.usage_metadata/finish_reasonandam em eventosstoponde quer que o wire os reporte —message_startemessage_deltado Anthropic, os chunks de finalização e uso do Chat Completions,response.completed— mesclados campo por campo pela base; um eventostopde um cliente nunca termina o stream público. Um evento de rede sem nada universal é o evento delta vazio: sem itens, uso e motivo de finalização nulos. A classe base entrega cada delta paraStreamItems(stream_items.py/streamItems.ts), que o envia conforme chega e fecha cada item com seu item.donequando o próximo item começa ou o stream do cliente termina; a base então emite o único evento stop ou levantaEmptyResponseError. Eventos que quebram o protocolo levantamStreamProtocolErrorem todos os modos, e os testes e2e compartilhados verificam o stream de cada cliente comassert_stream_grammar/assertStreamGrammar. - Os deltas de um item são contíguos e compartilham um
fidelity.item_id. A saída do modelo é serial, então um item transmite por vez e os eventos de fim de item do provedor (content_block_stop,response.output_item.done,step.stop) são listados como no-ops: um gateway que fecha seus itens tarde ou fora de ordem não muda nada. Passe o próprio id do provedor, como string, comofidelity.item_id: oindexdo bloco do Anthropic, oitem_iddo evento Responses ouitem.id ou item.call_id(None/undefinedonde um gateway não envia nenhum), oindexdo passo das Interações, o campo de rede do Chat Completions (reasoning_content,reasoning,content,tool_calls), o tipo de parte do generateContent (function_call,thought,inline_thinking,inline_data,text); vetores de embedding não carregam nenhum.StreamItemso remove, então ele nunca alcança consumidores, rastros ou históricos. StreamItemsdecide onde um item termina; os clientes nunca fazem isso. Um delta pertence ao próximo item quando ele carrega outroitem_id, é de outro tipo (um passo de pensamento das Interações indo texto, imagem, texto são três itens sob um índice) ou começa um item por si só: umtool_call.deltacarregando um nome (todo provedor o envia uma vez, no primeiro delta da chamada, então chamadas do Chat Completions não precisam de ids próprios), uma imagem (inline_thinkingouinline_datacom um tipo MIMEimage/*, enquanto o áudio transmite em chunks de um item), um vetor de embedding. Caso contrário, ele continua o item que está transmitindo agora: um delta sem id faz isso, e os argumentos de uma chamada também, qualquer que seja o id que um gateway coloque neles. Fidelidade enviada sozinha sob o id do item é daquele item, qualquer que seja o tipo que a carregue (umthought_signaturedas Interações após um pensamento de imagem). A tabela por tipo naquele arquivo — o campo crescente, os campos de cabeçalho do primeiro delta, como os chunks se juntam — é o único código por tipo no stream: o item feito é o primeiro delta do item com o campo crescente substituído pela junção de