Todas as publicações sobre gateways que você lê citam uma porcentagem de disponibilidade. Esta não cita, porque não publicamos um SLA e preferimos descrever um mecanismo que você pode verificar a um número que não pode. A seguir está todo o caminho entre um upstream que retorna um erro e o seu cliente que recebe um: quatro camadas, quatro constantes e os motivos pelos quais cada constante tem esse valor.
Por que um único upstream não basta
Um provedor de modelos passar vinte minutos com problemas é algo normal. Incidentes regionais, aperto do limitador de taxa ou uma crise de capacidade em um modelo recém-lançado — nada disso é exótico, e tudo fica invisível para você até que sua própria taxa de erros aumente. Se sua aplicação conversa com um único endpoint, sua disponibilidade é a disponibilidade desse endpoint, e nada pode ser feito pelo cliente além de tentar novamente — quando isso acontece, o usuário já esperou.
A alternativa é ter outro lugar para onde ir. A Kunavo resolve uma cadeia de roteamento para cada modelo — até três canais independentes que podem atendê-lo, ordenados por custo — e as quatro camadas abaixo mostram como uma solicitação percorre essa cadeia sem que você precise escrever lógica de novas tentativas.
Camada 1: o disjuntor (antes do envio da solicitação)
O failover mais barato é aquele que nunca custa uma viagem de ida e volta. Cada processo do gateway mantém uma contagem móvel de falhas de cinco minutos por canal. Três falhas dentro dessa janela fazem o canal ser considerado aberto: pelos sessenta segundos seguintes, ele é removido da cadeia antes mesmo de qualquer solicitação ser despachada.
// Layer 1, simplified from lib/providers/provider-health.ts.
// A channel that has been failing is skipped BEFORE we send anything,
// so the cost of avoiding it is zero — no request, no timeout, no wait.
const WINDOW_MS = 300_000; // failures older than 5 minutes are forgotten
const FAIL_THRESHOLD = 3; // 3 failures inside that window opens the circuit
const COOLDOWN_MS = 60_000; // and the channel is bypassed for 60 seconds
function isOpen(channel: string, now = Date.now()): boolean {
const s = state.get(channel);
return s != null && s.openUntil > now;
}
function recordFailure(channel: string, now = Date.now()) {
const s = state.get(channel) ?? { failures: [], openUntil: 0 };
s.failures = s.failures.filter((t) => now - t < WINDOW_MS);
s.failures.push(now);
if (s.failures.length >= FAIL_THRESHOLD) s.openUntil = now + COOLDOWN_MS;
}Esta é a única camada genuinamente instantânea, e vale ser preciso sobre o motivo: nada está sendo medido rapidamente. A decisão foi tomada até cinco minutos antes por uma solicitação anterior que sofreu a falha em seu nome. O estado é por processo, portanto cada máquina aprende de forma independente — sob tráfego real, um canal problemático dispara em todas elas em questão de segundos, e um canal que ficou indisponível apenas brevemente se recupera sozinho quando o período de espera termina.
Camada 2: failover dentro da solicitação (até três saltos)
Quando um canal que parecia saudável retorna um erro mesmo assim, a solicitação passa para o próximo canal da cadeia. O limite é de três saltos, ROUTING_CHAIN_MAX, e a cadeia é ordenada por custo; portanto, a primeira tentativa é sempre o canal mais barato capaz de atender ao modelo.
// Layer 2. The routing chain is resolved from the catalog, cost-ordered,
// and capped at ROUTING_CHAIN_MAX hops (3). It is static configuration, not
// a learned weighting: you can read the exact chain for any model in the
// admin UI, and it is the same chain for every customer.
async function dispatch(req: ChatRequest) {
const chain = routingChain(req.model) // up to 3 channels, cheapest first
.filter((c) => !isOpen(c.id)); // layer 1 removes the known-bad
let lastError: UpstreamError | null = null;
for (const channel of chain) {
try {
return await callUpstream(channel, req);
} catch (err) {
if (!isRetryable(err)) throw err; // a 400 is yours, not ours
recordFailure(channel.id, Date.now());
lastError = err;
}
}
throw lastError ?? new Error("all channels exhausted");
}Erros do cliente não fazem failover. Um 400 por uma solicitação malformada ou um 404 por um modelo inexistente é a mesma resposta em todos os canais; tentar novamente três vezes apenas faria você esperar três vezes mais pela mesma mensagem. Somente falhas upstream que permitem novas tentativas percorrem a cadeia.
O custo real desta camada é uma ida e volta malsucedida por salto. Se um canal aceita sua conexão e depois retorna 503 após dois segundos, você esperou esses dois segundos antes de a segunda tentativa começar. Essa é a verdadeira razão pela qual a camada 1 importa: ela impede que um canal que está falhando de forma confiável cobre esse pedágio de dois segundos em todas as solicitações.
Camada 3: o portão do streaming (30 segundos)
O streaming torna o failover mais difícil de uma maneira fácil de não perceber. Um upstream pode retornar 200 OK, abrir um stream e só então emitir um evento de erro. Quando isso chega, um proxy ingênuo já enviou ao seu cliente um cabeçalho e alguns bytes, e não há mais como prosseguir para o próximo — a solicitação está comprometida com um canal que não vai respondê-la.
// Layer 3, from lib/providers/stream-gate.ts. An upstream can answer 200 OK
// and then put the error inside the stream. Committing that to your client
// means you get a truncated answer and we cannot fail over any more, so the
// gateway holds the first frames back until it knows which it is.
export const STREAM_GATE_TIMEOUT_MS = 30_000;
// Outcomes:
// a decisive content frame -> commit, stream the rest through untouched
// a decisive error frame -> discard, fall through to the next channel
// nothing decisive in 30s -> commit anyway (a slow model is not an error)Por isso, o gateway não confirma com base no código de status. Ele mantém os frames iniciais até ver algo decisivo: conteúdo real, caso em que o stream é confirmado e repassado sem alterações daí em diante; ou um erro, caso em que nada chegou até você e a solicitação continua pela cadeia. Trinta segundos é o limite para essa decisão, e ele existe para modelos de raciocínio que legitimamente pensam por bastante tempo antes de emitir o primeiro token. Depois disso, confirmamos, porque um modelo lento não é um modelo quebrado.
Camada 4: o tempo limite do cabeçalho (240 segundos)
A última camada é aquela de que ninguém gosta. Se um upstream aceita a conexão e depois não retorna nenhum cabeçalho de resposta, a solicitação fica travada, e a única coisa que a encerra é um tempo limite — UPSTREAM_HEADERS_TIMEOUT_MS, quatro minutos.
Quatro minutos é muito tempo para esperar, e isso é deliberado. A geração de vídeo e imagem legitimamente leva minutos; um tempo limite curto o bastante para parecer responsivo no chat cancelaria trabalhos de vídeo que iriam dar certo. Um upstream travado também é raro em comparação com um que falha explicitamente, e os casos explícitos já são tratados pelas camadas 1 a 3. Preferimos ser lentos no caso raro a estar errados no caso comum.
Se você quiser um limite mais rígido para sua própria aplicação, defina um tempo limite no cliente. O nosso é uma proteção final, não uma meta de latência.
O que não afirmamos
- Nenhum número de failover em milissegundos. A camada 1 não custa nenhuma ida e volta porque a decisão antecede sua solicitação. As camadas 2 a 4 duram exatamente o tempo que o upstream com falha levou para falhar. Nenhum desses valores é um número que possamos colocar honestamente em uma página de marketing.
- Nenhum percentual de disponibilidade. Não há SLA nem esquema de créditos, então um número seria apenas decoração. A página de status mostra a taxa de sucesso que realmente observamos, incluindo os períodos em que ela não foi boa.
- Nenhuma substituição silenciosa de modelo. O failover move uma solicitação entre canais que atendem ao mesmo modelo. Nunca respondemos com um modelo mais barato do que aquele que você solicitou — uma prática que vale perguntar diretamente a qualquer gateway, porque ela é invisível na resposta.
O que isso significa quando você está construindo sobre ele
Principalmente, significa que você pode excluir um loop de novas tentativas. Os erros recuperáveis do upstream já são tentados novamente em canais independentes antes que você veja uma falha, portanto uma nova tentativa no cliente após um 5xx nosso está repetindo algo que já foi tentado três vezes. Mantenha seu tempo limite e remova o backoff.
A única coisa que vale a pena tratar por conta própria é um 402 insufficient balance, que não é um caso de failover — nenhuma quantidade de novas tentativas reabastece uma carteira. Consulte a referência de erros para saber quais status justificam uma segunda tentativa e quais são finais.