Documentação do app v1.0

Motor de seleção de chave

Cada requisição que chega ao Load Balance percorre o mesmo caminho até uma chave concreta: valida a credencial, resolve a organização, tira da frente quem está em cooldown ou fora da janela de uso, escolhe pela estratégia do grupo e só então chama o provedor. Este guia mostra cada etapa e o que acontece quando a chave escolhida falha.

Nenhuma escolha é aleatória

A ordem dentro de um grupo é sempre priority asc, depois lastUsed asc, e o id como desempate estável: duas requisições que encontrarem o mesmo estado do pool escolhem a mesma chave. Quem monta essa ordem e aplica os filtros é pool.ts.

Pré-requisitos#

Antes de seguir o caminho inteiro, você precisa destes quatro fatos sobre a sua configuração.

  • Sua URL de proxy está publicada e pertence a uma organização. O pool de chaves é sempre por organização — não existe pool geral.
  • Você tem uma chave sk-lb- pessoal ou de serviço. Só conta valor com esse prefixo, venha de x-api-key ou de Authorization: Bearer.
  • O grupo tem ao menos uma chave ativa e uma strategy definida. O padrão é round_robin.
  • Para hedge ou race, o grupo tem duas ou mais chaves elegíveis do mesmo provider e do mesmo authType — o portão barra o resto.
  • O caminho de uma requisição#

    Uma requisição atravessa oito etapas. Nenhuma delas fala com o provedor antes das três primeiras.

    1. O proxy recebe

      Os quatro POST públicos são os dois com orgSlug e os dois legados sem ele. Os HEAD das mesmas rotas respondem 200 com corpo vazio: é o preflight que o cliente dispara contra a ANTHROPIC_BASE_URL antes da primeira chamada real.

      • POST /{orgSlug}/{customUrl}/v1/messages
      • POST /{orgSlug}/{customUrl}/v1/messages/count_tokens
      • POST /{customUrl}/v1/messages
      • POST /{customUrl}/v1/messages/count_tokens
    2. A organização e a config são resolvidas

      Com orgSlug, o handler resolve a organização e depois a config da URL dentro dela. Organização inexistente responde igual a config inexistente, para a URL pública não enumerar organizações. Sem orgSlug — a rota legada, com data de remoção declarada para 2026-12-31 — o LB resolve só pelo customUrl e responde 409 pedindo a migração quando a URL existe em mais de uma organização.

    3. A chave sk-lb- é validada

      A credencial é conferida antes de qualquer processamento. Um Bearer sk-ant-oat01-… é o OAuth do próprio cliente, não uma chave do LB, e não conta. Estas são as respostas que você pode receber:

      SituaçãoStatusO que o cliente recebe
      Contrato da empresa encerrado na Conta403permission_error com ORG_SUSPENDED
      Sem chave, chave inválida ou URL que você não vê401authentication_error
      Config inexistente404Custom URL config not found
      Config sem organização403o pool é por organização e não existe pool geral
    4. A chave é removida dos headers

      stripProxyKeyHeaders tira a sk-lb- da request: a credencial do proxy nunca segue para o upstream, porque alguns caminhos repassam Authorization e x-api-key. Em seguida o chamador é registrado em memória — userApiKeyId quando a chave é pessoal, clientApiKeyId quando é de serviço.

    5. Filtro de janela e de cooldown

      A seleção passa sempre por withoutLimitedAccounts, que tira as contas fora da janela de 5 h ou semanal, e depois pelo filtro de cooldown — antes de qualquer escolha. O snapshot vem do Redis com TTL de 15 s e sai ordenado por priority asc, lastUsed asc e id asc.

      Se todas as contas do grupo estiverem fora da janela, o LB não escolhe ninguém: responde 429 imediato com o Retry-After real do provedor, sem tocar no upstream, e o corpo diz qual janela estourou e quando a conta volta.

    6. A chave é escolhida

      O handler principal chama o balancer na primeira linha útil. Se a chave escolhida tiver model próprio configurado, ele sobrescreve o modelo pedido pelo cliente, sempre.

    7. A chamada sai e o failover assume

      Qualquer resposta fora da faixa 2xx é retentável entre chaves. O teto é o snapshot capturado do pool — uma tentativa por chave, sem reciclar — e o relógio é UPSTREAM_TIMEOUT, 180 s por dial.

    8. A resposta volta para o cliente

      No caminho comum, o 2xx do upstream é repassado. Em streaming, o proxy segura o head até ler o primeiro frame SSE válido, com teto de 15 s, e só depois escreve o 200. Se todas as tentativas falharem, o cliente recebe 503 com corpo genérico overloaded_error.

    As cinco estratégias#

    A strategy do grupo decide como a lista de candidatas vira uma chave. São cinco valores, com round_robin como padrão.

    EstratégiaComo escolheQuando usar
    round_robinCursor incremental por grupo, módulo do tamanho da listaVocê quer distribuir o tráfego sem medir nada. Com uma chave só, ela sempre atende.
    priorityA primeira disponível do snapshot, ordenada por priority ascUma chave principal deve absorver o tráfego e as outras são reserva.
    ratedA de menor score de rating entre as disponíveisVocê quer a chave mais rápida conhecida e aceita o custo de medir.
    hedgeA primária sai como em rated; a reserva dispara depois, por timerLatência importa mais que consumo: a segunda chamada só sai se a primeira demorar.
    raceA primária sai como em rated; as concorrentes saem no mesmo instanteVocê quer o menor tempo possível ao preço de mais consumo.

    O rating de rated combina TTFT, latência total e taxa de erro, e o peso do erro é grande porque, nos dados de produção citados, erro domina a latência como causa de requisição ruim. Uma chave sem amostra própria não entra em último lugar por falta de dados: ela entra com a mediana de TTFT do grupo. Se o grupo inteiro não tem nenhum TTFT conhecido, todas ficam iguais, o desempate estável preserva a ordem do snapshot e a corrida cai para o caminho único. Quem implementa o ranqueamento é keyRating.ts.

    Cooldown e desativação#

    Nem toda falha tira a chave de cena. O LB classifica o erro em três categorias e decide, a partir delas, o que fazer com a chave que respondeu:

  • temporary — 429, 5xx e wrappers de gateway. Cooldown, nunca desativa.
  • request_error — 400 de entrada longa, 404 de modelo, 413, 422 e conteúdo bloqueado. Nunca desativa; fica registrado e filtrável.
  • key_error — 401 inválida, 403 de limite atingido e 402 sem saldo. Desativa.
  • O gatilho específico define a duração do cooldown:

    GatilhoDuraçãoDesativa?
    429 (rate limit), pool com até 100 chaves60 snão
    429, pool com mais de 100 chaves10 snão
    402 (billing)BILLING_COOLDOWN_MS, padrão 60 snunca
    temporary — 5xx e wrapper de gateway60 s por padrãonão
    401 em chave meta_device5 s depois de um remint bem-sucedidonão, se o remint resolver
    Três timeouts em 10 minTIMEOUT_COOLDOWN_MS, 5 minnão
    Falha de auth ou OAuth ao montar os headersOAUTH_BACKOFF_MS, 10 minnão
    429 no meio do stream60 s, mais increment429não
    Dial travado antes do primeiro chunk30 snão
    key_error — 401 inválida, 403, 402 sem saldo—sim

    O cooldown de 429 é proporcional ao tamanho do pool: acima de 100 chaves no snapshot ele cai para 10 s, porque com grupos de 400+ o valor fixo de 60 s fazia o failover percorrer a lista inteira em segundos. O estado mora em memória, com espelho best-effort no Redis, e o boot relê o Redis preservando o tempo que faltava do expiry absoluto — um cooldown de reset de janela, que dura horas, não pode virar 60 s. Quem implementa isso é cooldown.ts.

    AUTO_DISABLE_ON_ERROR é o interruptor geral: ligado por padrão, desligado, nenhuma chave vira active=false mesmo em key_error — o proxy continua sanitizando a resposta, mas ninguém sai do rodízio.

    O mesmo erro em três chaves é erro do pedido, não das chaves

    Quando o mesmo par de status e mensagem aparece em três chaves distintas dentro da mesma requisição, o LB entende que o problema é a requisição — modelo digitado errado, tool schema malformado. O guard marca a viagem, reverte todas as desativações feitas nela, para de desativar e para de discar chaves novas, e não sai do loop: chega ao 503 terminal do mesmo jeito. Vale para o caminho com e sem streaming.

    Failover#

    O gate é universal: qualquer status fora da faixa 2xx é retentável, no streaming e fora dele. O que muda é o teto e as regras.

  • Não há número fixo de tentativas. O teto é o snapshot capturado do pool, uma tentativa por chave.
  • O snapshot é tirado uma vez, antes de iterar, para que um cooldown de 60 s que expire no meio do loop não reintroduza uma chave já tentada.
  • Falha de auth ao montar os headers põe a chave em cooldown por 10 min e tenta o resto do pool em ordem, pulando quem já está em cooldown.
  • Cada fallback recebe o próprio transform de provider, inclusive outra chave do mesmo provider, e o corpo é re-sanitizado do zero para a chave que vai atender.
  • A reserva de RPM é acertada em todo caminho de saída: movida quando outra chave atende, cancelada quando a requisição falha.
  • Nada do upstream vaza. Todo non-2xx terminal vira 503 com corpo genérico; a única exceção é o corpo de mensagem do 429 e do 402, porque o cliente pode agir com ele. E, se os headers SSE já saíram, o status não muda mais: o proxy emite um frame event: error genérico e fecha o stream.

    Quando todas estão frias#

    Quando todas as candidatas estão em cooldown, o LB não desiste nem espera: pickEarliestCooldownExpiry devolve a chave cujo cooldown expira primeiro. É o último recurso, e vale tanto no caminho do grupo quanto no pool sem grupo.

    Em empate de expiry a escolha é aleatória, e isso é deliberado — a implementação anterior devolvia sempre a primeira da lista, e o proxy martelava a mesma chave mesmo já sabendo que ela estava em cova.

    Janela de uso esgotada é outro assunto: quando todas as contas do grupo estão fora da janela de 5 h ou semanal, o LB não desempata por cooldown, responde 429 com o Retry-After do provedor. O corte padrão é 95% de consumo e cada chave pode ter o seu próprio — leia Limites de uso.

    Hedge e race#

    hedge e race não mudam a chave primária: ela sai exatamente como em rated. O que muda é quando o resto entra. Antes de qualquer disparo, o portão barra nesta ordem e o primeiro motivo vence:

    OrdemCondiçãoreason
    1O grupo tem o interruptor killSwitch ligadokill_switch
    2mode definido e diferente de hedge e de racesingle_mode
    3Requisição com tools e allowTools igual a falsetools
    4Mais de um provider distinto entre as chaves do grupomultiple_base_urls
    5O grupo inclui o provider opencodeopencode_zen_go
    6Menos de duas chaves elegíveisinsufficient_keys
    7Sem TTFT conhecido da primária — não há o que compararttft_unknown

    Endpoints diferentes mas equivalentes — mesmo provider, contas distintas — são justamente o caso de uso do hedge. O que decide a equivalência não é a string da URL: é o provider, que dá o formato da resposta, mais o authType, que dá os headers.

    Passado o portão, só dois números entram na conta:

  • hedgeDelayMs — padrão do servidor de 2 s, faixa de 500 ms a 30 s. O timer é armado antes do dial e conta do início da requisição, não da chegada dos headers: se a janela só começasse depois do dial, num upstream lento a reserva jamais dispararia. E, se a primária já deu o primeiro token quando a janela abre, a reserva não abre.
  • raceSize — padrão 2, faixa de 2 a 4. O clamp existe para que um valor gravado à mão não abra 40 dials.
  • A corrida gasta upstream de verdade

    shadow ligado é o padrão do grupo: a corrida roda como se estivesse ativa e só descarta as respostas perdedoras, enquanto o cliente fica no caminho único. E a garantia é sobre o cliente — ele recebe a resposta de uma chave só, com um tool_use_id único e consistente do começo ao fim. O que o provedor fez com o request da perdedora antes de o abort chegar é outra conta: o LB não interpreta disable_parallel_tool_use.

    Duas chaves da mesma baseUrl nunca correm juntas, em nenhuma das duas estratégias. A comparação é estrita e dois baseUrl ausentes contam como a mesma URL. O motivo é de domínio: a cota de 429 é da conta/URL, não da chave, então duas chaves diferentes da mesma conta em cova levariam 429 juntas.

    A vitória é decidida no primeiro chunk, nunca depois. O writeHead(200, 'text/event-stream') é ponto sem volta — trocar de upstream ali exigiria renormalizar o message.id e os índices SSE, e o cliente consome esse protocolo. Depois do 200 a requisição segue uma stream só até o fim, e a perdedora é abortada no dial em voo. Quando a reserva vence, o LB realoca a chave alvo para quem realmente atendeu: senão uso, cooldown e erro creditariam a perdedora.

    Próximos passos#

    Você já sabe como a chave é escolhida. Para ver o que acontece nos bytes seguintes, e por que o tempo até o primeiro token entra no rating da chave, siga para o guia de streaming.

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

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