Commit Graph

2871 Commits

Author SHA1 Message Date
Dizzle
a152eb92db fix(resilience): stop unbounded queue that hangs 6min until Aborted (#12715)
Fila sem teto que segura a request seis minutos até o cliente abortar é pior que 503 imediato: consome slot, mascara a saturação e ainda entrega erro no fim. Um orçamento `maxWaitMs` por conexão compartilhado entre gate, slot padrão do provider e fila do Bottleneck é a forma certa — o teto tem que ser um só, senão cada camada espera o seu.

O `max(perConn, upstream)` no `executionMaxWaitMs` é o detalhe que evita a correção matar request em voo, que seria trocar um defeito por outro.

Registro a atribuição: você manteve o #12635 aberto para o @Tushar49 e creditou a percepção dele (providers lentos precisam de 2min→10min por conexão) enquanto adiciona o encanamento que faltava. É o jeito certo de construir sobre PR de outra pessoa sem tomar o crédito.

Sobre o `npm run lint` desmarcado com a nota do eslint quebrado no ambiente: deixar em branco e explicar vale mais que marcar sem ter rodado. Rodei aqui: limpo.

Revalidei sobre o tip: **13/13**, typecheck:core limpo, check-file-size OK. O `file-size-baseline.json` conflitou com os rebaselines desta campanha — resolvido aditivamente, JSON revalidado com `json.load`.
2026-09-10 10:49:34 -03:00
Ravi Tharuma
d6a61074dc fix(quota): align AUTH window labels with usage API and clarify 503 (#12884)
Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar.

Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK.

**Dois consertos meus na sua branch.**

1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no #12746/#12811, mergeados nesta mesma campanha depois que você cortou a branch.

2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o #12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
2026-09-10 10:47:11 -03:00
Ravi Tharuma
3e2a6d8f35 fix(credentialHealth): do not poison multi-upstream openai-compat conn on one model 402 (#12875)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Envenenar a conexão inteira por um 402 de **um** modelo é o erro clássico de granularidade em provider openai-compatible com múltiplos upstreams — derruba modelos que estavam saudáveis. Restringir ao modelo afetado é o comportamento correto, e é a mesma distinção que o guia de resiliência faz entre cooldown de conexão e lockout de modelo.
2026-09-10 10:42:34 -03:00
Dizzle
12c7895bbd fix(sse): rotate opencode accounts on geo-blocked 403 (#12941)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Estender a rotação que já existe para 429 ao 403 de bloqueio geográfico é a generalização certa, e manter a rejeição de fingerprint (Cloudflare 1010) fora dela é o que impede a rotação de queimar todas as contas contra uma recusa que não é de egresso.

Nota: os checkboxes de validação do corpo ficaram em branco, mas o diff traz dois arquivos de teste — vale marcar da próxima para o revisor não precisar conferir.
2026-09-10 10:19:14 -03:00
Markus Hartung
1216cff05b fix(streaming): fail fast when an upstream stream produces only lifecycle/heartbeat events, never real content (#12741)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Um stream que só emite eventos de ciclo de vida e heartbeat, sem conteúdo nenhum, é falha disfarçada de sucesso: o cliente espera até o timeout dele. Falhar rápido devolve o controle.
2026-09-10 10:19:10 -03:00
Markus Hartung
2b1c57485c fix(sse): stop rebuilding a truncated summary from the collector's cap-dropped event array (#12718)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Reconstruir o resumo a partir de um array que o próprio coletor já truncou por cap produz um resumo que parece completo e não é — pior que resumo ausente, porque não se distingue. Parar de reconstruir dali é a correção.
2026-09-10 10:19:06 -03:00
Dizzle
91990d606b fix(sse): emit trailing usage estimate in translate streams when upstream stays silent (#12828)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

O #12151 cobriu só metade: passthrough emitia o chunk final de usage, translate calculava a estimativa **depois** de fechar o stream, então o número só chegava ao log do servidor e nunca ao cliente. Fechar essa metade é o que faz a feature existir de fato.

Não emitir segundo chunk quando o upstream já mandou usage real é o detalhe que impede a correção de virar contagem dobrada.
2026-09-10 10:19:02 -03:00
Ravi Tharuma
3272bedd1b fix(translator): drop or map agent_message on Chat Completions fallback (#12880)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

`agent_message` chegando num fallback de Chat Completions é um item que o cliente não sabe interpretar; mapear ou descartar é a escolha certa, e escolher por item em vez de derrubar a resposta inteira mantém o fallback útil.
2026-09-10 10:18:58 -03:00
Dizzle
8ecdd88c15 fix(pool): diagnose empty pools, align catalog filter, and surface drop reasons (#12795)
Um pool `auto/*` vazio que não diz por que está vazio é a pior forma de falha: o operador vê ausência e não sabe se é config, cota ou catálogo. Registrar qual estágio removeu quantos, e carregar `dropReason` em cada entrada de `quotaHealth.providers`, transforma silêncio em diagnóstico.

Os três defeitos achados de carona valem tanto quanto a feature — em especial a cota livre recorrente sem teto sendo tratada como desconhecida em vez de segura, que é justamente o caso que mais aparece.

Revalidei após reconstruir sobre o tip: **15/15**, typecheck:core limpo, `check-api-typecheck` OK (289).

**Uma mudança minha no `catalogPaidFilter.ts`.** Mantive a sua extração — ela é mais limpa que o predicado inline — mas troquei o corpo para usar `isFreeForProvider` por id. O módulo OR-eava `providerHasFreeModels(resolved) || providerHasFreeModels(canonical)` num único `freeProvider` e depois testava `isFreeModel` em cada um. Com isso, um id cujo próprio provider não documenta tier livre passa a ler como grátis sempre que o alias irmão documenta — que é exatamente o buraco que o #12744 fechou e já está no tip. Por id preserva a garantia; nenhuma outra linha do módulo mudou.

**Dívida registrada:** o `virtualFactory.ts` foi de 1187 para 1219 somando esta onda e cruzou o teto de 1200 pela primeira vez. Congelado com os candidatos a extração nomeados na justificativa.
2026-09-10 09:25:59 -03:00
Dizzle
d88fc2bce7 fix(routing): table pricing with catalog fallback for off-table models, pooled latency bootstrap, fresh tier cache (#12792)
Um modelo grátis fora da tabela herdando $5/$15 por milhão e afundando no roteamento cost-aware é o defeito mais caro desta onda: silencioso, e inverte exatamente a decisão que o operador quer.

Parar de chutar 1500ms de latência para modelo desconhecido e usar a mediana observada do pool — com contador de quantas vezes o chute dispara — é trocar heurística por medição do jeito certo. O contador é o que permite saber se valeu.

Revalidei após reconstruir a branch sobre o tip: **33/33** nas suítes da PR, typecheck:core limpo, `check-api-typecheck` OK (289).

**Duas integrações:**

1. `computeSnapshotWeights` conflitou com o #12794 (health via breaker + quality), já mergeado. Os dois compõem e ambos ficaram: o seu termo de `reliability` — que era a única chave que o caminho de snapshot ainda ignorava — mais o health observado e o quality do #12794.
2. `scripts/quality/run-all-gates.mjs` conflitou com o `check:provider-order-sync` do #12790. Aditivo, os dois gates coexistem.

**Nota de dívida:** o `virtualFactory.ts` cruzou o teto de 1200 linhas pela primeira vez (1187 → 1207) somando esta onda. Congelei em vez de dividir e registrei os dois candidatos a extração na justificativa — `computeSnapshotWeights` (~85 linhas) e o grupo de elegibilidade de credencial (~70). Qualquer um dos dois volta o arquivo para baixo do cap.
2026-09-10 09:20:18 -03:00
Dizzle
9795372ef1 fix(auto-combo): give every mode pack quality and reliability weights (#12731)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

`reliability-first` que não pesava reliability é o defeito mais constrangedor possível num mode pack, e a causa é clara: `modePacks.ts:13` substituía os defaults por inteiro. Financiar os novos pesos com `quota`/`costInv`/`tierPriority` mantendo cada pack somando 1.0 é a parte que exige cuidado e você fez.

Manter `quality-first` em 0.03, igual ao default, para que ele não fique mais fraco que `balanced`, é o tipo de detalhe que só aparece quando se checa a tabela inteira.

**Integração:** conflitou com o #12794 no `computeSnapshotWeights`; os dois compõem e ambos ficaram.
2026-09-10 09:13:04 -03:00
Dizzle
2b276e2043 fix(auto-combo): stop neutral snapshot scoring on observed tier/quality/breaker (#12794)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

Tratar breaker aberto como equivalente a fechado no scoring de snapshot é pior que não pontuar: afirma saúde onde há falha conhecida. Trocar as três constantes neutras por valor observado é a correção, e manter preço e orçamento fora do escopo mantém a PR revisável.

**Integração:** o `computeSnapshotWeights` conflitou com o #12731, que adiciona peso de `reliability` a partir de `failureRate`/`errorRate`. Os dois cobrem chaves diferentes e compõem — ficaram ambos: reliability do #12731, health via breaker e quality desta PR, quota neutro nos dois. Nenhum dos dois lados foi descartado.
2026-09-10 09:12:58 -03:00
Dizzle
0048be75c8 feat(providers): advertise free-tier capability in provider plugin manifest (#12786)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

Metadado de display derivado da mesma fonte da decisão, com um gate STRICT que quebra o CI se a contagem do manifesto divergir do catálogo — é o detalhe que impede a tag de virar mentira daqui a três meses.

Registrar que 77 entradas de catálogo viram 76 no manifesto porque o `arcee-ai` ainda não tem entrada no registry é exatamente o tipo de discrepância que costuma virar bug fantasma.
2026-09-10 09:12:10 -03:00
Dizzle
39f9fc06c2 fix(models): stop treating provider-supplied free flags as trusted without a documented free tier (#12744)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

Parar de honrar `isFree:true`, `:free` e `0/0` vindos do upstream **antes** de consultar o catálogo é a inversão certa: hoje um provider sem tier livre documentado consegue se declarar grátis e o listing diverge do roteador `auto/*`. Checar as heurísticas depois do hit de catálogo fecha a porta sem quebrar o caminho de linhas custom locais, que continuam confiáveis pelo caminho próprio.

O `isFreeModel("or", …)` com um alias que não existe é o tipo de bug que passa despercebido porque falha silenciosamente para o lado permissivo.

**Integração:** o `decideHidePaid` que o #12795 extraiu passou a usar o seu `isFreeForProvider` por id, em vez de OR-ear os dois aliases num único `freeProvider`. A forma por id é a garantia que esta PR estabelece, então ela prevaleceu.
2026-09-10 09:12:07 -03:00
Bob.Hou
bfbd090a96 fix(codex): lift nested child cooldowns on parent clear and on snapshot headroom (#12951)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289). Revalidei o head atual mergeado com o tip: typecheck:core limpo e **36/36** entre `db-rate-limit-guard` e as suítes desta PR.

Levantar o cooldown do escopo pai sem deixar os filhos aninhados presos é o miolo — um cooldown órfão em filho é invisível no dashboard e mantém a conexão fora de rota sem explicação.

**Nota de coordenação:** o `setConnectionRateLimitUntil` colidiu com o #12788 (guard contra timestamp não-finito ou já expirado), que mergeei nesta mesma onda. Eu tinha resolvido a integração na minha worktree, mas ao empurrar o push foi rejeitado — você já tinha empurrado `441fd44`, `f853ba5` e `af2ed01` com a integração feita, e a sua ordenação é equivalente à minha. Descartei a minha e mantive a sua; o crédito é seu inteiro. Fica o registro de que push rejeitado não é erro leve: se eu tivesse mergeado sem reler, teria levado a branch errada.
2026-09-10 08:15:57 -03:00
anhtahaylove
9492357360 fix(deepseek): release the PoW worker slot when spawning fails (#13097)
`solveInWorker` increments `activeWorkerCount` before constructing the Worker,
but the `cleanup()` that decrements it lives inside the promise executor and only
runs once the worker exists. Anything that throws first -- most obviously
`resolveWorkerPath()` when the worker script is missing, since it resolves
against `process.cwd()` -- leaves the counter permanently incremented.

With `MAX_CONCURRENT_WORKERS = 2`, two such failures wedge the solver for the
lifetime of the process: every later call rejects with "capacity reached (2)"
while no worker is actually running, and the real cause is hidden.

Construct the Worker inside a try/catch and release the slot before rejecting.

Fixes #13094
2026-09-09 20:58:23 -03:00
Bob.Hou
0f81e7557c feat(volcengine): canonical quota window mapping and safe multi-connection plan binding (#12950)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.

Mapeamento canônico de janela de quota mais binding multi-conexão seguro, com 4 arquivos de teste para 4 de produção — proporção que dá para revisar.
2026-09-08 09:29:45 -03:00
Diego Rodrigues de Sa e Souza
89c42d36df fix(security): redact AIza credentials of any length in error bodies (#12964)
Vazamento de credencial em corpo de erro. `tests/unit/error-sanitizer-sk-key-qv45.test.ts` falhava no tip em 8ms:

```
AssertionError: Google key survived: Bad credentials for AIzaSyA1B2C3D4E5F6G7H8I9J0KaLbMcNdOeP
```

O padrão era `/AIza[0-9A-Za-z_-]{35}/` — comprimento **exato**. Uma chave Google padrão tem 39 caracteres e casa; qualquer credencial `AIza…` mais curta ou mais longa passava direto para o corpo do erro.

Os dois lados divergiram na reconciliação de dois PRs do mesmo GHSA: o padrão com `{35}` veio do #12506, o teste anti-drift que cobra `/\\bAIza[A-Za-z0-9_-]{20,}/` veio do #12620. Está vermelho desde que os dois entraram em sequência.

`{20,}` no lugar de `{35}`. Numa mensagem de erro, redigir demais uma string que apenas começa com `AIza` não custa nada; redigir de menos vaza credencial — o lado errado para errar é claro.

Evidência: o arquivo vai de 7/9 para **9/9**. Bateria de sanitização com 538 testes: 533 passam, e as 5 restantes são pré-existentes no tip, não desta mudança (4 levam 21–25s por spawn de processo isolado sob carga; `tunnel-routes-error-sanitization` falha igual no tip puro, verificado). Nenhum teste foi enfraquecido — o padrão foi ampliado para satisfazer uma asserção que já existia.
2026-09-08 09:10:36 -03:00
Diego Rodrigues de Sa e Souza
3ab53d188c fix(sse): restore abort mapping, cache telemetry and fence safety on the non-streaming leg (#12990)
Consertadas 5 das 7 regressões que o #12867 introduziu em `tests/unit/chatcore-translation-paths.test.ts` — arquivo que ele não toca, e por isso fora da minha validação focada quando o mergeei. Medido: **74/74** em `ce49d96` (antes), **67/74** em `d6f3150` (depois), **72/74** agora.

**Abort de cliente perdeu o mapeamento (3 testes).** O leg classificava por `error.name === "AbortError"`, mas `abort(reason)` pode rejeitar com string crua sem `name` — essa forma caía em 502 em vez de 499, o que o #7907 fixou. E a mensagem passava por `formatProviderError`, entregando `[499]: request aborted by client` ao cliente. O `chatCore` sempre usou `isLocalStreamLifecycleError` e o literal `"Request aborted"`; espelhado.

**`clientResponse` sintético em abort (1 teste).** O caminho antigo omitia o campo porque o cliente já tinha desconectado — esse corpo é o que teríamos enviado, e o dashboard lê o campo como "o que o cliente recebeu". O caminho novo gravava sempre.

**Telemetria de prompt cache sumiu do call log (1 teste).** `claudePromptCacheLogMeta` só era construído dentro do `executeProviderRequest`; o leg virou dono do primeiro send e a variável ficou `null`, então `_omniroute.claudePromptCache` desapareceu **em silêncio** de todo call log desse caminho. Não é teste chato: é observabilidade perdida em produção.

**Corpo não canonicalizável derrubava a request (1 teste).** `derivePostInjectionRequestIdentity` era chamado antes de qualquer checagem de flag; ele canonicaliza o corpo e o `canonicalStringify` rejeita `Date`, `Map` e instâncias de classe por desenho. Um corpo com essas formas lançava `TypeError` em **toda** request não-streaming, inclusive com `SERVER_OWNED_TOOL_LOOP_ENABLED` desligada, que é o default. Agora deriva só quando o loop pode rodar e falha fechada.

Evidência: 74 testes do arquivo 72/74; 138 nas 5 suítes vizinhas com 136 passando; `typecheck:core` limpo; `check-api-typecheck` OK 289; ESLint 0.

**As 2 restantes ficam abertas de propósito** — `refreshes GitHub credentials after 401` e `locks per-model quota failures`. Mesma causa: o leg encerra num não-2xx sem passar pela classificação de falha do `chatCore`. `nonStreamingProviderLeg.ts` não tem uma ocorrência de `lockModel`, `refreshCredentials` ou `markAccountUnavailable`; o `chatCore` tem ~170 linhas disso mais o bloco de refresh 401. Em produção: token Copilot não renova no 401, e 402/429 por quota não trava o modelo naquela conexão. Não consertei porque devolver a `Response` ao `chatCore` é impossível (já consumida por `.text()`) e reimplementar a classificação no leg é decisão de desenho do refactor — @HouMinXi tem o contexto.
2026-09-08 09:10:03 -03:00
Diego Rodrigues de Sa e Souza
99282e1054 fix(sse): pin the ok variant of the non-streaming leg result in chatCore (#12963)
Base-red: `API Route Typecheck` falhava no tip com 13 TS2339 novos em `chatCore.ts`, vindos do #12867 — que eu mergeei validando só com `typecheck:core`, que não cobre esse arquivo.

Causa: `legResult` é a união `NonStreamingProviderLegResult`; o guard de erro estreita para a variante `ok`, mas a reatribuição condicional do tool loop devolve o tipo declarado e as 13 leituras seguintes perdem a narrowing. Corrigido fixando a variante num binding próprio — `loopApply.leg` já é `& { kind: "ok" }`, então sem cast.

Gate: 302 erros com 13 novos → **289, todos dentro da baseline congelada**. `typecheck:core` limpo, ESLint 0 no arquivo.

Dois commits: o primeiro é Prettier puro sobre o arquivo do tip (que chegou fora do padrão pelo #12867), verificado byte a byte contra `prettier(tip)`; o segundo é a mudança semântica, 39 linhas.

Os demais vermelhos deste PR são herdados e cobertos por #12990, #12964 e #12970.
2026-09-08 09:05:16 -03:00
Bob.Hou
d6f315018a fix(chat): continue after a server-owned tool on Chat Completions (#12867)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, e a suíte vitest:ui completa (2149) verde.

Sobre esta PR especificamente: rodei os **23 arquivos de teste** que ela toca sobre o tip final, depois do merge da base — **392/392**. A migration `174_server_tool_executions.sql` não colide (o tip está em 173, e você já a renumerou em `c35f0fd7`).

O dono foi consultado antes do merge, porque o loop está atrás da flag `SERVER_OWNED_TOOL_LOOP_ENABLED` mas o primeiro send não-streaming mudou de dono sem flag, e a verificação manual em combo com Memory continuava desmarcada. A condição dele foi: entra se os testes focados passarem aqui. Passaram.

O lock de passthrough (`fetchCalls.length === 1`) é a parte que mais me convenceu — o double-dispatch que um `if (stream)` em volta do send existente causaria é exatamente o tipo de regressão que não aparece em teste de comportamento, só em contagem de chamada.

**Três ajustes meus na sua branch:**

1. `tests/unit/chatcore-stream-error-result.test.ts` procurava `"const legResult = await runNonStreamingProviderLeg"`, mas o seu commit final `6077b9dd` passou a reatribuir `legResult` e trocou para `let`. O guard falhava na sua própria branch (confirmei que o arquivo e o `chatCore.ts` eram byte-idênticos ao head da PR, então não era efeito da leva). Passou a aceitar `const|let` — a intenção do guard é o try/catch em volta da chamada, não a palavra-chave.

2. `tests/integration/skills-pipeline.test.ts` foi de 1156 para 1338 linhas e estourou o `testCap` de 1200. Segui o mesmo caminho que você já tinha tomado em `a1d2d20d` para os testes unitários: extraí os três casos do server-owned tool loop para `tests/integration/server-owned-tool-loop-pipeline.test.ts` (259 linhas), com instância própria do harness. O glob `tests/integration/*.test.ts` pega o arquivo novo sem registro adicional. 3/3 verdes isolados.

3. O arquivo novo herdou cinco `any` do original — que só passavam por estarem congelados no `eslint-suppressions.json` sob o nome antigo. Tipei como `Record<string, unknown>`. E `tests/unit/non-streaming-finalization.test.ts` tinha dois argumentos não usados em `trackPendingRequest`, agora prefixados com `_`.

Nada disso toca produção nem enfraquece asserção.
2026-09-07 09:15:00 -03:00
Bob.Hou
ce49d969ca refactor(combo): move handleRoundRobinCombo into roundRobinCombo.ts (#12811)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

**Sobre a reconstrução da branch.** Esta PR continha os 7 commits do #12746 mais os 3 do round-robin. O dono escolheu mergear os dois em sequência em vez de fechar um como subsumido, então depois que o squash do #12746 entrou eu reconstruí esta branch: cherry-pick de `05059880`, `54168238` e `ddd6bcbf` sobre o tip novo, e force-push. Autoria preservada — os três commits continuam seus (`Minxi Hou <houminxi@gmail.com>`), verificado com `git log --format=%an` antes do push. A PR foi de +4167/−3187 em 14 arquivos para +1281/−1182 em 5, que é o delta real do round-robin.

O `05059880` ("guard round-robin extract before the lift") é o commit que faz esse tipo de extract ser revisável: sem um teste que fixe o contrato antes do movimento, mover 1198 linhas é indistinguível de reescrever 1198 linhas.

Revalidei sobre o tip reconstruído: `round-robin-combo`, `combo-attempt-loop`, `execute-target-attempt`, `execute-target-gates` e `combo-loop-safety-timer-leak-11804` — 24/24 — com typecheck:core limpo e o cap de arquivo OK.
2026-09-07 09:09:51 -03:00
Bob.Hou
6b587d0046 refactor(combo): split executeTarget into gates, attempt, and loop (#12746)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

Guardar os contratos com testes ANTES de levantar o bloco (`05059880` no irmão, e o `287658f5` marcando os `executeTargetGates` como lift-as-is) é o que torna um refactor deste tamanho auditável. Sem essa ordem, um extract de 3 mil linhas é indistinguível de uma reescrita.

Revalidei depois do merge da base: `combo-attempt-loop`, `execute-target-attempt`, `execute-target-gates` e `combo-loop-safety-timer-leak-11804` — 20/20 — mais typecheck:core limpo e o cap de arquivo OK.

O #12811 entra na sequência logo em seguida, com os três commits do round-robin sobre este.
2026-09-07 09:07:19 -03:00
Bob.Hou
c1b34db50d feat(combo): quota-weighted routing — skip empty accounts, draw by leftover (#12789)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

Reservar o sorteio antes do próximo `await` (`2cf74acc`) é a parte não-óbvia e a que mais importa: sem isso dois pipelines no mesmo processo observam `inflight=0` na mesma conta e convergem para ela. O comentário no código explica isso melhor do que o commit message.

**Um ajuste meu na sua branch.** O `tests/unit/combo/quota-weighted-strategy.test.ts` era intermitente — falhava em cerca de 1 a cada 5 execuções, alternando entre `A/B isolation: 7 hard-empty…` e `floor=0 puts 0.5% in the main pool`, sempre com dois pares de mesma faixa trocando de posição. A causa é o helper de fixture:

```ts
const iso = (ms = 86_400_000) => new Date(Date.now() + ms).toISOString();
```

Como `iso()` é chamado a cada invocação do fetcher, dois peers que deveriam empatar recebiam `resetAt` com um milissegundo de diferença sempre que o relógio virava entre as duas chamadas. Pressão de reset entra no score, então esse epsilon quebrava o empate e `sortByScoreThenIndex` nunca chegava ao fallback por índice de inserção.

Fixei a base do relógio uma vez só (`CLOCK_BASE`). Nenhuma asserção foi tocada — as garantias de ordem, tamanho e exclusão continuam idênticas. 10/10 execuções verdes depois, e mais 6/6 após o merge da base nesta branch.

Também mergeei a base para resolver `file-size-baseline.json` (aditivo) e `src/domain/quotaCache.ts`, onde o seu placeholder `_providerSpecificData` cedeu lugar à implementação do #12803, que usa o parâmetro de fato.
2026-09-07 09:05:29 -03:00
Bob.Hou
ebdbd2c67d feat(models): live account catalog for Claude, Codex, Copilot, AGY (#12866)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

O ponto que sustenta a PR é o `models.dev` virar overlay de preço em vez de fonte de catálogo. Um catálogo estático que sobrevive à conta já ter listado ids mais novos é o tipo de defeito que só aparece quando o modelo novo é justamente o que se quer usar.

Nota de integração: `activeSyncedCatalog.ts` colidiu com o #12934 (união dos `customModels` do picker no catálogo de despacho). Como você extraiu o bloco original para `loadConnectionCatalog`, os dois se compõem: a união dos irmãos agy/antigravity primeiro, o `unionCustomModels` por cima. Revalidei com `custom-models-live-catalog-12597`, `live-model-catalog-reconciliation-8926`, `sync-models-degraded-cached-catalog-9683`, `models-dev-catalog-read-gate`, `discovery-class`, `reactive-model-sync` e `l1-oauth-autosync-default` juntos — 48/48 — mais typecheck:core limpo.

Sobre o `autoSync` padrão em Claude/Codex/Copilot com scheduler de 6h: passei isso pelo dono antes de mergear e a decisão foi manter como está.
2026-09-07 09:02:59 -03:00
Bob.Hou
1b97f42ba3 fix(combo): treat a pin-only step as implicit connection allowlist (#12697)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Após o merge da base nesta branch, os 11/11 do `combo-pin-implicit-allowlist` foram revalidados.

A distinção entre pin de step de combo e pin forçado por header (`x-omniroute-connection`) é o que salva a PR de virar uma restrição ampla demais — o header continua permitindo fallback para conexões irmãs, o step não.

Apontar que o `a11930ec4` para a rotação dentro do `handleSingleModel` mas não popula `allowedConnectionIds` no resolve foi a peça que explicou por que os dois são complementares e não redundantes. Sem isso a PR pareceria duplicar um gate que já existia.
2026-09-07 09:00:37 -03:00
Bob.Hou
f12b87c80b fix(claude): extra-usage switch does not skip 5h preflight (#12803)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

`blockExtraUsage: false` significa "pode usar crédito extra", nunca "esconda a conta antes de despachar" — a conta saía da rota justamente quando o crédito extra existia para ser usado. Os 32/32 cobrem os quatro pontos onde a mesma decisão era tomada, e revalidei após o merge da base (32/32 de novo).

Nota de integração: o seu `isQuotaExhaustedForRequest` colidiu com o placeholder `_providerSpecificData` do #12789 na worktree combinada. Ficou a sua implementação, que é a que de fato usa o parâmetro. A base foi mergeada na branch para resolver o `file-size-baseline.json` (aditivo, JSON revalidado).
2026-09-07 08:59:39 -03:00
Bob.Hou
c042a51884 feat(grok-cli): show and redeem banked reset credits on Provider Limits (#12805)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

A decodificação dos campos aninhados 10/20/30 do `GetRemainingResets` ao vivo (último commit) é o que separa isto de um palpite sobre o formato do frame. Mostrar zero em vez de esconder a linha é a escolha certa: crédito zerado é informação, ausência de linha é ambiguidade.
2026-09-07 08:58:12 -03:00
Bob.Hou
e8a91173de fix(combos): inherit model_context_overrides onto effort-suffixed targets (#12475) (#12926)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

A busca do sufixo mais longo primeiro (`-xhigh` antes de `-high`) é o detalhe que faz a herança funcionar em vez de quase-funcionar. Manter `getResolvedModelContextOverride` fora do escopo, com o teste existente registrando que aquele caminho continua sem herança, deixa a fronteira explícita.
2026-09-07 08:57:27 -03:00
Bob.Hou
600abe68d0 fix(dashboard): moonshot voucher/cash leftover follows bucket balance (#12733)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

Available 0% com Voucher 100% e Cash 100% não é estado de carteira que exista — foi o sinal certo para puxar o fio. Tratar leftover como booleano só para Available e cravar 100% nos outros dois buckets é o tipo de defeito que passa despercebido enquanto a conta tem saldo.

Os dois testes cobrem os dois lados: o produtor e o caminho até `getQuotaRemainingPercentage` com `isCredits` + CNY.
2026-09-07 08:57:02 -03:00
Bob.Hou
d857bd053a fix(glm): drop extra 16th arg to SSE transform helper (#12770)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.

O `65536` era o 16º posicional de um helper com 15 parâmetros — `TS2554` vivo no tip (`open-sse/executors/glm.ts:244`, confirmado aqui antes do board). O teste de guarda de aridade é o que impede a reincidência: ele checa a assinatura do helper e o call site, não o comportamento, que é exatamente onde o erro morava.

Obrigado por isolar isso do #12711 em vez de deixar o `glm.ts` viajar junto com pin/combo-split/moonshot.
2026-09-07 08:55:45 -03:00
Soroush Ahmadi
2b2d34eb53 fix(cursor): guard non-array tool_calls in request translator (#12691) 2026-09-05 23:32:02 -03:00
tom
92a617c23f fix(sse): re-enable prompt compression for native Codex passthrough (#12834)
Native Codex passthrough (POST /v1/responses, provider=codex) was unconditionally
excluded from prompt compression, writing only skip_reason='excluded' analytics
rows. Prompt compression now depends only on the operator exclusions list;
reactive compaction and combo overflow fail-fast intentionally still bypass
(prompt-only scope).

Closes #12793

Regression guard: tests/unit/codex-prompt-compression-passthrough.test.ts
2026-09-05 23:30:48 -03:00
Diego Rodrigues de Sa e Souza
a9f7598c60 feat(db): fail-closed previous_response_id continuation for redacted video turns (#12150 P2b) (#12707)
Merged, with one column-reconciliation gap closed.

The fail-closed reasoning is right and the comments carry it well: a stored snapshot whose cues were replaced by `[redacted-video-transcript]` must not be rehydrated as continuation history, because forwarding placeholder text upstream as if it were the client's real turn is worse than making the client resend. Treating it exactly like `previous_response_not_found` means no new client-visible behaviour to document. Migration 173 does not collide — the tip runs to 172.

**What I added:** `video_content_removed` to `ensureCallLogsColumns` in `src/lib/db/schemaColumns.ts`, plus a case in `tests/unit/db-schema-columns-split.test.ts`.

`resolvePreviousResponseState` now SELECTs that column on every `previous_response_id` lookup. Migration 173 creates it, but this repo carries a separate reconciliation path for lineages that skipped a migration — and on such a database the SELECT would throw `no such column: video_content_removed` instead of failing closed. That is the same hole #12470 closed for `provider_connections.last_ping_at` earlier today, so the pattern was fresh. Verified red-then-green: stubbing the new reconciliation out drops the suite to 8/9; restored, 9/9.

Validated on `release/v3.8.51`: `responses-continuation-store`, `save-call-log-persistence`, `video-bridge-log-redaction` and `db-schema-columns-split` all green (54 focused tests, 0 failures). `typecheck:core` and `lint` clean. The integration run logs `[DB] Added call_logs.video_content_removed column`, which is the reconciliation firing on a fresh test database.
2026-09-05 03:15:25 -03:00
Diego Rodrigues de Sa e Souza
488f57e9d3 feat(catalog): eligibility-gated free-tier bucket (#12669)
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral

* fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages

* fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test

* docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts

* chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header

* feat(catalog): eligibilityGate field and gatedRecurringTokens total

* docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts

* feat(check): docs-counts gate validates the eligibility-gated free-tier figure

* fix(docs): budget card reads computeFreeModelTotals() instead of regex-parsing the catalog

* docs(free-tiers): retire the stale Gemini onboarding quota text

* feat(radar): carry eligibilityGate through the feed schema, the merge and the summary API

* feat(dashboard): show the eligibility-gated free-tier figure apart from the headline

* docs(free-tier): refresh catalog-entry counts to 442 after base sync

* feat(catalog): ModelScope as the first eligibility-gated pool; document the gated bucket and how we count

* docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts

* docs(free-tiers): correct the unsummed-catalog comparison figure to the current catalog

* docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream)

* docs(free-tiers): re-sync numbers after merging PR1 (Cerebras reclassified upstream)

* fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3)

* fix(catalog): keep eligibility-gated rows out of every headline-adjacent figure

The eligibility gate was honored by the steady headline and the pool count, but
three adjacent figures still counted gated rows: the credit reductions feeding
steadyWithRecurringCreditsTokens/firstMonthRealisticTokens, the uncappedProviders
list ("permanently free, no cap"), and the docs gate's free-forever provider set,
which was built from freeType alone.

- computeFreeModelTotals: filter !isGated in the recurring-credit, one-time-credit
  and uncapped predicates; gatedProviders semantics unchanged (steady rows only).
- check-docs-counts-sync: exclude eligibility-gated rows from the FOREVER set,
  which moves the live free-forever count 53 -> 52 (the base's value). README,
  promise-pillars.svg and FREE-TIERS-GUIDE re-synced.
- gen-budget-card-svg: skip gated one-time credits like the totals do, and fail
  loudly on `--out` without a path.
- Tests: gated one-time credit does not move firstMonthRealisticTokens; a gated
  uncapped row is not in uncappedProviders; shipped gated rows carry no credit
  tokens; the committed budget card is byte-identical to a fresh generation.

* test(catalog): allow eligibilityGate in the no-per-row-rating key allowlist

The allowlist landed on the base with #12318, after this branch's field was designed;
eligibilityGate says who may claim a quota, not how much a row can be trusted.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw24@gmail.com>
2026-09-04 00:45:38 -03:00
Markus Hartung
74c2d26393 fix(responses-continuation): chain off the effective post-reconstruction input, not the pre-reconstruction client bytes (#12641)
Validado em lote numa worktree combinada com os 3 PRs desta leva sobre o tip de `release/v3.8.51`: os três boardaram sem conflito, `typecheck:core` limpo e **22/22** nos arquivos de teste que trazem.

O crescimento de `src/sse/handlers/chat.ts` (2450 → 2454) é do #12641 e vai num PR de rebaseline próprio.

Obrigado, @hartmark.
2026-09-04 00:03:16 -03:00
Bob.Hou
2a6eff0aec fix(combo): keep Antigravity Gemini usable when Claude weekly is empty (#12637)
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.

Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.

Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.

Obrigado, @HouMinXi.
2026-09-03 23:47:28 -03:00
Bob.Hou
d36d077a4d fix(resilience): keep Overloaded STREAM_EARLY_EOF off the provider breaker (#12626)
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.

Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.

Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.

Obrigado, @HouMinXi.
2026-09-03 23:45:47 -03:00
Bob.Hou
36be267a17 fix(providers): add CLAUDE_CODE_CLIENT_VERSION and GITHUB_COPILOT_CLI_VERSION env overrides (#12632)
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.

Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.

Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.

Obrigado, @HouMinXi.
2026-09-03 23:43:38 -03:00
Bob.Hou
57d7c8bc88 fix(providers): sanitize boolean required and nested bare maps for Gemini (#12269) (#12624)
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.

Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.

Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.

Obrigado, @HouMinXi.
2026-09-03 23:42:40 -03:00
Ravi Tharuma
6aa3690dea feat(providers): filter GitHub combo members against live catalog (#12473)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:39:43 -03:00
Ravi Tharuma
85b8d128eb fix(auth): do not park healthy quota accounts as expired (#12452)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:39:22 -03:00
Ravi Tharuma
8c1dfc416d fix(combo): do not treat credits-exhausted 401 as auth skip (#12449)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:38:57 -03:00
Ravi Tharuma
3d2bcc9f12 feat(api): emit gateway-measured tokens-per-second excluding TTFT (#12631)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:38:37 -03:00
Ravi Tharuma
bb8e75a00d fix(resilience): surface Responses failed.error.message in 502s (#12472)
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.

A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.

O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.

Obrigado, @RaviTharuma.
2026-09-03 23:37:47 -03:00
Diego Rodrigues de Sa e Souza
16b0d4e3ad fix(catalog): re-audit free-tier quotas against official pages (#12649)
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral

* fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages

* fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test

* docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts

* chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header

* docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts

* docs(free-tiers): retire the stale Gemini onboarding quota text

* docs(free-tier): refresh catalog-entry counts to 442 after base sync

* docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts

* docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream)

* fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-09-03 23:26:32 -03:00
Diego Rodrigues de Sa e Souza
2265ce761f fix(security): harden public error boundaries (#12506)
Validado sobre o tip de `release/v3.8.51` depois de reconciliar com o #12620, que entrou primeiro nesta mesma sessão e ataca a mesma classe de problema por outra arquitetura.

**A colisão e como foi resolvida.** O #12620 consertou o GHSA-qv45-56jc-4wmj adicionando `RAW_CREDENTIAL_PATTERNS` a `error.ts` e importando-os em `upstreamErrorPassthrough.ts`. Este PR resolve o mesmo problema quebrando `error.ts` em `errorSanitization.ts` + `errorPathRedaction.ts`. Mantive a divisão em módulos deste PR, porque ao comparar os dois vocabulários o dele já era mais amplo: o `STRONG_CREDENTIAL_TOKEN` daqui cobre `sk-`/`sk_` **com lookbehind e uma variante para a forma embutida** (que pega `sk-proj-…`), mais Slack `xox-`, AWS `AKIA`/`ASIA`, `github_pat_`/`ghp_`/`glpat-` e JWT de três segmentos.

A única forma que o #12620 carregava e este conjunto não tinha era a chave do Google (`AIza…`) — adicionada aqui, com o mesmo quantificador limitado que os irmãos usam (AGENTS.md → PII §1, já que isso roda sobre corpos upstream não confiáveis).

**A verificação não foi por inspeção.** Rodei as suítes do próprio #12620 contra esta estrutura: **48/48** em `error-sanitizer-sk-key-qv45`, `bifrost-relay-response-leak-9m72`, `search-baseurl-client-override-3f8g` e `search-baseurl-ssrf-guard` — incluindo a asserção anti-drift daquela suíte, que é o oráculo certo aqui: *para todo corpo que a camada de passthrough recusa como vazante, o sanitizador de fallback não pode devolvê-lo intacto*. Ela passa, então a propriedade de segurança dos três GHSAs sobrevive à troca de arquitetura.

Os 21 arquivos de teste deste PR: **259/259**. `typecheck:core` limpo.
2026-09-03 21:31:13 -03:00
Bob.Hou
109cf0f26c fix(providers): reclassify Cerebras as a one-time $5 signup credit (#12591)
Validado sobre o tip de release/v3.8.51 — e o PR ficou completo depois que o autor adicionou a tabela de preços.

O que fazia o teste falhar antes não era a lista free (que o PR já tinha corrigido em `LEGACY_FREE_PROVIDERS` e `tierDefaults.json`), e sim que `classifyTier` cai no ramo cost-based e todos os modelos Cerebras estavam declarados com `input: 0, output: 0` — $0/M ≤ threshold devolve 'free' de qualquer jeito. A tabela de preços resolve isso na raiz.

Confirmei o `gpt-oss-120b` a $0,35/$0,75 de forma independente contra a fonte pública, o que corrobora o resto da tabela. **4/4** no teste-guarda e **25/25** somando free-tier-catalog e free-models; typecheck:core limpo. Obrigado, @HouMinXi.
2026-09-03 21:30:32 -03:00
Diego Rodrigues de Sa e Souza
350ac8c12d fix(sse): preserve 1min.ai partial output before stream errors (#12466)
Validado após reconciliar com o #12465, que entrou primeiro e criou o mesmo arquivo novo `open-sse/utils/streamReadiness.ts` com desenho divergente de cancelamento.

Mantive a versão desta branch, que defere o release do lock para quando a leitura em voo termina e faz `reader.cancel()` fire-and-forget — assim uma promise de provider que nunca resolve não torna o cancelamento ilimitado. A escolha não foi por preferência: rodei as suítes dos **dois** PRs contra ela, 21/21 no readiness compartilhado e **22/22** incluindo o boundary do Perplexity do próprio #12465. typecheck:core limpo.
2026-09-03 21:06:28 -03:00
Diego Rodrigues de Sa e Souza
627fcba605 fix(sse): preserve Perplexity stream failures (#12465)
Validado em lote numa worktree combinada com os 14 PRs desta campanha de error-boundary sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **120/120** nos 23 arquivos de teste que os PRs trazem.

Um ponto que só apareceu no tree combinado: **#12465 e #12466 criam o mesmo arquivo novo** `open-sse/utils/streamReadiness.ts` (que não existe no tip) com desenhos divergentes de cancelamento — `cancelled` + `releaseLock` imediato num, `readInFlight`/`cancelRequested` com `cancelReader` fire-and-forget no outro. Adotei a versão do #12466, que difere e defere o release do lock para quando a leitura em voo termina, e validei a escolha rodando as suítes dos **dois** PRs contra ela: 21/21 no readiness compartilhado e 22/22 incluindo o boundary do Perplexity.
2026-09-03 21:00:14 -03:00