o problema
Uma chamada comum à Messages API é uma pergunta e uma espera. Você abre a conexão, manda o prompt e fica parado até o modelo terminar de gerar tudo. Trezentos tokens ou trinta mil dá no mesmo do ponto de vista do cliente: o primeiro byte útil chega junto com o último. Numa interface de chat, isso é a diferença entre um cursor piscando por vinte segundos e um texto que começa a aparecer quase imediatamente.
O segundo problema é menos estético. Geração longa segura a conexão aberta por muito tempo, e a infraestrutura HTTP no meio do caminho não gosta disso. Com max_tokens alto, a requisição não-streaming simplesmente não sobrevive ao timeout — os SDKs oficiais tratam isso como erro de uso e exigem streaming, mesmo quando você não tem nenhum interesse em processar a resposta incrementalmente. É por isso que o modo streaming não é um recurso de interface: é o caminho normal para qualquer geração grande.
a ideia
"stream": true transforma a resposta em um fluxo de server-sent events. Em vez de um objeto Message completo, você recebe uma sequência de eventos nomeados que descrevem a construção desse objeto: começou uma mensagem, abriu um bloco de conteúdo, chegou um pedaço, fechou o bloco, acabou. O contrato de destino é o mesmo — se você acumular tudo, tem exatamente o Message que .create() devolveria.
A troca é onde mora a montagem. No modo síncrono, o servidor monta. No streaming, quem monta é você. Os SDKs escondem isso atrás de helpers (get_final_message() em Python, finalMessage() em TypeScript, message.Accumulate(event) em Go, MessageAccumulator em Java, .accumulated_message em Ruby), mas numa integração direta com a API o acumulador é código seu.
como funciona
O fluxo é estável. Primeiro um message_start, com um Message de content vazio. Depois uma série de blocos de conteúdo, cada um com content_block_start, um ou mais content_block_delta e um content_block_stop. Cada bloco carrega um index que corresponde à posição dele no array content final — é por esse índice que você reconstrói, não pela ordem de chegada. Em seguida um ou mais message_delta com mudanças de topo (stop_reason, usage) e, por fim, message_stop. Eventos ping podem aparecer em qualquer ponto.
O delta muda de tipo conforme o bloco. Texto vem em text_delta, com o campo text. Ferramentas vêm em input_json_delta, com partial_json: strings parciais que só formam um objeto válido quando concatenadas até o fim do bloco. Thinking vem em thinking_delta, e logo antes do content_block_stop chega um signature_delta — a assinatura que verifica a integridade do bloco de raciocínio. Com display: "omitted", o bloco de thinking abre, recebe só a assinatura e fecha, sem nenhum thinking_delta.
Em Go, o laço mínimo separa evento e delta em dois switches de tipo:
for stream.Next() {
event := stream.Current()
switch v := event.AsAny().(type) {
case anthropic.ContentBlockDeltaEvent:
switch d := v.Delta.AsAny().(type) {
case anthropic.TextDelta:
fmt.Print(d.Text)
}
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
Ferramentas de servidor aparecem no mesmo stream como blocos próprios: uma busca na web produz um server_tool_use com o query em JSON parcial e, depois, um web_search_tool_result que chega inteiro, sem deltas.
o que isso custou
Três armadilhas de contabilidade e estado. A primeira: o usage do message_delta é cumulativo — quem soma os eventos conta errado. A segunda: tool_use.input é sempre um objeto no fim, mas nunca durante; parsear cedo quebra. E a granularidade prometida é parcial — os modelos atuais só emitem uma chave e valor completos por vez, então o stream de uma ferramenta tem silêncios longos enquanto o modelo trabalha. O eager_input_streaming existe justamente para tirar esse buffer do servidor, por ferramenta.
A terceira é a falha. Erros chegam dentro do stream: um overloaded_error que num contexto não-streaming seria um HTTP 529 aqui vira um evento numa conexão que já respondeu 200. E a recuperação é incompleta por construção — a documentação afirma que blocos de thinking e de tool_use não podem ser parcialmente recuperados, e que a retomada só é possível a partir do último bloco de texto. Pior: a forma de retomar depende do modelo. Até Claude 4.5, você devolve o parcial como início de uma mensagem do assistant; do 4.6 em diante, devolve como mensagem do usuário pedindo continuação. O mesmo cliente falando com duas gerações de modelo precisa de dois caminhos.
Some a isso a política de versionamento: novos tipos de evento podem ser adicionados, e o código tem que tolerar o desconhecido sem estourar. Um switch exaustivo com default: panic é uma bomba-relógio.
onde isso aparece hoje
Streaming é o substrato de quase tudo que veio depois na plataforma. Extended thinking só é observável porque existe thinking_delta, e a assinatura que o acompanha é o que permite devolver o raciocínio em turnos seguintes. Tool use depende do acumulador de JSON parcial, e o uso avançado de ferramentas empurra a granularidade desse acumulador para baixo. O Agent SDK herda o loop inteiro: um agente é um cliente que lê deltas, decide num content_block_stop e reabre outro stream.
O contraponto está do outro lado da mesa. Quando não há ninguém esperando na frente da tela, batch processing resolve o mesmo problema de geração longa sem manter conexão nenhuma aberta.