Documentação do app v1.0

Streaming e TTFT

Acompanhe o text/event-stream do primeiro byte até o fim e saiba onde o TTFT é medido. Você separa a latência do upstream da latência do proxy e reage certo quando o primeiro frame não chega.

Escopo

Este guia cobre o caminho com stream: true. No caminho não-streaming a resposta chega inteira: não existe TTFT, e o proxy registra só o tempo total.

Pré-requisitos#

Para acompanhar o fluxo de ponta a ponta, você precisa de:

  • Uma customUrl que o cliente aponta por ANTHROPIC_BASE_URL — o passo a passo está no Quickstart — proxy.
  • Um ApiKeyGroup com chaves ativas. A strategy decide quem atende, e rated, hedge e race leem o TTFT para ordenar o pool.
  • Um cliente que envie stream: true no body de /v1/messages.
  • O que é TTFT#

    TTFT (time to first token) é o tempo entre o recebimento do pedido no proxy e o primeiro frame SSE válido repassado ao cliente. É o número que descreve quanto tempo você espera até a resposta começar a aparecer.

    O proxy guarda quatro medidas por request. Compare as colunas antes de atribuir a lentidão a uma chave:

    MétricaO que medePor que importa
    TTFTpedido recebido → primeiro frame SSE válido repassado ao clienteé a latência percebida, e é o dado que alimenta o rating de velocidade das chaves
    responseTimepedido recebido → fim da respostacusto total da request; usa o mesmo relógio do TTFT, então as duas colunas se comparam linha a linha
    ttftEwmaMsmédia exponencial do TTFT por chave, guardada no Redisé o número que rated, hedge e race consomem na seleção
    primaryTtftMs e measuredTtftMsTTFT esperado da primária contra o TTFT medido, no log do modo shadowmostram se a corrida teria escolhido outra chave

    Como o stream passa#

    O caminho tem quatro tempos, nesta ordem. Cada um fecha uma porta: depois dele, muda o que ainda é possível fazer com a resposta.

    1. 1 · Headers segurados

      Depois do dial, o handler não confirma 200. Ele segura o head e lê o primeiro frame SSE do upstream com readFirstFrame, sob o teto de FIRST_CHUNK_TIMEOUT_MS. Enquanto o head não sai, o failover para a próxima chave continua possível.

    2. 2 · Primeiro frame

      Se o primeiro frame é dado válido, os bytes voltam para a frente da stream com unshift — replay byte-idêntico — e só então o proxy escreve o head SSE. Se o primeiro frame é erro, o corpo, até 64 KB, segue para o classificador e a chave seguinte é tentada em silêncio.

    3. 3 · Corpo

      O coletor repassa só os bytes que o SseParser reconheceu como frame válido. Texto cru colado entre frames não chega ao cliente. É neste mesmo ponto que o TTFT é carimbado.

    4. 4 · Fim

      Quando o stream termina, detectStreamErrors varre os eventos coletados e aplica a mesma política de erro do caminho HTTP. O cliente recebe o fechamento normal do stream.

    O formato que atravessa o fio é o SSE canônico do Anthropic Messages. Cada frame carrega uma linha data: com um objeto JSON, e os frames são separados por uma linha em branco:

    text
    event: message_start
    data: {"type":"message_start","message":{"id":"msg_01H8","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":24,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1}}}
    
    event: content_block_start
    data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
    
    event: content_block_delta
    data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Olá"}}
    
    event: content_block_delta
    data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}}
    
    event: content_block_stop
    data: {"type":"content_block_stop","index":0}
    
    event: message_delta
    data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":12}}
    
    event: message_stop
    data: {"type":"message_stop"}

    O sentinela [DONE] também conta como frame válido, e o parser o descarta em silêncio. Frame sem linha data: parseável não vale.

    Antes do 200, o resultado da leitura do primeiro frame decide o caminho. Estes são os cinco desfechos possíveis:

    Desfecho de readFirstFrameQuando aconteceO que o proxy faz
    datao primeiro frame é dado válidodevolve os bytes à frente da stream e confirma o 200
    erroro primeiro frame já é erro do upstreamclassifica o corpo, aplica a política de erro na chave e tenta a seguinte
    endedo upstream fechou o stream sem mandar nadamesmo failover do caso anterior
    timeoutestourou FIRST_CHUNK_TIMEOUT_MS sem chegar frameconta como falha da chave e faz failover — o cliente nunca viu o 200
    abortedo cliente desconectou durante a leituranada é escrito e o dial é destruído, sem socket órfão
    O que FIRST_CHUNK_TIMEOUT_MS controla

    É o teto de espera pelo primeiro frame SSE válido. Default 15000 ms, faixa de 1000 a 120000. Sem esse teto, uma chave presa só sairia da frente quando o UPSTREAM_TIMEOUT de 180 s estourasse; com ele, a chave lenta sai em segundos e a próxima é tentada. O caso fica registrado no log como [FIRST-CHUNK] timeout de primeiro chunk.

    Onde o proxy mede#

    O TTFT é carimbado no transform do coletor, no mesmo if (validBytes) que repassa os bytes reconhecidos: é o instante em que o primeiro byte útil atravessa para o cliente. O carimbo nunca reseta — do segundo frame em diante, o valor já não é TTFT.

    O ponto não é o primeiro byte cru. Entre o upstream e o cliente passam o parse do SSE, os transforms de provider e a fila do socket. Medir no start do upstream, ou no primeiro byte cru, daria um número menor do que o cliente sente, e você compararia esse número com a experiência do usuário acreditando que eram a mesma coisa.

    Três consequências seguem desse ponto de medição:

  • O flush do coletor não carimba. Se nada foi reconhecido antes dele, não houve primeiro token para o cliente e getTtftMs() devolve null.
  • O valor é firstByteAtMs - startTimeMs, no mesmo relógio de responseTime. Por isso as duas medidas se comparam linha a linha.
  • Esse TTFT alimenta recordKeyRating via logRequestToDatabase, e daí o ttftEwmaMs de cada chave no Redis.
  • Erro no meio do stream#

    Depois que o 200 sai, o status HTTP está congelado. O proxy age sobre a chave e sobre o corpo que emite, mas não muda o que o cliente já recebeu.

    O 200 é ponto sem volta

    Um erro que chega depois de dados válidos já repassados não passa por failover. Esconder um erro no meio do stream é impossível em qualquer protocolo de streaming: o cliente já tem parte da resposta.

    Nesse ponto, o proxy faz o seguinte:

  • Emite um frame event: error com corpo genérico e encerra o stream. A mensagem real do erro não é repassada: pode conter stack trace de TLS, hostname interno ou a string literal aborted.
  • Detecta o erro em três formatos: type: response.failed do OpenAI Responses, type: error do Anthropic e o objeto error sem type no topo, que Mistral, Z.AI e OpenRouter usam.
  • Classifica com o mesmo errorClassifier do caminho HTTP, passando 200 como status de entrada. O status real sai do code do corpo, do tipo ou do texto da mensagem.
  • Aplica a política da chave: 429 vira cooldown de 60 s com increment429; temporary vira cooldown; request_error só registra; billing_exhausted entra em cooldown e nunca desativa; 401 em chave meta_device tenta o remint; o resto desativa a chave.
  • Registra a request com o status real, e não com 200.
  • O frame que o cliente recebe é este:

    text
    event: error
    data: {"type":"error","error":{"type":"overloaded_error","message":"All configured providers are unavailable. Please try again shortly."}}

    Erro que chega antes do primeiro frame válido é outro caso: o head ainda não saiu, então o proxy aplica a política na chave e faz failover em silêncio. Você recebe a mesma resposta que receberia se a primeira chave tivesse funcionado.

    O que afeta a latência#

    Cada fator abaixo age num ponto diferente do caminho. Use a terceira coluna para achar onde mexer:

    FatorOnde ageO que controla
    Geração do modelo no upstreamantes do primeiro frameé a maior parte do TTFT; rated ordena as chaves por esse número
    Chave presa sem primeiro frameentre o dial e o 200FIRST_CHUNK_TIMEOUT_MS tira a chave da frente em 15 s, em vez de esperar 180 s
    Parse do SSE e transforms de providerno caminho até o clienteentram na medição do TTFT; não são latência escondida
    Backpressure do clientena fila do socketo proxy faz push-through: um cliente lento atrasa o que chega depois dele
    Cooldown e failoverna escolha da chave seguintechave em cooldown não entra no pool; cada fallback paga um novo dial e um corpo re-sanitizado
    hedge e racea partir de hedgeDelayMsa reserva dispara 2000 ms depois do início da request; com shadow: true o cliente não muda de caminho
    Retenção de log do streamsó em memóriaSTREAM_LOG_LIMIT_BYTES (1 MB) limita o que fica guardado, não o que é repassado

    Próximos passos#

    Com o stream e o TTFT claros, veja como essa medição vira escolha de chave:

    Depois de dominar o básico, explore além:

    Documentação gerada com a skill docs-padrao.