Pular para o conteúdo

ADR-0025 SDK de IA

SDK de IA padrão da empresa — TanStack AI, fixado em versões exatas pré-1.0 e agnóstico do fornecedor; cada produto escolhe o seu fornecedor de modelos num ADR próprio.

Decisão
TanStack AI, fixado; fornecedor por produtoConstruir a app
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

Os produtos vão acrescentar funcionalidades de IA: um assistente de conversa, resumos, extração de dados de documentos, ações que o modelo executa através de tools. As decisões à volta estão tomadas, e cada uma impõe uma regra a esta:

  • Framework: TanStack Start com server functions e SSR com streaming (ADR-0002); server routes em /api/* para tudo o que é consumido como API HTTP (ADR-0036)
  • Validação: Zod 4 em todas as fronteiras (ADR-0005). Os argumentos de uma tool e a resposta estruturada de um modelo também são fronteiras — o modelo é um chamador que não controlamos
  • Autenticação: sessões do Better Auth, verificadas no servidor (ADR-0009)
  • Rate limiting: um só limitador no middleware global de pedidos do Start, indexado pelo ID do utilizador (ADR-0010). O seu limite inicial de escrita é de 300 pedidos por utilizador por minuto — dimensionado para formulários, não para chamadas que custam dinheiro
  • Estado do cliente: dados do servidor no Query, estado de UI no URL e no React (ADR-0012)
  • Observabilidade: OpenTelemetry, filtrado na app antes de exportar; a telemetria já transporta IDs de utilizadores, pelo que o que registamos é uma decisão de privacidade (ADR-0015)
  • Testes: sem chamadas reais a terceiros (ADR-0014) — um modelo é uma delas
  • Renderização: o texto do modelo é Markdown (ADR-0021), com realce de sintaxe no código (ADR-0022)

O que está em aberto: o SDK que fala com os modelos e as regras para o usar. O fornecedor de modelos não está em aberto aqui. Qual o fornecedor, o modelo e a região é uma decisão por produto, porque depende dos dados que o produto envia e dos termos contratuais que consegue obter. Vai para o ADR próprio do produto (Como decidimos, regra 5), e a secção Decisão diz o que esse ADR tem de cobrir.

Três factos moldam a resposta:

  • Uma chamada a um modelo é um pedido pago a um terceiro. Custa dinheiro por token, pode durar um minuto e tudo o que pomos no prompt sai da nossa infraestrutura. O rate limiting (ADR-0010) e a proteção de dados (RGPD; somos uma empresa da UE) aplicam-se desde a primeira chamada
  • A saída do modelo é input não confiável. Tudo o que o modelo lê — uma mensagem de um utilizador, um documento, o resultado de uma tool, uma página web — pode influenciar o que escreve e que tools chama. O OWASP Top 10 for LLM Applications (2025) lista prompt injection, improper output handling, excessive agency e unbounded consumption. Tratamos o texto do modelo, os argumentos das tools e as respostas estruturadas como o payload de um webhook (ADR-0005)
  • O SDK é pré-1.0 e evolui depressa. O @tanstack/ai está na 0.70.0. A questão é como o adotar com segurança, não se está terminado

Requisitos:

  • Chat com streaming em React, com estados de paragem e de erro, que se encaixe nas server functions do Start e nas rotas da API
  • Agnóstico do fornecedor: trocar ou acrescentar um fornecedor altera um adapter, num único sítio do servidor
  • Tools tipadas cujos schemas são Zod, e saída estruturada validada
  • Corre em Bun no nosso contentor, sem gateway alojado nem plataforma no caminho (ADR-0003)
  • Spans OpenTelemetry e métricas de tokens, com o conteúdo dos prompts desligado por predefinição
  • Um modelo falso com respostas definidas no teste, para que os testes não precisem de rede nem de chave

O @tanstack/ai contém o núcleo: chat(), tools, saída estruturada, middleware e helpers de servidor. O @tanstack/ai-react contém os hooks. Cada fornecedor é um pacote adapter: OpenAI (@tanstack/ai-openai 0.30.1), Anthropic (@tanstack/ai-anthropic 0.22.1), Gemini, Vertex, Bedrock, Mistral, Ollama, OpenRouter, Groq, Grok e mais — além de openaiCompatible para qualquer endpoint compatível com a OpenAI.

Prós

  • Mesmos mantenedores e mesma organização do Router, do Query, do Start e do Form; MIT. @tanstack/ai 0.70.0 (2026-10-19) e @tanstack/ai-react 0.31.0. Cerca de 594 mil downloads por semana (2026-10-12 a 2026-10-18)
  • O chat() devolve um async iterable de eventos; o toServerSentEventsResponse() transforma-o numa Response SSE. No React, o useChat() lê-a através de fetchServerSentEvents() para uma rota HTTP, ou de um fetcher para uma server function do Start. O stop() aborta o pedido, e um AbortController passado ao chat() pára também a chamada ao modelo
  • As tools definem-se uma só vez com toolDefinition() e um schema Zod, e implementam-se depois com .server() ou .client(). O SDK valida os argumentos de cada chamada antes de a tool correr; uma falha volta ao modelo como erro de tool. O needsApproval pausa uma tool até o utilizador aprovar
  • Saída estruturada através de outputSchema (Zod 4.2+, ArkType ou Valibot)
  • O context construído no servidor é passado às tools e ao middleware, e nunca é enviado ao modelo — um ponto pronto para o utilizador autenticado e para as dependências injetadas
  • Um middleware OpenTelemetry (@tanstack/ai/middlewares/otel) emite um span por chamada a chat(), por volta do modelo e por chamada a uma tool, com atributos GenAI e histogramas de tokens e de duração. A captura de prompts e de respostas está desligada, a menos que se use captureContent: true — lemos a predefinição no código-fonte da 0.70.0
  • O fakeText() de @tanstack/ai/testing é um adapter com guião: respostas em fila, chamadas a tools, erros, uso de tokens estimado
  • Quando a chamada seguinte vai para outro fornecedor, o SDK reescreve o histórico guardado (assinaturas de raciocínio, IDs de chamadas a tools) para o adaptar — o que torna realista um fornecedor de reserva
  • Um painel de devtools para mensagens e chamadas a tools (@tanstack/react-ai-devtools). Biblioteca pura: sem camada de plataforma, funciona em qualquer host que devolva uma Response web

Contras

  • Pré-1.0 e em mudança constante. 102 versões em cerca de 10 meses e meio; 25 novas linhas minor entre a 0.43 (2026-08-04) e a 0.70 (2026-10-19). Já saíram alterações incompatíveis dentro da 0.x — o formato do protocolo HTTP, as opções de amostragem movidas para modelOptions, as props do TextPart, os pacotes do MCP SDK — cada uma com um guia de migração
  • Cada adapter e o @tanstack/ai-react têm o @tanstack/ai ^0.70.0 como peer dependency, o que numa versão 0.x significa apenas a mesma minor
  • Mais jovem e mais pequeno do que o AI SDK: cerca de 594 mil downloads por semana contra cerca de 18,4 M, e menos respostas online. A documentação no GitHub acompanha o main
  • A documentação mostra padrões que proibimos por predefinição: uma chave guardada no browser (BYOK), um fornecedor escolhido pelo cliente (forwardedProps) e uma server function cujo validador é apenas um cast de TypeScript
  • Os SDKs dos fornecedores vêm como dependências dos adapters, com intervalos que não controlamos
  • O @tanstack/ai-react 0.31.0 tem o @tanstack/markdown ^0.0.13 como dependência obrigatória (não opcional), usada pelo seu TextPart pré-construído. O renderizador de Markdown dele é uma segunda cópia, mais antiga, da biblioteca por trás do ADR-0021

ai 7.0.148 (Apache-2.0, publicado em 2026-10-21), @ai-sdk/react 4.0.152 e pacotes de fornecedores como @ai-sdk/openai 4.0.99.

Prós

  • Uma major estável e a maior base de utilizadores: cerca de 18,4 M de downloads por semana para o ai e 5,7 M para o @ai-sdk/react
  • Dezenas de pacotes de fornecedores oficiais; useChat; tool calling; um modelo simulado em ai/test (MockLanguageModelV4); OpenTelemetry através do @ai-sdk/otel 1.0.148
  • Uma abstração de agente pronta, o ToolLoopAgent

Contras

  • Depois de registada a sua integração OpenTelemetry, as entradas e as saídas são registadas por predefinição; a privacidade exige recordInputs: false e recordOutputs: false. A opção segura é a exceção — o oposto da nossa
  • Mantido pela Vercel, a empresa de plataforma que recusámos como destino de deploy (ADR-0003) e que agora é dona do Better Auth (ADR-0009); a sua documentação lista o Vercel AI Gateway em primeiro lugar entre os fornecedores. A licença e correr no nosso próprio contentor mantêm a saída em aberto, e nada no SDK exige a plataforma
  • Um segundo ciclo de lançamentos ao lado do da TanStack, e 236 versões do ai no npm nos últimos 90 dias: semver estável, mas ruído constante de dependências

openai 7.39.0 (Apache-2.0) e @anthropic-ai/sdk 0.137.0 (MIT).

Prós: nada entre nós e o fornecedor; acesso imediato às novidades do fornecedor; o menor conjunto de dependências; cerca de 25,3 M e 28,3 M de downloads por semana. Contras: cada um tem as suas próprias formas de mensagens, tools e streams, que se espalhariam pelas nossas funcionalidades — trocar de fornecedor passa a ser uma reescrita. Sem hook de React, sem ciclo de tools, sem modelo falso entre fornecedores: cada produto reconstruiria o transporte SSE, o abort, o uso, a validação de tools e os spans OTel. O @anthropic-ai/sdk é, ele próprio, pré-1.0. (Os adapters da opção A envolvem estes mesmos SDKs.)

langchain 1.5.18, @langchain/core 1.2.20 e @langchain/langgraph 1.5.1, todos MIT.

Prós: 1.x estável; integrações vastas; o LangGraph para workflows com estado e vários passos; cerca de 1,7 M de downloads por semana para o langchain. Contras: uma framework de agentes em camadas — um harness create_agent, a orquestração do LangGraph, o tracing do LangSmith — quando precisamos de um SDK fino. As suas abstrações ficariam entre o nosso código e o modelo, e o langchain depende tanto do LangGraph como do cliente langsmith.

@mastra/core 1.78.0 e @mastra/react 1.11.0, Apache-2.0 exceto o código em diretórios ee/, que tem licença própria.

Prós: 1.x estável; agentes, workflows e memória numa só framework TypeScript; cerca de 1,3 M de downloads por semana para o @mastra/core. Contras: uma framework que quer ser dona do runtime dos agentes — o @mastra/core tem 30 dependências, entre as quais hono e ws. Adotaríamos o seu runtime, não um SDK, e verificaríamos a licença ee/ funcionalidade a funcionalidade.

  • Server function do Start: createServerFn({ method: "POST" }) a devolver toServerSentEventsResponse(...), chamada a partir de useChat({ fetcher }). Para a nossa própria UI, como diz a convenção do ADR-0036. O mesmo caminho de qualquer escrita: limitador, verificação CSRF, validação
  • Server route em /api/*, lida com fetchServerSentEvents(): para consumidores fora da nossa app React — mobile, parceiros, scripts. O limitador cobre-a; a verificação CSRF das server functions não, pelo que não pode aceitar o cookie de sessão

O TanStack AI é o SDK de IA padrão da empresa para todas as novas apps: o @tanstack/ai e um adapter de fornecedor no servidor, o @tanstack/ai-react no browser. É aceite enquanto pré-1.0, fixado em versões exatas. Este ADR escolhe o SDK e as regras para o usar — não um fornecedor de modelos. Cada produto escolhe o seu fornecedor num ADR próprio.

Em concreto:

  • Pré-1.0, aceite e fixado — como o ADR-0008 fez com o Drizzle v1. Sem ^ nem ~ em nenhum pacote @tanstack/ai* nem no @tanstack/react-ai-devtools. Hoje: @tanstack/ai 0.70.0, @tanstack/ai-react 0.31.0 e os adapters que o ADR de um produto indicar (por exemplo @tanstack/ai-openai 0.30.1 ou @tanstack/ai-anthropic 0.22.1). Avançam em conjunto, num só grupo do Renovate (ADR-0016), porque cada adapter e o @tanstack/ai-react têm como peer dependency a mesma minor do @tanstack/ai. Os pacotes @tanstack/ai-* transitivos (ai-client, ai-utils, ai-event-client) ficam presos pelo lockfile, que o CI instala congelado. As atualizações são deliberadas: ler as notas de lançamento e os guias de migração, correr os testes sobre o modelo falso e fazer deploy do servidor e do bundle do browser em conjunto. Reavaliar quando a 1.0 sair, num novo ADR
  • As chamadas a modelos correm só no servidor. O chat() e os adapters são importados apenas por código de servidor; o browser importa o @tanstack/ai-react e nunca um pacote adapter. A chave do fornecedor é uma variável de ambiente do servidor, validada no arranque com Zod (ADR-0005) e nunca exposta ao cliente (sem prefixo VITE_). Sem BYOK (@tanstack/ai-react/byok): uma chave guardada no browser muda quem é o cliente do fornecedor, pelo que um produto que a queira escreve o seu próprio ADR
  • Uma só fronteira de adapter. Exatamente um módulo, src/server/ai/adapter.ts, importa um pacote de fornecedor e lê a sua chave. Devolve o tipo de adapter do SDK (AnyTextAdapter) construído a partir de configuração validada. O código das funcionalidades, as server functions e as rotas recebem o adapter como parâmetro e nunca importam um pacote de fornecedor; os testes passam o fakeText() no seu lugar, e um produto que acrescente um fornecedor de reserva acrescenta aqui uma segunda factory e mais nada. O fornecedor e o modelo vêm da configuração, nunca do pedido — o exemplo de troca em runtime do SDK lê forwardedProps.provider do corpo do pedido, o que deixaria um cliente escolher um fornecedor que nenhum ADR aprovou. Um produto que ofereça escolha de modelo mapeia um enum Zod para uma lista de permitidos no servidor
  • Handlers finos, regras em módulos simples. Uma server function ou um handler de rota da API apenas faz parse, autoriza, limita e chama uma função da funcionalidade. Um módulo, src/server/ai/chat.ts, é o único que chama o chat(): recebe como parâmetros o adapter, as mensagens validadas, o utilizador e um sinal de abort, e liga o middleware (OpenTelemetry, uso, orçamento de tools, mapeamento de erros). Os prompts e o conjunto de tools vivem em módulos simples que recebem as suas dependências — adapter, relógio, acesso a dados através do context — como parâmetros (ADR-0014). O prompt de sistema é definido no servidor com systemPrompts; o texto do utilizador nunca se torna uma mensagem de sistema
  • Server function para a nossa UI, uma server route para clientes externos (ADR-0036). A nossa UI chama uma server function POST — createServerFn({ method: "POST" }).validator(schema) — que devolve toServerSentEventsResponse(chat(...)), lida por useChat({ fetcher }). Nunca GET: o limitador ignora leituras (ADR-0012). Uma server route em /api/v1/ai/* serve apenas consumidores que não são browsers. Faz parse com chatParamsFromRequestBody(await request.json()) dentro de try/catch, respondendo ela própria 400, e rejeita qualquer corpo que não seja application/json. Autentica-se através do apiCaller, com um token e nunca com o cookie de sessão, pelo que não precisa de verificação CSRF — a do ADR-0013 cobre apenas server functions. (Alterado em 2026-11-13: este ADR usou primeiro uma rota Hono com o csrf() do Hono; o ADR-0036 substituiu o Hono pelas server routes do Start.)
  • Primeiro autenticado, depois limitado por utilizador. Todo o ponto de entrada de IA identifica o caller através do Better Auth antes de tocar no SDK — a sessão para a server function, o token para a rota da API — e responde 401 caso contrário; não há endpoint de IA anónimo. O limite de escrita genérico do ADR-0010 é demasiado folgado para chamadas a modelos, pelo que as rotas de IA consomem também um âmbito ai mais estrito do mesmo limitador, indexado pelo ID do utilizador — a começar em 10 pedidos por utilizador por 60 s, ajustado por produto. Um pedido é uma execução inteira de chat(), que pode chamar o modelo várias vezes. Acima do limite: 429 com Retry-After, como no ADR-0010
  • Limitar o que um pedido pode custar. O limitador conta pedidos, não tokens, pelo que cada execução é também limitada:
    • limites Zod para o número de mensagens e o tamanho do texto na fronteira
    • um agentLoopStrategy explícito, como maxIterations(n) — a predefinição são cinco voltas ao modelo, e uma volta pode emitir várias chamadas a tools — mais um orçamento de chamadas a tools no middleware (onBeforeToolCall)
    • um teto de tokens de saída nos modelOptions nativos do fornecedor do adapter
    • um orçamento diário de tokens por utilizador, contado a partir de onUsage no mesmo Valkey (ADR-0010); o valor é definido no ADR do produto
    • um só AbortController passado ao chat() e ao toServerSentEventsResponse(), para que o Stop ou um separador fechado pare também a chamada ao modelo
    • um prazo de execução abaixo do timeout de escrita da frente: o respondingTimeouts.writeTimeout do Traefik “covers the time from the end of the request header read to the end of the response write” (cobre o tempo desde o fim da leitura dos cabeçalhos do pedido até ao fim da escrita da resposta), e o ADR-0017 começa-o em 60 s, pelo que um stream mais longo é cortado na frente. Abortar as execuções aos 50 s (valor inicial); um produto que precise de execuções mais longas altera o valor da frente de forma deliberada
  • Zod em todas as fronteiras que o SDK abre:
    • Pedido: o chatParamsFromRequest verifica apenas a forma do pedido AG-UI. Validamos com o nosso próprio schema Zod, descartamos as mensagens system e developer enviadas pelo cliente e nunca passamos ao chat() as tools do cliente — o servidor constrói a lista de tools. Os forwardedProps são controlados pelo cliente: pôr cada campo numa lista de permitidos antes de o usar
    • Tools: toolDefinition() com inputSchema e outputSchema Zod, limitados com .max() e enums. Não JSON Schema em bruto, que tipa args como unknown. O SDK valida cada chamada; o handler continua a autorizar
    • Saída estruturada: outputSchema com Zod 4.2+. O caminho sem streaming valida; no caminho com streaming (stream: true) o SDK deixa a validação para nós — fazer parse com o mesmo schema (parseWithStandardSchema) antes de usar o objeto
  • As tools correm no servidor, a menos que só toquem na UI. .server() para dados, segredos e efeitos secundários, obtendo as dependências e o utilizador autenticado de chat({ context }) — construído no servidor, nunca a partir do corpo do pedido. .client() só para efeitos de UI, como um toast ou uma navegação: corre no browser e o seu resultado volta do cliente, logo é input não confiável. Os handlers autorizam pelo utilizador da sessão, não pela conversa: o modelo não é uma autoridade, e uma aprovação é um passo de UX, porque as aprovações retomam a partir do histórico de mensagens que o browser devolve. O resultado de uma tool também chega ao browser em stream (TOOL_CALL_RESULT), pelo que só devolve o que o utilizador autenticado pode ver. Privilégio mínimo: o menor conjunto de tools por rota, só de leitura por predefinição; escritas, envios, remoções e despesas usam needsApproval: true e voltam a verificar a permissão no handler
  • Estado vem dos hooks, o texto passa pelo nosso Markdown. O browser importa o useChat e os restantes hooks apenas da entrada raiz do @tanstack/ai-react — nunca de @tanstack/ai-react/ui (o Chat, o ChatMessage e o TextPart pré-construídos), e nunca o TextPart nem qualquer outro renderizador de Markdown do SDK. O TextPart renderiza com a sua própria cópia do @tanstack/markdown, fixada de forma exata na linha 0.0.x, o que poria um segundo renderizador de Markdown, com configuração própria, ao lado do nosso (ver Consequências). Os hooks tratam do estado e do streaming; cada parte text é renderizada pelo componente Markdown partilhado do ADR-0021 (src/components/markdown/markdown.tsx, configurado em src/lib/markdown.ts). O chat não tem configuração própria: o allowHtml nunca é definido, as imagens remotas são descartadas a menos que a sua origem esteja na lista de permitidos do ADR-0021, e a extensão de streaming está sempre ligada
  • A saída do modelo é input não confiável. As partes de texto renderizam-se apenas pelo caminho seguro de Markdown do ADR-0021 — nunca HTML em bruto, nunca dangerouslySetInnerHTML — e os blocos de código pelo ADR-0022. As regras do ADR-0021 para ligações e imagens aplicam-se, porque um modelo pode ser levado a emitir uma ligação ou imagem cujo URL leva dados da conversa para fora. As respostas estruturadas passam por parse com Zod antes de serem usadas. O texto do modelo nunca se torna um URL, um caminho de ficheiro, SQL ou um argumento de shell sem validação
  • Sem dados pessoais em logs ou traces por predefinição.
    • Ligar o otelMiddleware({ tracer, meter }) de @tanstack/ai/middlewares/otel uma só vez, em chat.ts. O ADR-0015 já instala o @opentelemetry/api; o intervalo de peers do SDK é >=1.9.0
    • Deixar o captureContent em false, a sua predefinição: os spans passam então a transportar o modelo, as contagens de tokens, o motivo de fim e os nomes e resultados das tools — nunca prompts, respostas, nem argumentos e resultados de tools
    • Nunca definir debug: true fora do desenvolvimento (imprime chunks em bruto do fornecedor e chamadas a tools), e nunca escrever prompts, respostas, nem entrada e saída de tools nos logs do pino. Registar IDs de utilizadores, nunca emails
    • Um produto que tenha de capturar conteúdo para depuração ou avaliação di-lo no seu ADR de fornecedor, com uma função redact e uma retenção mais curta do que a dos traces no backend. Se o redact lançar uma exceção, o SDK regista [redaction_failed] em vez do conteúdo
    • Os erros também deixam escapar dados. O SDK põe a mensagem da exceção — e, em alguns adapters, o corpo do erro do fornecedor — no chunk RUN_ERROR que segue em stream para o browser, e regista as exceções das tools nos spans mesmo com a captura desligada. Os handlers das tools lançam erros genéricos sem dados pessoais, e o servidor substitui a mensagem do RUN_ERROR por uma genérica mais o ID do pedido antes de o stream sair (ADR-0013, ADR-0015)
  • O estado do chat é estado do cliente. O useChat é dono da conversa em curso — mensagens, streaming, paragem, erros. Não é nem dados do servidor nem estado do URL (ADR-0012): não copiar mensagens para o Query nem para o Zustand. As conversas guardadas, se um produto as mantiver, são estado do servidor (Query com server functions GET, escritas por POST) e uma decisão do produto, com as suas próprias regras de retenção e de eliminação
  • Os testes usam um modelo falso, nunca um real. Os testes de servidor e de integração injetam o fakeText() através da fronteira do adapter — respostas, chamadas a tools e erros definidos no teste, com state.callCount para fazer asserções. Os testes de componentes dão ao useChat uma ligação stream() sobre o chat() com um adapter falso. Nenhuma chamada real a um modelo no CI; o onUnhandledFrame: "error" do MSW (ADR-0014) apanha uma fuga. Toda a app com funcionalidades de IA acrescenta testes de guarda a par dos quatro do ADR-0014: um pedido sem sessão é recusado e o modelo falso nunca é chamado; um pedido acima do limite recebe 429 com Retry-After; um corpo demasiado grande é rejeitado antes do modelo; uma mensagem system do cliente e uma lista de tools do cliente nunca chegam ao adapter. A avaliação da qualidade das respostas pertence ao produto
  • Devtools só em desenvolvimento: @tanstack/react-ai-devtools e @tanstack/react-devtools como devDependencies. O painel mostra mensagens e entradas e saídas de tools; não vai para produção
  • Fora do âmbito, cada um a precisar do seu próprio ADR: o fornecedor de modelos; a persistência de conversas (@tanstack/ai-persistence), a memória e os streams retomáveis; o MCP; o code mode e as sandboxes; a geração de media e a voz em tempo real; os embeddings e a recuperação de informação

O que o ADR de fornecedor de cada produto tem de cobrir:

  • Fornecedor, modelo e região. IDs de modelo exatos, não aliases flutuantes. Onde os prompts são processados e guardados — residência dos dados: uma região ou endpoint da UE, ou alojamento próprio com @tanstack/ai-ollama ou openaiCompatible. As transferências para fora do EEE precisam de um mecanismo do capítulo V do RGPD. Um gateway (OpenRouter, Vercel AI Gateway) põe mais um subcontratante no caminho
  • Dados pessoais nos prompts. Que categorias chegam ao fornecedor; o que removemos ou pseudonimizamos antes (minimização de dados, artigo 5.º, n.º 1, alínea c), do RGPD); o fundamento de licitude; um acordo de tratamento de dados com o fornecedor e os seus subcontratantes (artigo 28.º)
  • Retenção e termos de treino. Durante quanto tempo o fornecedor guarda prompts e saídas, incluindo as cópias de monitorização de abusos; se as entradas treinam modelos; opções de retenção zero; e onde isto está escrito — o contrato, não uma página de marketing
  • Custo. Preço por token, uso esperado por utilizador, um orçamento mensal com alerta, e os limites de pedidos e de tokens diários por utilizador deste ADR
  • Alternativa de reserva. O que acontece quando o fornecedor está em baixo, aplica limites ou retira o modelo: um segundo fornecedor por trás da fronteira do adapter, ou a funcionalidade degrada-se e di-lo. Timeouts e retries — um retry é uma segunda cobrança
  • Utilizadores. A UI diz que o utilizador está a falar com um sistema de IA (artigo 50.º, n.º 1, do Regulamento da IA da UE, aplicável desde 2026-08-02); o que fica guardado e como um utilizador o elimina
  • Alcance. As tools e os dados em que o modelo pode tocar

Os fatores decisivos: os mesmos mantenedores e o mesmo ciclo de lançamentos do Router, do Query, do Start e do Form; schemas Zod que tipam tools e respostas e verificam cada chamada; captura de conteúdo desligada por predefinição no seu middleware OpenTelemetry; um modelo falso com respostas definidas no teste para testes offline; e nenhuma camada de plataforma entre nós e o fornecedor. Aceitamos uma API 0.x que muda de poucos em poucos dias, em troca de versões fixadas e atualizações no nosso calendário — a mesma troca do Drizzle v1 no ADR-0008.

Mais fácil:

  • Um fornecedor é um adapter numa só fronteira: mudar, acrescentar ou fazer failover de um fornecedor toca num só módulo, e o SDK reescreve o histórico guardado quando a chamada seguinte vai para outro fornecedor
  • Uma só linguagem de schemas: o Zod tipa os argumentos e os resultados de uma tool e uma resposta estruturada, e o SDK verifica cada chamada
  • Todo o pedido de IA segue o mesmo caminho de qualquer escrita: useChat → server function POST → ID do pedido → limitador → verificação CSRF → sessão → validação Zod → módulo da funcionalidade → adapter → fornecedor
  • Spans de cada execução, volta ao modelo e chamada a uma tool — com contagens de tokens, e custo quando o fornecedor o reporta — aparecem com o resto da telemetria da app (ADR-0015) sem uma palavra de prompt
  • Os testes correm offline e dão sempre a mesma resposta

Mais difícil:

  • Risco de pré-1.0, dito às claras. Qualquer versão minor pode quebrar tipos ou comportamento: o formato do protocolo, os nomes de opções e as props de componentes já mudaram dentro da 0.x. Versões fixadas e um pull request agrupado do Renovate tornam cada atualização uma coisa que agendamos, mas não gratuita — cada uma exige notas de lançamento, uma execução de testes e um deploy conjunto. Se as alterações incompatíveis continuarem a este ritmo depois da 1.0, esta decisão está errada e um novo ADR a substitui
  • Uma subida de minor do núcleo obriga a subidas correspondentes em todos os adapters e no @tanstack/ai-react: não se pode aplicar sozinha uma correção do núcleo
  • Uma segunda cópia do @tanstack/markdown continua a ser instalada. O @tanstack/ai-react 0.31.0 lista-o em dependencies como ^0.0.13, e um caret em 0.0.x só aceita essa versão exata, pelo que o gestor de pacotes a mantém ao lado da nossa 1.x (ADR-0021). Nunca a importamos: os hooks da entrada raiz não a carregam, só o @tanstack/ai-react/ui o faz (verificado no pacote 0.31.0), e a regra acima mantém essa entrada fora do nosso código. A cópia conta na mesma para o lockfile e para as auditorias de dependências, e manter o /ui de fora é uma regra de revisão. Verificar com bun why @tanstack/markdown: esperam-se duas versões, e um bundle de produção que contenha a antiga significa que o /ui ou o TextPart foi importado. Reavaliar quando o @tanstack/ai-react passar para o @tanstack/markdown 1.x
  • Servidor e browser ficam acoplados pelo formato do protocolo do stream. Depois de uma atualização que o altere, os separadores abertos antes do deploy podem falhar até serem recarregados, e os deploys graduais executam brevemente duas versões (ADR-0010). Mostrar erros com o ID do pedido e uma indicação para recarregar
  • O SDK não autentica, não limita pedidos, não remove dados nem sanitiza — essas regras são nossas, aplicadas pela revisão e pelos testes de guarda. Rever alterações de IA como alterações de segurança
  • O controlo de custos é nosso: o limitador conta pedidos, não tokens, pelo que os tetos de ciclo, de tokens, de chamadas a tools e de prazo vivem no nosso código
  • A documentação e os exemplos precisam de tradução. A documentação no GitHub acompanha o main; o pacote publicado inclui src/ e skills/ para agentes, não docs/. O seu exemplo de server function chama .inputValidator(...), que os tipos de createServerFn do Start 1.170 não têm (o nosso é .validator()), e não valida nada em runtime
  • Os adapters trazem o SDK do fornecedor com o seu próprio intervalo: o @tanstack/ai-anthropic 0.22.1 depende de @anthropic-ai/sdk ^0.97.1 (2026-05-19), enquanto a última no npm é a 0.137.0. Uma correção no SDK do fornecedor pode ficar à espera de uma versão do adapter
  • As convenções GenAI do OpenTelemetry ainda estão em estado Development, e o middleware emite gen_ai.system onde a convenção atual diz gen_ai.provider.name. Construir dashboards sobre o que vemos no Tempo e voltar a verificar em cada atualização
  • Uma comunidade mais pequena do que a do AI SDK, e menos fornecedores oficiais; as lacunas passam pelo OpenRouter ou pelo openaiCompatible, ao preço de um gateway no caminho dos dados

Do que abdicamos: da major estável v7 e do ecossistema maior do AI SDK, e do minimalismo de chamar diretamente o SDK de um fornecedor. Aceite — um SDK tipado e agnóstico do fornecedor, da nossa própria stack TanStack e com a captura de prompts desligada por predefinição, pesa mais do que a maturidade numa predefinição da empresa; os produtos com outras necessidades escrevem o seu próprio ADR. Reabrir esta decisão quando a 1.0 sair, se o projeto estagnar ou se um produto precisar de um fornecedor que este SDK não alcança. Reavaliar a regra do Markdown quando o @tanstack/ai-react passar para o @tanstack/markdown 1.x: só então o seu renderizador pode partilhar uma só cópia e uma só política com o nosso.