Compare commits

..

83 Commits

Author SHA1 Message Date
Markus Hartung
bed4d24049 fix(config): exclude cookie-auth image bridges from unprefixed model scan (#10848) 2026-08-20 20:33:19 -03:00
MSiva
bc9090ba65 fix(translator): merge consecutive same-role contents in direct claudeToGeminiRequest (#10658)
Obrigado — bug real: a tradução direta claudeToGeminiRequest emitia mensagens consecutivas do mesmo role em contents[], o que a API do Gemini rejeita com HTTP 400 (turnos alternados user/model são obrigatórios). Traz claudeToGeminiRequest à paridade com openaiToGeminiRequest reutilizando mergeConsecutiveSameRoleContents.

Validação (worktree combinado a partir de origin/release/v3.8.50, merge limpo, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/claude-to-gemini-consecutive-roles.test.ts — 7/7 passando
- tests/unit/claude-to-gemini-budget-tokens-zero-6813.test.ts — 2/2 passando (sem regressão)
2026-08-20 20:04:20 -03:00
Paco Cartones
d9cb4f5f5d fix(files): validate the list limit query parameter (#10673)
Obrigado — bug real: GET /v1/files aceitava limit negativo sem validação (`-5 || 20` avalia truthy em -5, então Math.min(-5, 10000) = -5 passava direto). Agora valida integer/positivo/tamanho e retorna 400 estruturado para valores inválidos, preservando o default 20 e o máximo 10.000.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/integration/files-api-limit-validation.test.ts — 5/5 passando
- tests/integration/files-api.test.ts — 12/12 passando (sem regressão)
- tests/unit/batch_api.test.ts teve 1 falha, confirmada DRIFT pré-existente idêntica no tip puro do release (não relacionada, timing de cancelamento de batch)
2026-08-20 19:56:34 -03:00
Diego Rodrigues de Sa e Souza
ce6249cbb7 Merge pull request #10528 from excessivechaos/fix/direct-dispatcher-timeout-10214
fix(network): bound direct-path response-start timeout and retry on fresh socket (#10214)
2026-08-20 19:55:57 -03:00
Diego Rodrigues de Sa e Souza
9935f80971 fix(perplexity-web): make the built-in-search hint opt-in (#10904)
Merged — extraction of the one still-uncovered fix from #8634 (the other two items — mode "search"→CONCISE downgrade, pplx-opus generation — were already applied on this release tip). typecheck/file-size/changelog/complexity/cognitive-complexity gates all clean, 32/32 tests passing.
2026-08-20 19:34:18 -03:00
Diego Rodrigues de Sa e Souza
e968d11b1c feat(home): add Recent Requests panel + excludeTests allowlist fix (#10900)
Merged — reimplementation extracting the non-conflicting Recent Requests panel + excludeTests allowlist fix from #8450 (see PR body for the full scoping rationale, including why the topology UX rework was deliberately excluded — it contradicts the already-shipped #8428). typecheck/file-size/changelog/complexity/cognitive-complexity/i18n-coverage gates all clean, 2/2 unit + 1/1 vitest passing.
2026-08-20 18:26:02 -03:00
Diego Rodrigues de Sa e Souza
7afafcecc9 feat(sse): add GLM-5.3 models and effort tiers (#10896)
Merged — clean extraction from #10358's genuinely new content (see PR body for the rationale: an unrelated .planning/codebase/ scaffolding dump was dropped). typecheck/file-size/changelog/provider-consistency gates clean, 18/18 tests passing.
2026-08-20 18:06:41 -03:00
Tiangao
c79faa45fb fix(image): support OpenRouter reference-image edits (#10197) (#10363)
Obrigado — feature real e bem verificada: POST /v1/images/edits rejeitava o provider built-in openrouter mesmo ele suportando edição por imagem de referência via sua Image API unificada. Traduz a imagem de entrada para o formato input_references documentado do OpenRouter e despacha para /api/v1/images, removendo o prefixo do provider do model id antes de encaminhar.

Nota: o contribuidor não conseguiu rodar o teste localmente (better-sqlite3 ausente no ambiente dele) — rodei aqui.

Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/10197-openrouter-image-edits-route.test.ts — 3/3 passando (forward bem-sucedido, credenciais ausentes 401, rate-limit)
2026-08-20 18:04:22 -03:00
Diego Rodrigues de Sa e Souza
871832820f fix(memory): enable agent memory save via MCP tools + builtin stream guard (#10887)
Merged — clean single-commit extraction from #9115's genuinely new content (see PR body for the full extraction rationale: 66-commit branch, only 1 commit matched the stated scope). typecheck/file-size/changelog gates clean, 19/19 unit + 14/14 integration tests passing.
2026-08-20 17:28:37 -03:00
3g0r1ch
d87b97a786 feat(routing): adaptive feedback loop v2 — operational/semantic quality, confidence, TTFT/ITL, end-to-end test (#10881)
Obrigado — feature substancial e bem estruturada: separa qualidade operacional (comportamento de wire: 4xx/5xx, 429, respostas malformadas, stream interrompido) de qualidade semântica (só setada por avaliadores externos, nunca inferida do sucesso HTTP), com confidence/sample-awareness para não deixar poucos sucessos de sorte dominarem o ranking. Instrumentação de streaming (TTFT/ITL) threaded até RoutingEvent, endpoint de explicabilidade, e teste E2E determinístico cobrindo degradação→recuperação→blip.

Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- 59/59 testes passando (mlx-provider, routing-adaptive-e2e, routing-events(-concurrency), routing-otel, routing-quality, routing-scoring-quality, stream-timing, auto-combo-scoring-clamp)
2026-08-20 17:28:30 -03:00
Rouzbeh†
25ba4f2a34 feat(antigravity): auto-rotate BYOP accounts to siblings on GCP_PROJECT_REQUIRED (#10470)
Obrigado — follow-up bem feito do #10424: um 422 GCP_PROJECT_REQUIRED (BYOP — a conta Google precisa trazer seu próprio GCP Project) é específico da CONTA, não do provider inteiro, então o fast-fail anterior falhava mesmo quando uma conta antigravity irmã saudável poderia atender o request. Agora rotaciona automaticamente para a conta irmã (excluindo a conta BYOP por 24h) e só surfaça o erro acionável quando não há irmã disponível. Estado de rotação rastreado separado de maxAttempts, então falhas normais do antigravity nunca ganham uma segunda chance (sem dispatch duplo).

Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo — auto-merge em chatCore.ts, 0 conflitos reais):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/antigravity-byop-account-rotation.test.ts + error-classifier.test.ts — 36/36 passando
2026-08-20 17:00:43 -03:00
Aman
c40ff16a1d fix(providers): preserve health on inconclusive probes (#10799)
Obrigado — distinção precisa entre saúde de credencial e disponibilidade upstream: timeout do NVIDIA e HTTP 400 genérico do Antigravity/AGY passavam a poluir a saúde da credencial como se fossem falha de autenticação, quando na verdade são resultados inconclusivos. Preserva o path explícito de geo-block do Google (esse continua indo pelo tratamento de geo/egress existente). Reconciliado nesta sessão contra o release tip atualizado (pós #10878/#10873) — conflito real em scheduler.ts resolvido de forma additiva (pacing por intervalo do release + recheck mais lento para probes inconclusivos empilhados).

Validação (reconciliação a partir de origin/release/v3.8.50, gates estáticos rebaselineados para a soma legítima de #10878+#10799 no test/route.ts):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/provider-health-inconclusive-probes.test.ts + nvidia-nim-validator.test.ts + antigravity-geoblock-resilience.test.ts — 22/22 passando
2026-08-20 16:42:02 -03:00
Aman
0b51a242ce fix(provider-health): keep unsupported validation probes neutral (#10878)
Obrigado — corrige um caso real onde um 404/405 no chat-probe de validação genérica OpenAI-like era tratado como credencial inválida, quando na verdade significa apenas que o provider não expõe essa superfície de validação. Agora o validador retorna `unsupported: true`, a rota de teste responde `skipped: true`, a saúde persistida não é reescrita, o cache de CredentialHealth não é poluído, e o scheduler ainda respeita o healthCheckInterval configurado — sem introduzir exceção específica de provider nem tocar comportamento do MiMoCode.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/provider-validation-unsupported-neutral.test.ts — passando (TDD RED→GREEN completo: classificação do validador, não-mutação de saúde persistida, pacing do scheduler, regressão de lease-skip, comportamento existente de 403/429 preservado)
2026-08-20 16:35:51 -03:00
Aman
7f90af645c fix(db): remove stale MiMoCode state after provider sunset (#10873)
Obrigado — follow-up limpo do #10186: MiMoCode foi removido do OmniRoute, mas instalações que o configuraram antes da remoção retêm estado provider-scoped órfão (provider_connections, registered_keys, provider_key_limits, discovery_results, customModels). Migração de retirement segue o padrão explícito já usado para outros providers aposentados, preservando corretamente usage_history/call_logs históricos.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/migration-159-remove-mimocode-provider.test.ts — passando (mimocode + alias mcode removidos, estado de outros providers preservado, idempotência, histórico preservado)
2026-08-20 16:35:41 -03:00
Dizzle
e6801bace1 feat(proxy): surface anonymous egress-IP sharing in the health sweep and the egress API (#10876)
Obrigado — expõe onde o operador realmente olha (sweep periódico de saúde de proxy + resposta da API de egress) o sinal de compartilhamento anônimo de IP de egress entre contas de um mesmo rotation group, que já existia (analyzeEgressSharing) mas só era acessível via curl autenticado. Fecha #10677. Respeita a decisão de redação do #10348/#10539: apenas contagens por padrão, nenhum IP/identidade de conta a menos que PROXY_LOG_INCLUDE_IPS=true.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/proxy-egress-route-summary.test.ts + proxy-egress-summary.test.ts + proxy-health-egress-line.test.ts — 23/23 passando (agregado, formatter, linha do sweep com output real capturado, rota completa com auth de management)
2026-08-20 15:47:34 -03:00
Dizzle
82ed31d27a docs(openapi): document GET and PUT on /api/combos/[id] (#10875)
Obrigado — TDD exemplar num gap real de contrato: as duas operações que o dashboard realmente chama em /api/combos/{id} (GET e PUT) estavam ausentes do openapi.yaml, enquanto a única operação documentada (patch, antes deste #10869) não tinha handler. Adiciona um floor de cobertura por OPERAÇÃO (não só por PATH) que o gate existente não capturava, medido em 343/985 (34.8%).

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/openapi-coverage.test.ts — passando com o novo floor de operações
- openapi-routes/openapi-coverage/openapi-security-tiers gates — PASS
2026-08-20 15:47:24 -03:00
Dizzle
dacf4c3c1a fix(cli): report .env lines that never take effect (#10870)
Obrigado — resolve o cenário real do #6194: uma linha de .env que o shell já tinha exportado antes (ex.: HOSTNAME=0.0.0.0) era silenciosamente ignorada pelo loader first-wins, sem nenhum aviso — o servidor bindava no hostname da máquina, localhost parava de responder, e ModelSync/health checks falhavam com ECONNREFUSED sem pista nenhuma. Agora cada chave mascarada emite um warning em stderr (nome da chave + as duas origens, nunca o valor); um .env ilegível também vira warning em vez de falha silenciosa no boot.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/cli-env-collision.test.ts — 4/4 passando (3 falham no base)
- Suítes CLI env vizinhas (cli-data-dir-env-loading, cli-env-inline-comment-10100, cli-data-dir-env, cli-entrypoint, cli-electron-to-cli-migration-server-env-7302, cli-storage-key-bootstrap) — intactas e verdes
2026-08-20 15:47:15 -03:00
Dizzle
362c5acbfe feat(api): accept PATCH on /api/combos/[id] (#10869)
Obrigado — bug real de contrato: o openapi.yaml já documentava patch em /api/combos/{id}, mas a rota nunca exportou PATCH, então um cliente gerado a partir do spec publicado recebia 405 do App Router antes de qualquer handler rodar. Fix mínimo (delegação de 4 linhas para PUT, mesmo padrão já usado em /api/providers/[id] e 25 outras rotas /api/**).

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/combo-patch-verb.test.ts — 2/2 passando (falham antes com "comboRoute.PATCH is not a function")
- Suíte combo completa — mesmas 5 falhas herdadas de #9985, confirmadas DRIFT
2026-08-20 15:47:05 -03:00
Dizzle
7fd82eb146 fix(api): guard combo updates against emptying, and fix copilot combo targets (#10866)
Obrigado — bug real e bem raiz-causado, dois bugs da mesma família: (1) PUT /api/combos/<id> com models:[] zerava os targets sem aviso, quebrando um combo funcionando (o invariante "combo tem ≥1 modelo" já era reforçado por ~9 consumidores downstream, menos o write path); (2) as tools do Copilot escreviam em targets em vez de models, então todo combo criado via Copilot reportava sucesso mas roteava para lugar nenhum.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/combo-empty-models.test.ts — 4/4 passando
- Suíte combo completa (tests/unit/combo*.test.ts) — 1213 testes, 1208 passando, 5 falhas idênticas em ambos os lados (confirmadas DRIFT pré-existente do #9985 via probe contra o tip puro do release)
2026-08-20 15:46:57 -03:00
Diego Rodrigues de Sa e Souza
6f28688b04 fix(antigravity): map Gemini 3.7 Flash tiers to upstream tiered endpoint model (#10341) (#10882)
Merged — clean single-commit cherry-pick extracted from #10879's genuinely new content (see PR body for the full extraction rationale). typecheck/file-size/changelog gates clean, 11/11 tests passing.
2026-08-20 15:37:10 -03:00
adevwithpurpose
12d0acbe06 fix(conversations): bound reconnect walk + memoize turn hashes (#7847) (#10800)
Obrigado — bug de produção real e muito bem raiz-causado: resolveConversationId travava a request path por 10-130s em históricos longos de agente (medido em produção: p50 12.6s / max 130.2s em requests com ≥200 mensagens), por re-hashear o texto completo de cada turno a cada passo do walk de reconexão (O(starts × anchors × walkLength) HMACs síncronos).

Fix cirúrgico: memoiza o hash de cada turno por request + budget de passos compartilhado entre candidatos (degrada como no-match, nunca como attach não verificado ou latência ilimitada). Resultado medido: 17.2s → 0.3s no repro.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/conversationTracker-reconnect-7847.test.ts (novo) — cobre o cap de budget, degradação com budget zero, e guarda de regressão de wall-clock (falha em 17.2s pré-fix)
- 17 testes de semântica pré-existentes + conversationTurnContent (5) — passando sem alteração
2026-08-20 15:34:32 -03:00
adevwithpurpose
61051a1460 perf(compression): accelerate Lite and Caveman whitespace & artifact cleaners with native V8 RegExp (#10834)
Obrigado — ganho de performance real e bem medido: substitui loops char-a-char por RegExp nativa do V8 em cleanupArtifacts/normalizeMessageWhitespace, 53-296x mais rápido nos payloads testados (830KB: 109ms→1.6ms; 3.36MB: 296ms→5.4ms), com paridade byte-a-byte confirmada.

Durante a validação do lote combinado encontramos uma regressão real: o rewrite removeu isCodeDominantText (a guarda do #9144) e seu ponto de decisão em cavemanCompress, reintroduzindo a recapitalização destrutiva de código não-cercado (function→Function). Identificado por tests/unit/compression/caveman-file-reference-9144.test.ts, que passa no tip puro do release e falhava após este PR. Restaurei a guarda em cima da nova implementação regex (commit e5edb1a6, autoria preservada + Co-authored-by), mantendo o ganho de performance sem reintroduzir o bug.

Validação final (worktree combinado a partir de origin/release/v3.8.50):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/compression/lite.test.ts + caveman-*.test.ts — 99/99 passando (incluindo o #9144 restaurado)
2026-08-20 15:34:20 -03:00
adevwithpurpose
1fb466a1ee fix(cli): prevent DEP0190 child process spawn deprecation on Windows (#10835)
Obrigado — silencia o DeprecationWarning DEP0190 do Node 22+ ao invocar wrappers .cmd/.bat no Windows via shell:true, usando windowsVerbatimArguments/windowsHide em vez da stringificação legada não segura.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- Suítes de cobertura existentes de tool-detector (cli-helper-tool-detector-paths-6162, cli-tool-detector-imports, tool-detector-win32-7279, tool-detector, tool-detector-opencode-jsonc-10227) — 23/23 passando
2026-08-20 15:34:08 -03:00
adevwithpurpose
2c84ce19df fix(build): ensure standalone package.json declares module type for Node 24 worker compatibility (#10836)
Obrigado — corrige um warning real de runtime em produção sob Node.js 24: o package.json do standalone gerado pelo Next.js não declara "type": "module", forçando reparse de todo worker thread ESM (callLogArtifactWorker, onnxWorker) a cada spawn.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- Confirmado que colocate-standalone.mjs escreve "type": "module" corretamente; testado sob Node 24.19.0, workers sobem sem warning de reparse
2026-08-20 15:34:00 -03:00
Nguyen Thanh Dat
e5b7c40d11 fix(security): judge outbound hosts by address, not by spelling (#10843)
Obrigado — fix de segurança real (reportado via GHSA-qcfj-c39q-88jh): isCloudMetadataHost() decidia por spelling dotted-decimal, então um literal IPv4-mapped IPv6 (ex.: [::ffff:169.254.169.254]) alcançava o guard já canonicalizado por new URL() e não era reconhecido como endpoint de metadata de cloud — bypass no modo que permite endpoints privados/LAN (o default local-first). Também fecha o gap equivalente de 0.0.0.0/::.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/outbound-guard-mapped-ipv4.test.ts — 12/12 passando (IMDS, Alibaba, ECS task role, ambas as grafias, hosts públicos, guard `::`)
- Suítes SSRF relacionadas (webhook/firecrawl/kiro/provider-validation) — verdes
2026-08-20 15:20:29 -03:00
Nguyen Thanh Dat
053c64d380 fix(i18n): stop rendering the Disabled status as "person with a disability" (#10853)
Obrigado — corrige um erro sério de tradução em 8 locales, onde o status "Disabled" era traduzido pelo substantivo "pessoa com deficiência" (24 strings), estendendo o escopo original do #10812 (só japonês) para todos os locales afetados. Cada substituição usa o termo que o próprio catálogo já emprega para a mesma fonte em inglês — nenhuma terminologia nova introduzida.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/i18n-disabled-not-person-with-disability.test.ts — 1/1 passando (varre todos os locales)
- Glossary gates (zh-CN, zh-TW, ko) — PASS; i18n:check-ui-coverage e check-value-drift — PASS
- Duas camadas de proteção contra regressão: glossário (zh-CN/zh-TW) + teste catalog-wide cobrindo 21 termos
2026-08-20 15:20:19 -03:00
Nguyen Thanh Dat
3112304db6 fix(catalog): stop advertising auto/* when auto routing is disabled (#10857)
Obrigado — /v1/models continuava anunciando 38 IDs auto/* mesmo com autoRoutingEnabled: false, todos garantidos a falhar em tempo de request (HTTP 400). Une a condição de ocultação ao hideAutoCombos já existente sem adicionar uma dimensão nova à cache-key (evita quebrar #10313).

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/catalog-auto-routing-disabled-10831.test.ts — 2/2 passando
- Suítes catalog relacionadas (hide-auto-no-think, cache-key-hashing, eventloop-yield) — 9/9 passando
2026-08-20 15:20:08 -03:00
Nguyen Thanh Dat
a280bfc112 fix(context): budget base64 file payloads instead of counting them as text (#10858)
Obrigado — um PDF de ~1MB enviado como file/document base64 (OpenAI ou Claude) era medido caractere-a-caractere, estimando 350.022 tokens (o mesmo documento pelo path Gemini inlineData já estimava 1.209). Corrige a inconsistência reconhecendo os shapes que faltavam, sem introduzir constante nova.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/10840-file-token-context.test.ts — 5/5 passando
- Suítes de contexto relacionadas — 59/59 (5 arquivos) passando
2026-08-20 15:19:59 -03:00
Nguyen Thanh Dat
d99701d6b3 fix(mcp): give provider-bound tool calls their own fetch budget (#10860)
Obrigado — o hop de routing (route_request) herdava o budget de 10s de management em vez do budget de 60s de upstream que web_search/web_fetch já usavam, então uma rota de 35-40s abortava só pelo lado do MCP.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/mcp-upstream-fetch-timeout-9717.test.ts — 6/6 passando
- Suíte MCP completa — 149/153 (branch) vs 143/147 (release), as 4 falhas são idênticas nos dois lados e não relacionadas (closure de package-files, resolução de bundle dist/)
2026-08-20 15:19:50 -03:00
Nguyen Thanh Dat
80b517edea fix(providers): treat a degraded cached catalog as a failed model sync (#10862)
Obrigado — root cause preciso: a rota sync-models só reconhecia a degradação para local_catalog, não para o fallback de cache com warning, então uma chave expirada (401) virava silenciosamente "Nenhum modelo novo foi adicionado" em vez de um erro visível.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/sync-models-degraded-cached-catalog-9683.test.ts — 6/6 passando (payloads reais do models/route.ts)
- Suítes model-sync/provider-models/sync-models/siliconflow — 181/181 passando, incluindo as 3 asserções pré-existentes #5460/#5465
2026-08-20 15:19:41 -03:00
Nguyen Thanh Dat
a72dc25c04 fix(proxy): stop reporting IPv4-only proxies as dead (#10868)
Obrigado — bug real e bem raiz-causado: api64.ipify.org é IPv6-first e derruba tunnels IPv4-only, o que estava reportando proxies vivos como mortos.

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity 2563/2774, cognitive-complexity 1155/1223 (baseline)
- tests/unit/proxy-echo-ipv4-fallback-9694.test.ts — 8/8 passando (cobre ordem, split de budget, override, proxy morto de verdade)
- Suítes proxy-relacionadas: 805/817 na branch vs 797/809 no release, as 11 falhas são idênticas em ambos os lados e não relacionadas (TLS transport, tproxy CA, SSRF fallback)
2026-08-20 15:19:32 -03:00
小妍儿 ✨
621f30a188 fix(cli): restore packaged machine-token authentication (#10468)
Obrigado por restaurar e endurecer a autenticação por machine-token no CLI empacotado.

Validação (worktree combinado a partir de origin/release/v3.8.50, merge limpo, 0 conflitos — 34 arquivos, +1078/-247):

- `npm run typecheck:core` — limpo
- `node scripts/check/check-complexity.mjs` — OK (2558 violações vs baseline 2774)
- `node scripts/check/check-cognitive-complexity.mjs` — OK (1152 violações vs baseline 1223)
- `node scripts/check/check-file-size.mjs` — OK
- `node scripts/check/check-changelog-integrity.mjs` — OK
- Testes focados (8 arquivos: cli-doctor-command, cli-machine-token, lib/machineToken, lib/managementCliToken, agentSkills-generator, api/settings-audit, check-pack-boot, next-config) — 95/95 passando

Os dois achados de segurança do maintainer-feedback original (checagem de loopback tipo SSRF, escopo de cookie/CSRF) já estavam corrigidos e cobertos por teste no commit `2b785f0068a862fbd867221294325ad921787782` desta branch.
2026-08-20 15:07:19 -03:00
Bob.Hou
ff9a4c2fbd fix(combo): prevent unhandledRejection from per-model-timeout abort (#10846)
Merged — locally validated (11/11 focused tests across both new test files, typecheck:core clean after a 1-char fix pushed to this branch: ComboLogger.error is optional in combo/types.ts so the defensive race-catch needed log.error?.(...) — TS2722 otherwise). Solid production diagnosis (47 unhandledRejections traced to the orphaned race loser). Thanks!
2026-08-20 13:22:23 -03:00
Bob.Hou
f52fa9dc85 fix(embeddings): cool down account on hard errors (402/401/5xx) (#10529)
Merged — locally validated (focused embedding-cooldown tests green, gates green). Good parity with the chat path's existing pattern. Thanks!
2026-08-20 13:21:11 -03:00
Bob.Hou
424b950856 fix(api): classify OAuth probe timeout as network_error (#10663)
Merged — locally validated (focused tests green, gates green). Clean, minimal classification fix. Thanks!
2026-08-20 13:21:07 -03:00
Bob.Hou
01b3828278 fix(sse): strip corrupted request_id from upstream SSE responses (#10223) (#10666)
Merged — locally validated together with related HouMinXi PRs (22/22 focused tests, gates green). Note: the PR description text looks pasted from a different change — the actual diff (corrupted request_id strip, #10223) is what was reviewed and merged. Thanks!
2026-08-20 13:21:03 -03:00
stanley
998c3c2129 fix(pricing): DeepSeek V4 static defaults stale by 4 days, off by ~1.6-2.4x (#10635)
Merged — locally validated (28/28 focused pricing tests, gates green). Appreciate the conservative off-peak-only scope and the live verification against the pricing page. Thanks!
2026-08-20 13:11:07 -03:00
stanley
b59a88b7eb fix(pricing): 3 dead entries in LITELLM_PROVIDER_MAP silently drop synced pricing (#10636)
Merged — locally validated (28/28 focused pricing-sync tests, gates green). Excellent systematic audit of the whole alias map, not just the one you hit. Thanks!
2026-08-20 13:11:03 -03:00
stanley
54b39690e5 fix(compression): bound RTK raw-output store growth and make pointer reads O(bucket) (#10659) (#10660)
Merged — locally validated together with related stanleytejakusuma PRs (typecheck:core clean, complexity/cognitive/file-size/changelog gates green, focused tests passing). Great incident writeup and clean fix. Thanks!
2026-08-20 13:10:58 -03:00
Aaron Scherer
22e46a0875 fix(sse): advance Claude cache breakpoints on growing tails (#10684)
Merged — locally validated (110/110 focused tests, typecheck:core clean, gates green) after resolving base-drift against #10683 (both landed today, same test file — combined). Nice, well-tested cache-breakpoint fix. Thanks!
2026-08-20 13:05:46 -03:00
Aaron Scherer
2a10d16114 test(chatcore): wait for queued call log writes (#10683)
Merged — locally validated (70/70 combined chatcore-translation-paths tests, typecheck:core clean, gates green). Thanks!
2026-08-20 13:00:34 -03:00
Aaron Scherer
06315c445c test(aihorde): guard client browser bundles (#10682)
Merged — locally validated (typecheck:core clean, gates green). Thanks for the extra regression coverage!
2026-08-20 13:00:05 -03:00
Aaron Scherer
31031f93ef fix(analytics): query auto routing from call logs (#10685)
Merged — locally validated together with related cryptiklemur PRs (typecheck:core clean, gates green). Good catch on the phantom usage_logs table. Thanks!
2026-08-20 13:00:01 -03:00
Aaron Scherer
c6a0d09bcd fix(combo): await quota token limit lookup (#10686)
Merged — locally validated together with related cryptiklemur PRs (typecheck:core clean, complexity/cognitive/file-size/changelog gates green, focused tests passing). Real bug, clean fix, great regression test. Thanks!
2026-08-20 12:59:57 -03:00
Diego Rodrigues de Sa e Souza
b052c91014 fix(fusion): apply vision-compatibility filter to fusion panel + judge (#10737)
Merged — locally validated (fusion-vision-panel-3378 test green, file-size/changelog gates green) after resolving base-drift against #10842/#10838 (both landed just before).
2026-08-20 12:10:38 -03:00
Diego Rodrigues de Sa e Souza
bbcfb730ca feat(sse): Cursor plan images via Agent CLI (IMAGE_PROVIDERS.cursor) (#10842)
Merged — locally validated (60/60 combined image-generation tests, typecheck:core clean, complexity/cognitive/file-size/changelog gates green, small rebaseline for the combined imageRegistry.ts growth).
2026-08-20 12:08:41 -03:00
Diego Rodrigues de Sa e Souza
db7c3abaf6 fix(images): retry Codex image generation on a sibling ChatGPT account (#10838)
Merged — locally validated (60/60 combined image-generation tests, typecheck:core clean, complexity/cognitive/file-size/changelog gates green).
2026-08-20 12:08:37 -03:00
Ravi Tharuma
567b9db04d test(ci): lock env-doc-sync ignore for ad-hoc BOT_TOKEN/BOT_URL
Drive runEnvDocSync so BOT_TOKEN/BOT_URL are ignored by IGNORE_FROM_CODE and flagged when ignore is empty.
2026-08-20 11:56:20 -03:00
Ravi Tharuma
8bc2f0f10c chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync
scripts/ad-hoc mesh helpers read operator-supplied BOT_TOKEN/BOT_URL.
They are not OmniRoute runtime config and should not fail Docs Gates
on every PR.

Unblocks check:env-doc-sync on release/v3.8.50.
2026-08-20 11:56:20 -03:00
Markus Hartung
8c4a219746 Revert "chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync (#10828)"
This reverts commit 7288fa0dd7.
2026-08-20 11:55:04 -03:00
Ravi Tharuma
7288fa0dd7 chore(ci): ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync (#10828)
Merged — carried forward the PR's own real value (the first 2 commits: ignore ad-hoc BOT_TOKEN/BOT_URL in env-doc-sync, plus the lock-in test). The branch had accumulated 7 more commits chasing the moving release tip across several rebases (each one re-fixing base-reds that had already moved again by the next rebase) — dropped those since they no longer apply to the current tip, and cherry-picked just the 2 with lasting value, preserving your authorship. 14/14 focused tests pass, changelog gate green. Thanks!
2026-08-20 11:53:38 -03:00
Ravi Tharuma
d14a4d2da1 docs(docker): clarify latest tracks published stable SemVer (#10816)
Merged — locally validated (changelog gate green) after resolving base-drift against #10817's SQLite HA section (both landed today, same insertion point in DOCKER_GUIDE.md — combined, both sections kept). Thanks!
2026-08-20 11:51:39 -03:00
Ravi Tharuma
2acafd9c9e feat(api): add GET /livez as a process-alive probe (#10819)
Merged — locally validated (3/3 focused tests, changelog gate green) after resolving base-drift against #10827's event-loop-lag doc note (both landed today, same MONITORING_GUIDE.md table cell — combined the /livez recommendation with the #10303 lag caveat). Thanks!
2026-08-20 11:49:51 -03:00
Ravi Tharuma
9d2240eab7 fix(search): name /v1/search 502 provider and cause (#10756)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:48:18 -03:00
Ravi Tharuma
9eddafff60 fix(admission): reserve Responses and Messages bodies before clone (#10814)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:48:14 -03:00
Ravi Tharuma
7c6bf32186 docs(docker): document SQLite single-replica HA limits (#10817)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:48:10 -03:00
Ravi Tharuma
56b9d00335 fix(docker): warn when OMNIROUTE_MEMORY_MB disagrees with NODE_OPTIONS heap (#10818)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:48:07 -03:00
Ravi Tharuma
2f7315882b fix(auto): log empty auto-family pools once per process (#10820)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:48:03 -03:00
Ravi Tharuma
77d75022d6 fix(opencode-plugin): keep bare combo ids unprefixed (#10821)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:58 -03:00
Ravi Tharuma
6f08a089e7 feat(speech): accept response_format=ogg as an opus alias (#10822)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:53 -03:00
Ravi Tharuma
8b52596d7c docs(auth): distinguish access tokens, API keys, and management credentials (#10823)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:50 -03:00
Ravi Tharuma
6767f27011 docs(db): document throttled pre-write SQLite backups (#10824)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:47 -03:00
Ravi Tharuma
7ac6bbba37 docs(backend): document memory/skills/token-refresh event-loop cost (#10825)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:43 -03:00
Ravi Tharuma
84d7e33c26 feat(resilience): warn on slow /healthz event-loop lag (#10827)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:38 -03:00
Ravi Tharuma
80a59c0ae5 fix(images): route bare dall-e-3 to OpenAI (#10847)
Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 11:47:34 -03:00
backryun
8122f6b71c perf(electron): optionally unload renderer on close (4/8) (#10328)
Merged — locally validated (73/73 focused electron tests, typecheck:core clean, gates green) after resolving base-drift against #10327 (both landed today, real interleaved logic in createWindow/showMainWindow/window-all-closed — combined so the hidden-start lazy-open path from #10327 and the unload-on-close path from this PR both stay intact; verified by the existing test 'keeps the non-macOS app alive when unloading its last renderer'). Nice pair of electron perf PRs, thanks!
2026-08-20 11:33:45 -03:00
backryun
5e508147c4 perf(electron): defer hidden-start renderer creation (#10327)
Merged — locally validated (6/6 focused lazy-window tests, plus the wider electron suite, typecheck:core clean, gates green). Thanks!
2026-08-20 11:32:08 -03:00
backryun
a352c23bad fix(chatgpt-web): refresh current model catalog (#10637)
Merged — locally validated (123/123 focused chatgpt-web tests, typecheck:core clean, file-size/changelog gates green). Thanks!
2026-08-20 11:31:48 -03:00
Dizzle
4821f9ffdb fix(proxy): probe a proxy's assigned provider host instead of a generic target (#10664)
Merged — locally validated (16/16 focused tests, typecheck:core clean, file-size/changelog gates green) after resolving base-drift against #10654/#10657 (both landed earlier today, same file chain — combined additively, no logical conflict). Nice chain of proxy-probe-target fixes, thanks!
2026-08-20 10:36:10 -03:00
Dizzle
f4772500bc fix(api): share one probe-target resolution between both proxy health checks (#10657)
Merged — locally validated (15/15 focused tests, typecheck:core clean, file-size/changelog gates green) after resolving base-drift against #10654 (both landed today, same file — combined import block, no logical conflict). Thanks!
2026-08-20 10:32:38 -03:00
Dizzle
b43ad73166 fix(api): a target refusing the egress IP is not a healthy proxy (#10654)
Merged — locally validated in a combined batch worktree with the related proxy-health-probe PRs (typecheck:core clean, gates green). Thanks!
2026-08-20 10:31:10 -03:00
Dizzle
bb98e9a345 fix(probe): isolate probe-origin failures from all deactivation sites (#10694)
Merged — locally validated (23/23 focused probe-isolation tests, typecheck:core clean, file-size/changelog gates green). Reconciled with today's #8367 (codexAccount module extraction, merged earlier): the persistCodexQuotaState closure this PR touched had been extracted into persistCodexChildQuotaResponse — applied the same probe-origin isolation guard (!shouldIsolateProbeFailures()) at its new call site instead of reintroducing the old inline closure. Thanks for closing this real gap!
2026-08-20 10:30:29 -03:00
Dizzle
ff8b7b172f chore(startup): remove server-init.ts, a module nothing imports (#10780)
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 10:27:26 -03:00
Dizzle
0bfaaa4929 feat(credential-health): per-connection sweep interval via healthCheckInterval (#8443) (#10687)
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 10:27:23 -03:00
Dizzle
4c15c05f9b feat(call_logs): persist per-call error family and expose analytics breakdown (issue #10670) (#10679)
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 10:27:18 -03:00
Dizzle
8cd248b4f5 feat(server): native systemd sd_notify watchdog (Type=notify) (#10662)
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 10:27:14 -03:00
Dizzle
05a37634c2 feat(db): report the SQLite driver and its durability on the DB health check (#10652)
Merged — validated together with a batch of related maxmad64bis PRs in one combined worktree (typecheck:core clean, complexity/cognitive-complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution!
2026-08-20 10:27:10 -03:00
Xiangzhe
62f6e87869 fix(responses): replace synthetic reasoning keepalive (#10806)
Merged — locally validated (61/61 focused tests: early-stream-keepalive, chat-body-admission, responses-parse-once-4041, responses-route-early-keepalive-wiring; file-size/changelog gates clean, merges conflict-free against the current release tip). Good catch replacing the synthetic reasoning placeholder with a real response.in_progress bookkeeping event — keeps event-level watchdogs (Codex etc.) happy without any replayable fake reasoning content. Thanks!
2026-08-20 09:49:55 -03:00
Xiangzhe
74c54828fc [defer] fix(routing): keep approximate context estimates advisory (#10162)
Merged — per owner decision on RFC #10141: gateway token estimates (chars/4) become advisory-only for combo routing/context checks, never a hard pre-dispatch 400. Locally validated (31/31 focused tests across capability filtering, target resolution, and the #8841 repro), file-size/changelog gates clean, merges conflict-free against the current release tip. Thanks for the well-scoped fix and for flagging this as an RFC first!
2026-08-20 09:49:12 -03:00
Xiangzhe
f060117464 [v3.8.50] refactor(codex): isolate virtual quota pools (#8367)
Merged — locally validated (30/30 focused tests: chatcore-codex-account-pool, codex-account-cooldown-write, codex-account-pool, providers-route-codex-account-pool, resilience-explain-codex-account, sse-auth-codex-account-pool; typecheck:core clean; file-size/complexity/cognitive-complexity/changelog gates all green). Merges clean against the current release tip with zero conflicts. Great refactor — extracting persistCodexQuotaState out of chatCore.ts into a proper codexAccount/ module with virtual quota pool isolation is a solid improvement. Thanks!
2026-08-20 09:46:13 -03:00
Xiangzhe
9fc3b29217 fix(catalog): scope combo reasoning efforts by connection (#10723)
Merged — locally validated (72/72 focused tests, typecheck:core clean, all static gates green) after resolving base-drift conflicts (catalog.ts cooperative-yield insertion point, modelMetadataRegistry.ts snapshot-param signature). CI's red checks (Unit Tests fast-path shards, Fast Quality Gates, Docs Gates) are confirmed PRE-EXISTING base-red on the pure release tip — reproduced tests/unit/db-driver-bundling-externals.test.ts, tests/unit/model-catalog-runtime-invalidation.test.ts and others failing identically against origin/release/v3.8.50 with zero PR content, unrelated to this change. Thanks for the design and for absorbing #10724's value here — great work on both review rounds!
2026-08-20 09:31:12 -03:00
Markus Hartung
e9dd87ad77 docs: sync provider count to 343 (was stale at 342)
Today's merge-train batch1 (#10722 Token Kiosk, #10729 Cursor) each added one
new APIKEY_PROVIDERS entry, bringing the live provider count to 343 — the
hardcoded '342' in README.md, AGENTS.md, llm.txt, package.json's description,
PROVIDER_REFERENCE.md, and 4 hero/comparison SVGs went stale as a result.
check:docs-counts (STRICT) now passes; regenerated PROVIDER_REFERENCE.md via
npm run gen:provider-reference. The v3.8.50 growth-log table row in README.md
(line 66) is left as-is — it's a historical point-in-time snapshot, not a
live claim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-20 09:25:45 -03:00
excessivechaos
142ae93498 fix(network): bound direct-path response-start timeout 2026-08-17 08:26:57 -07:00
491 changed files with 23347 additions and 3879 deletions

View File

@@ -229,6 +229,15 @@ PORT=20128
# unaffected by this dev-only flag).
OMNIROUTE_USE_TURBOPACK=1
# Disable systemd sd_notify (Type=notify / WatchdogSec=) even when running
# under a systemd unit with NOTIFY_SOCKET set.
# Used by: scripts/dev/systemd-notify.mjs. Set to 1 to disable.
# OMNIROUTE_DISABLE_SD_NOTIFY=1
# Injected by systemd when running under a service unit (sd_notify protocol).
# Read by scripts/dev/systemd-notify.mjs — never set this yourself.
# NOTIFY_SOCKET=/run/systemd/notify
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
# OMNIROUTE_SKIP_DB_HEALTHCHECK=1
@@ -882,7 +891,7 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# MODEL_SYNC_INTERVAL_HOURS=24
# Provider limits sync interval in minutes (rate limit windows, quotas).
# Used by: src/server-init.ts — polls provider health endpoints.
# Used by: src/lib/usage/providerLimits.ts — polls provider health endpoints.
# Default: 70
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
@@ -1352,6 +1361,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body
# FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s)
# FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s)
# OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS=30000 # Bounded response-start window per direct
# # (no-proxy) attempt (#10214). A silently-dropped
# # pooled keep-alive socket surfaces no transport
# # error, so without this bound a direct request can
# # stall until undici's headersTimeout (600s) or the
# # caller's deadline; on expiry the request retries
# # once on a fresh no-keep-alive socket. 0 disables
# # the bound (default: 30000 = 30s).
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
@@ -1406,6 +1423,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
# ── Perplexity web: built-in-search hint ──
# Used by: open-sse/executors/perplexity-web/protocol.ts — appends "You have
# built-in web search. Answer questions directly using search results." to the
# caller's system message. Off by default: Perplexity's answer engine searches
# anyway, and for coding clients the sentence leaks into replies as
# meta-commentary. Set to 1/true/yes/on to restore the old behavior.
# OMNIROUTE_PPLX_SEARCH_HINT=0
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
@@ -1979,6 +2004,16 @@ APP_LOG_TO_FILE=true
# Reachability probe target for the scheduler and the auto-test endpoint.
# Point it at an internal/self-hosted URL to avoid the public default.
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
# Probes started at once per batch, for the scheduler and the auto-test endpoint.
# Floored at 1 and capped at 50. Default: 10.
# PROXY_HEALTH_TEST_CONCURRENCY=10
# Delay in ms between two probe departures inside a batch. Without it the whole batch
# leaves at once and a shared egress IP can trip a rate-limited target. 0 disables the
# spacing; capped at 5000. Default: 100.
# PROXY_HEALTH_TEST_STAGGER_MS=100
# Set "false" to stop probing the real host of a proxy's assigned provider (GET /models,
# no API key) and always use the generic target above instead. Default: enabled.
# PROXY_HEALTH_USE_PROVIDER_TARGET=true
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.

View File

@@ -1294,10 +1294,15 @@ export function mapRawModelToModelV2(
// `(providerID, modelID)`. If the raw id is already provider-prefixed
// (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or
// `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave
// it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with
// the resolved `providerId` so a bare key like `claude-opus-4` parses as
// `(omniroute, claude-opus-4)` and the credentials resolve correctly.
id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`,
// it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids
// (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed:
// OpenCode looks up `-m <plugin>/<combo>` as model id `<combo>` under
// the plugin provider (#10345). Other bare ids still prefix with
// `providerId` so credentials resolve as `(omniroute, model)`.
id:
raw.id.includes("/") || raw.owned_by === "combo"
? raw.id
: `${ctx.providerId}/${raw.id}`,
/**
* Display name. Falls back to raw.id when no enrichment is available;
* the caller (`createOmniRouteProviderHook`) overlays

View File

@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mapRawModelToModelV2 } from "../src/index.ts";
test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => {
const combo = mapRawModelToModelV2(
{
id: "gpt-5.6-sol",
owned_by: "combo",
context_length: 272000,
max_output_tokens: 8192,
},
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(combo.id, "gpt-5.6-sol");
assert.equal(combo.providerID, "omniroute");
const slashed = mapRawModelToModelV2(
{
id: "cx/gpt-5.6-sol",
owned_by: "combo",
context_length: 272000,
},
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(slashed.id, "cx/gpt-5.6-sol");
const ordinary = mapRawModelToModelV2(
{ id: "claude-primary", context_length: 200000 },
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
);
assert.equal(ordinary.id, "omniroute/claude-primary");
});

View File

@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 342 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 343 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 342 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 342 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 343 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 343 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
@@ -101,7 +101,7 @@
<tr>
<td align="right"><b>⚙️ Features</b></td>
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
<td align="center"><a href="#-342-ai-providers--90-free">🌐 Providers</a></td>
<td align="center"><a href="#-343-ai-providers--90-free">🌐 Providers</a></td>
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI &amp; MCP</a></td>
</tr>
<tr>
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 342 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 342 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<br/>
<br/>
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 342 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 343 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<sub>📊 Full methodology &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -559,7 +559,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **342-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **343-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
<div align="center">
## 🌐 342 AI Providers — 90+ Free
## 🌐 343 AI Providers — 90+ Free
</div>
> The most complete catalog of any open-source router: **342 providers**, **90+ with a free tier**, **56 free forever**.
> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**.
<div align="center">
@@ -988,6 +988,8 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
```
`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
> branch. These mutable tags are intended only for testing unreleased fixes and

View File

@@ -52,6 +52,19 @@ function resolveUrl(path, opts) {
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
}
/** The machine-derived token is valid only for the local loopback server. */
export function isLoopbackUrl(value) {
try {
const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
if (hostname === "localhost" || hostname === "::1") return true;
if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true;
if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true;
return false;
} catch {
return false;
}
}
export async function buildHeaders(opts) {
const headers = new Headers(opts.headers || {});
if (!headers.has("accept")) headers.set("accept", "application/json");
@@ -87,10 +100,17 @@ export async function buildHeaders(opts) {
if (auth && !headers.has("authorization")) {
headers.set("authorization", `Bearer ${auth}`);
}
// Inject machine-id derived CLI token; env var override for testing.
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
headers.set(CLI_TOKEN_HEADER, cliToken);
// Inject the machine-derived credential only for an explicit local loopback
// destination. Remote contexts and absolute remote URLs use scoped access
// tokens and must never receive this machine-bound local credential.
const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts);
if (!isLoopbackUrl(destinationUrl)) {
headers.delete(CLI_TOKEN_HEADER);
} else {
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
headers.set(CLI_TOKEN_HEADER, cliToken);
}
}
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
headers.set("idempotency-key", opts.idempotencyKey);
@@ -195,8 +215,12 @@ function fetchOnce(url, init, timeoutMs) {
export async function apiFetch(path, opts = {}) {
const method = String(opts.method || "GET").toUpperCase();
const url = resolveUrl(path, opts);
const headers = await buildHeaders(opts);
const headers = await buildHeaders({ ...opts, destinationUrl: url });
const body = serializeBody(opts.body, headers);
// Undici preserves custom headers across cross-origin redirects. A local server
// redirect must never turn the loopback machine credential into an outbound
// secret, so fail redirects whenever this header is present.
const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect;
const timeout =
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
@@ -205,7 +229,7 @@ export async function apiFetch(path, opts = {}) {
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetchOnce(url, { method, headers, body }, timeout);
const res = await fetchOnce(url, { method, headers, body, redirect }, timeout);
if (res.ok) return enrichResponse(res, opts);
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
const delay = computeBackoff(attempt, res.headers.get("retry-after"));

View File

@@ -4,7 +4,9 @@ import os from "node:os";
import path from "node:path";
import { createDecipheriv, scryptSync } from "node:crypto";
import { fileURLToPath, pathToFileURL } from "node:url";
import { isLoopbackUrl } from "../api.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
@@ -378,11 +380,11 @@ function checkMemory() {
});
}
async function fetchWithTimeout(url) {
async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
try {
return await fetch(url, { signal: controller.signal });
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
@@ -471,6 +473,98 @@ async function checkServerLiveness(options = {}) {
);
}
export async function checkMachineTokenAuth(options = {}) {
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") {
return warn("CLI machine token", "CLI machine-token authentication is disabled", {
derived: false,
accepted: false,
disabled: true,
tokenExposed: false,
});
}
let url;
try {
const parsed = new URL(resolveLivenessUrl(options));
if (
!["http:", "https:"].includes(parsed.protocol) ||
parsed.username ||
parsed.password ||
!isLoopbackUrl(parsed.toString())
) {
return warn(
"CLI machine token",
"Machine-token probes are limited to HTTP(S) loopback endpoints",
{ derived: false, accepted: false, tokenExposed: false }
);
}
parsed.pathname = "/api/cli/whoami";
parsed.search = "";
parsed.hash = "";
url = parsed.toString();
} catch {
return warn("CLI machine token", "Could not resolve the management endpoint", {
derived: false,
accepted: false,
tokenExposed: false,
});
}
const token = await getCliToken();
if (!token) {
return fail(
"CLI machine token",
"Could not derive a machine token; verify the node-machine-id runtime is installed",
{ derived: false, accepted: false, tokenExposed: false }
);
}
try {
const response = await fetchWithTimeout(url, {
headers: { [CLI_TOKEN_HEADER]: token },
redirect: "error",
});
if (response.ok) {
return ok("CLI machine token", "Server accepted the local machine token", {
url,
status: response.status,
derived: true,
accepted: true,
tokenExposed: false,
});
}
if (response.status === 401 || response.status === 403) {
return warn(
"CLI machine token",
"Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect <host> --key <oma_live_...>`",
{
url,
status: response.status,
derived: true,
accepted: false,
containerBoundaryLikely: true,
tokenExposed: false,
}
);
}
return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, {
url,
status: response.status,
derived: true,
accepted: false,
tokenExposed: false,
});
} catch {
return warn("CLI machine token", "Machine-token endpoint could not be reached", {
url,
status: 0,
derived: true,
accepted: false,
tokenExposed: false,
});
}
}
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
@@ -488,6 +582,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
if (!options.skipLiveness) {
checks.push(await checkServerLiveness(options));
checks.push(await checkMachineTokenAuth(options));
}
// CLI tool health checks

View File

@@ -121,7 +121,16 @@ function writeLinuxSystemdUnit(cliPath) {
"Wants=network-online.target",
"",
"[Service]",
"Type=simple",
// Type=notify + WatchdogSec: the server sends READY=1 once listening and
// WATCHDOG=1 every 60s; if its event loop ever blocks (frozen process),
// the pings stop and systemd kills+restarts the service. NotifyAccess=all
// because the pings come from the server child, not the serve supervisor.
// Foreground serve only: `--daemon` escapes the cgroup and would break
// the notify handshake.
"Type=notify",
"NotifyAccess=all",
"WatchdogSec=180",
"TimeoutStartSec=300",
`ExecStart=${buildServeExecLine(cliPath, { tray: false })}`,
"Restart=on-failure",
"RestartSec=5",

View File

@@ -12,25 +12,39 @@ function getActiveSalt() {
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
}
export async function getCliToken() {
const salt = getActiveSalt();
if (_cached !== null && _cachedSalt === salt) return _cached;
export function deriveCliToken(machineIdModule, salt) {
try {
// node-machine-id is CommonJS: under `await import()` its exports land on
// `.default`, so destructuring `machineIdSync` off the namespace yields
// undefined and calling it throws — which the catch below turned into an
// empty token, silently disabling CLI auth for every management request.
// Same resolution order as src/lib/machineToken.ts.
const mod = await import("node-machine-id");
const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync;
if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
const machineIdSync =
machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
if (typeof machineIdSync !== "function") return "";
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
const mid = machineIdSync(true);
_cached = crypto.createHmac("sha256", mid).update(salt).digest("hex");
const rawId = machineIdSync(true);
if (!rawId) return "";
return crypto.createHmac("sha256", rawId).update(salt).digest("hex");
} catch {
return "";
}
}
export async function getCliToken() {
const salt = getActiveSalt();
if (_cached !== null && _cachedSalt === salt) return _cached;
try {
const imported = await import("node-machine-id");
const token = deriveCliToken(imported, salt);
if (!token) {
// Swallowing here changes control flow (every management call goes out
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled");
}
_cached = token;
} catch (e) {
// Swallowing here changes control flow (every management call goes out
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e);
_cached = "";
}

View File

@@ -119,6 +119,9 @@ function loadEnvFile() {
addEnvPath(join(ROOT, ".env"));
}
const keyOrigin = new Map();
const shadowed = new Map();
for (const envPath of envPaths) {
try {
if (existsSync(envPath)) {
@@ -131,19 +134,31 @@ function loadEnvFile() {
const key = trimmed.slice(0, eqIdx).trim();
if (process.env[key] === undefined) {
process.env[key] = parseEnvValue(trimmed.slice(eqIdx + 1));
keyOrigin.set(key, envPath);
} else if (!shadowed.has(key)) {
// The line is inert: something set this key first. Report it once
// per key, whether the winner was an earlier file or the process
// environment (#6194: a shell's own HOSTNAME beat the .env and the
// server bound to the wrong address in silence).
shadowed.set(key, { winner: keyOrigin.get(key) ?? null, loser: envPath });
}
}
}
loadedEnvPaths.push(envPath);
}
} catch {
// Ignore errors reading env files.
} catch (err) {
console.warn(` \x1b[33m⚠ Could not read ${envPath}: ${err?.message ?? err}\x1b[0m`);
}
}
for (const envPath of loadedEnvPaths) {
console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`);
}
for (const [key, { winner, loser }] of shadowed) {
const setter = winner ? winner : "the environment";
console.warn(` \x1b[33m⚠ ${key} in ${loser} is ignored, ${setter} set it first\x1b[0m`);
}
}
loadEnvFile();

View File

@@ -0,0 +1 @@
- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303))

View File

@@ -0,0 +1 @@
- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316))

View File

@@ -0,0 +1 @@
- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587))

View File

@@ -0,0 +1 @@
- feat(server): emit systemd sd_notify READY/WATCHDOG/STOPPING (generated unit becomes Type=notify with WatchdogSec=180) so a frozen server process is killed and restarted by systemd instead of lingering undetected

View File

@@ -0,0 +1 @@
- **feat(call_logs):** persist the per-call error family in `call_logs.error_type` and expose a failure breakdown (`errorBreakdown`) in the usage analytics endpoint, reusing the existing production classifier ([#10670](https://github.com/diegosouzapw/OmniRoute/issues/10670))

View File

@@ -0,0 +1 @@
- **feat(proxy):** the proxy-health sweep and `GET /api/settings/proxies/egress` now report an anonymous summary of egress-IP sharing — how many rotation groups share an egress IP and the largest number of accounts behind one IP — computed from persisted `proxy_logs` over a 24h window. No IPs and no account identities by default; `PROXY_LOG_INCLUDE_IPS=true` restores raw details. ([#10677](https://github.com/diegosouzapw/OmniRoute/issues/10677))

View File

@@ -0,0 +1 @@
- feat(api): accept PATCH on /api/combos/[id], the verb the OpenAPI spec already documents (#10869)

View File

@@ -0,0 +1 @@
- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx

View File

@@ -0,0 +1 @@
- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935

View File

@@ -0,0 +1,2 @@
- **feat(credential-health):** pace the credential health sweep per connection via `provider_connections.healthCheckInterval` (minutes, 0 = never), with `CREDENTIAL_HEALTH_CHECK_INTERVAL` as the global default ([#8443](https://github.com/diegosouzapw/OmniRoute/issues/8443))
- **behavior change:** `healthCheckInterval` is a shared column — it paces both the OAuth token refresh and the credential health sweep, and `0` disables both. The connection editor defaults it to 60, so configured OAuth connections are now credential-checked at 60min instead of the previous ~10min (aligned with the probe-volume goal of #8443)

View File

@@ -0,0 +1 @@
- feat(sse): add Cursor plan image generation via Agent CLI (`IMAGE_PROVIDERS.cursor`, format `cursor-agent-image`), reusing the chat Cursor OAuth connection

View File

@@ -0,0 +1 @@
- **fix(routing):** keep approximate Combo context estimates advisory so requests reach concrete targets instead of returning a pre-dispatch 400 ([#10162](https://github.com/diegosouzapw/OmniRoute/pull/10162)) — thanks @xz-dev

View File

@@ -0,0 +1 @@
- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345))

View File

@@ -0,0 +1 @@
- **fix(backend):** log `auto/<family> matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346))

View File

@@ -0,0 +1 @@
- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353))

View File

@@ -0,0 +1 @@
- **fix(antigravity):** automatically rotate to a sibling account when one is BYOP (GCP Project ID required, `gcp_project_required` 422) — the account is excluded from selection for 24h and the request succeeds via another account instead of failing fast; the actionable 422 is surfaced only when no sibling exists (follow-up to the #10424 BYOP fast-fail) ([#10470](https://github.com/diegosouzapw/OmniRoute/pull/10470)) — thanks @rqzbeh

View File

@@ -0,0 +1 @@
- **fix(network):** direct (no-proxy) egress now bounds each attempt's response-start window (default 30s, `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`) and retries once on a fresh no-keep-alive socket, so a silently-dropped pooled keep-alive connection can no longer stall direct providers (opencode-go, command-code) until a service restart ([#10214](https://github.com/diegosouzapw/OmniRoute/issues/10214))

View File

@@ -0,0 +1 @@
- **Combo routing:** await each connection's token limit before reserving quota. The old lookup treated the `Promise` as a connection and dropped `rateLimitOverrides.tpm` ([#10686](https://github.com/diegosouzapw/OmniRoute/pull/10686)).

View File

@@ -0,0 +1 @@
- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))

View File

@@ -0,0 +1 @@
- **fix(providers):** Keep NVIDIA timeout probes and generic Antigravity/AGY HTTP 400 probes from poisoning credential health while preserving explicit Google geo-block handling ([#10799](https://github.com/diegosouzapw/OmniRoute/pull/10799)) — thanks @Zartharas

View File

@@ -0,0 +1 @@
- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832))

View File

@@ -0,0 +1 @@
- **fix(security):** Outbound URL guard now resolves IPv4-mapped IPv6 literals to their embedded address, so `[::ffff:169.254.169.254]` is refused by the unconditional cloud-metadata block like its dotted spelling; `[::]` is refused alongside `0.0.0.0` ([#10843](https://github.com/diegosouzapw/OmniRoute/pull/10843)) — thanks @ntdat812

View File

@@ -0,0 +1 @@
- fix(config): exclude cookie-auth image bridges (chatgpt-web, gemini-web) from the unprefixed model scan so a bare id never silently binds to an unofficial web bridge (#10848)

View File

@@ -0,0 +1 @@
- **fix(i18n):** The "Disabled" status no longer renders as the noun for a person with a disability in Japanese, Spanish, Hindi, Polish, Telugu, Urdu and both Chinese locales — 24 strings now use each catalog's existing wording (ja 無効, es Deshabilitado, hi अक्षम, pl Wyłączone, te నిలిపివేయబడింది, ur غیر فعال, zh-CN 已禁用, zh-TW 已停用) ([#10812](https://github.com/diegosouzapw/OmniRoute/issues/10812), [#10853](https://github.com/diegosouzapw/OmniRoute/pull/10853)) — thanks @ntdat812

View File

@@ -0,0 +1 @@
- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812

View File

@@ -0,0 +1 @@
- **fix(context):** Base64 file payloads (OpenAI `file` parts, Responses `input_file`, Claude `document` blocks) are budgeted like the Gemini `inlineData` path instead of being counted as prompt text — a ~1MB PDF estimated at 350k tokens and was rejected on the context limit before reaching the provider's document pipeline ([#10840](https://github.com/diegosouzapw/OmniRoute/issues/10840), [#10858](https://github.com/diegosouzapw/OmniRoute/pull/10858)) — thanks @ntdat812

View File

@@ -0,0 +1 @@
- **fix(mcp):** MCP tool calls that wait on a model provider no longer abort after 10 seconds. `omniRouteFetch` applied a single hardcoded `AbortSignal.timeout(10000)` to every internal hop, and `omniroute_route_request` — which posts to `/v1/chat/completions` and waits on the upstream provider, plus auto-combo candidate probing before a provider is even chosen — passed no signal of its own, so it inherited it. Any route slower than 10s failed from the MCP side while the identical request succeeded through the REST API. `omniroute_web_search` and `omniroute_web_fetch` in the same file already carried an explicit 60s signal, so that value is now shared by all three provider-bound calls instead of being repeated as a literal, while management reads (health, resilience, rate limits, combos, quota, usage) keep their fast-fail 10s budget so a stalled local endpoint still cannot hold a tool call open. Both budgets are overridable through `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` and `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS`, replacing the reported workaround of patching the compiled `dist/.build/next/server/chunks/*.js`; a malformed or non-positive override falls back to the default rather than disabling the timeout

View File

@@ -0,0 +1 @@
- **fix(providers):** importing models with an expired API key now surfaces the credential error instead of reporting "No new models were added". The Import button posts to `/api/providers/{id}/sync-models`, which self-fetches the models route; that route does not fail on an upstream 401 but degrades to a catalog it already has, preferring the cache and using the local catalog only when there is no cache. A provider that imported successfully once therefore has a cache, so an expired key produced `{ source: "cache", warning: "Models probe failed (401) — using cached catalog" }` with HTTP 200 — and the #5460/#5465 degradation guard only recognised the `local_catalog` branch, so model-sync accepted it as a successful discovery, found every cached model already imported, and returned the empty-diff result. Retest does not go through this path, which is why it failed correctly and made the import look like a genuine "nothing to do". The existing rule — a degraded discovery must not be persisted as the synced catalog — is now applied to the branch it missed rather than special-casing 401/403, discriminating on the warning the fallback builder always attaches (an ordinary non-refresh cache hit attaches none, and model-sync always requests `refresh=true`). `isDegradedLocalCatalog` keeps its exact meaning and its existing tests

View File

@@ -0,0 +1 @@
- fix(api): reject a combo update that removes every model, and store the copilot's combo targets where the router reads them (#10866)

View File

@@ -0,0 +1 @@
- **fix(proxy):** proxy "Test connection" no longer reports an IPv4-only SOCKS5/SSH proxy as dead. #1255 moved every egress probe from `api.ipify.org` to `api64.ipify.org` so proxies with IPv6 egress could be tested, but `api64` is IPv6-first: a tunnel with no IPv6 route has nothing to connect to, so the probe hung until the caller's deadline and a proxy that was carrying live LLM traffic came back as a failure. Swapping the target to `api4` fixes that case and re-breaks the one #1255 fixed, so the probe now tries the targets in order instead — `api64` first, so a proxy with working IPv6 answers on the first attempt and keeps the exact behaviour #1255 introduced, including which of its addresses is reported (the egress IP is used as an identity to detect accounts of one rotation group sharing an address, so the attempts are sequential rather than raced). The attempts split the budget each call site already enforced, so no probe can take longer than it could before, and each attempt gets its own `AbortController` so exhausting the budget on an unreachable target does not abort the next one. `OMNIROUTE_PROXY_ECHO_URL` pins a single target — including a self-hosted echo — replacing the workaround of rewriting the compiled bundle after every upgrade. The relay branch of the test route still targets `api64` through `x-relay-target`, since that request egresses from the relay worker rather than the operator's tunnel

View File

@@ -0,0 +1 @@
- fix(cli): warn when a .env line never takes effect, and stop swallowing an unreadable .env (#10870)

View File

@@ -0,0 +1 @@
- **fix(db):** Remove stale MiMoCode provider configuration, including the legacy `mcode` alias, left after provider retirement while preserving historical usage and call logs ([#10873](https://github.com/diegosouzapw/OmniRoute/pull/10873)) — thanks @Zartharas

View File

@@ -0,0 +1 @@
- **fix(provider-health):** Keep unsupported 404/405 validation probes neutral so they do not poison stored credential health or scheduler failure state, while still honoring per-connection health-check pacing ([#10878](https://github.com/diegosouzapw/OmniRoute/pull/10878)) — thanks @Zartharas

View File

@@ -0,0 +1 @@
- **fix(antigravity):** map Gemini 3.7 Flash tier ids (`gemini-3.7-flash-high/medium/low`, bare `gemini-3.7-flash`) to the upstream `gemini-3.7-flash-tiered` model id Google's Cloud Code endpoint expects, and configure per-tier thinking budgets ([#10882](https://github.com/diegosouzapw/OmniRoute/pull/10882)) — thanks @adevwithpurpose

View File

@@ -0,0 +1 @@
- **fix(memory):** enable agent memory save/update via MCP tools (`memory_save`/`update`/`search`/`delete` builtins with per-provider schemas, `apiKeyId` optional with caller-principal fallback) and gate server-side memory builtin injection to non-stream requests only ([#10887](https://github.com/diegosouzapw/OmniRoute/pull/10887)) — thanks @Egorich-print

View File

@@ -0,0 +1 @@
- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax

View File

@@ -0,0 +1 @@
- **fix(translator):** merge consecutive same-role contents in direct Claude to Gemini request translation to prevent upstream HTTP 400 errors

View File

@@ -0,0 +1 @@
- **fix(catalog):** derive combo reasoning-effort tiers from the exact runtime-selectable connection scope, intersecting dynamic, pinned, allowlisted, and compatible provider-node evidence while failing closed on unknown capabilities.

View File

@@ -0,0 +1 @@
- **docs(docker):** spell out that `:latest` tracks the highest **published** stable SemVer (not git `main`), and that GitOps should pin `X.Y.Z` ([#10317](https://github.com/diegosouzapw/OmniRoute/issues/10317))

View File

@@ -0,0 +1 @@
- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349))

View File

@@ -0,0 +1 @@
- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350))

View File

@@ -0,0 +1 @@
- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351))

View File

@@ -0,0 +1 @@
- chore(startup): remove `src/server-init.ts` (183 lines, never imported — the boot path is `src/instrumentation-node.ts`) and correct four `"called from server-init.ts"` comments left pointing at the dead entry point (#10780)

View File

@@ -0,0 +1 @@
- **docs(openapi):** document the `GET` and `PUT` operations on `/api/combos/{id}`, and add an operation-level coverage floor so a missing verb can no longer hide behind a path that already counts as covered ([#10875](https://github.com/diegosouzapw/OmniRoute/pull/10875))

View File

@@ -0,0 +1 @@
- **docs(auth):** distinguish dashboard sessions, `oma_live_…` Access Tokens, manage-scoped API keys, and inference keys ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))

View File

@@ -0,0 +1 @@
- **chore(ci):** ignore ad-hoc `BOT_TOKEN`/`BOT_URL` in env-doc-sync (scripts/ad-hoc mesh helpers, not runtime config)

View File

@@ -54,11 +54,6 @@
"count": 3
}
},
"open-sse/handlers/chatCore/codexFailover.ts": {
"no-restricted-imports": {
"count": 1
}
},
"open-sse/handlers/chatCore/comboContextCache.ts": {
"no-restricted-imports": {
"count": 1
@@ -1023,11 +1018,6 @@
"count": 1
}
},
"src/lib/api/modelTestRunner.ts": {
"no-restricted-imports": {
"count": 1
}
},
"src/lib/api/proxyRegistryRouteHandlers.ts": {
"no-restricted-imports": {
"count": 1
@@ -1083,11 +1073,6 @@
"count": 1
}
},
"src/lib/embeddings/service.ts": {
"no-restricted-imports": {
"count": 1
}
},
"src/lib/evals/runtime.ts": {
"no-restricted-imports": {
"count": 1
@@ -2934,11 +2919,6 @@
"count": 83
}
},
"tests/unit/responses-parse-once-4041.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 6
}
},
"tests/unit/responses-translation-fixes.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 35

View File

@@ -304,6 +304,8 @@
"_rebaseline_2026_07_27_3850_relax_filesize_cap": "OWNER-APPROVED TEMPORARY relax for v3.8.50-3.8.54 PREPARE phase (docs/ROADMAP.md). cap 800->900 (+100), testCap 800->900 (+100). Targets: decompose-existing-frozen unchanged (frozen still only-shrink); this only relaxes the cap for NEW files in the decompose/extract-while-PREPARE phase (.51='executor registry in-place' and .52='combo.ts decomposition' create new leaf modules above 800). RE-TIGHTENING MANDATORY in v3.8.51: cap target 850 = 850 once decomposition wave stabilizes. SUPERSEDED by _rebaseline_2026_07_27_3850_relax_filesize_cap_v2_20pct (v1 +20% buffer) — retained for audit. Tracked via same roadmap issue.",
"_rebaseline_2026_07_27_v3849_train1h": "Merge-train 1H (31 PRs) — owner-approved 2026-07-27. Two distinct causes, kept separate on purpose: (1) GENUINE irreducible growth at existing chokepoints — providerLimits/auth (#8632 Kimi quota-reset recovery), rateLimitManager (#8616 idle wedged limiters), models-catalog-route.test (#8610 OpenCode Go effort aliases); (2) COLLISION with #8585, which banked shrinks measured on the pre-train release tip while 30 sibling PRs in the SAME train grew those files again — chat/accountFallback (#8628), chatCore (#8613), videoGeneration (#8581), imageGeneration. The zero-headroom frozen entries cannot absorb either. Ceilings re-pinned to the post-merge tip; #8612 (also in this train) automates shrink-banking so this self-inflicted drift stops recurring. Detail: src/lib/usage/providerLimits.ts 1006->1013 (#8632); src/sse/services/auth.ts 2492->2508 (#8632); open-sse/services/rateLimitManager.ts 1014->1060 (#8616); src/sse/handlers/chat.ts 1842->1845 (#8628); open-sse/handlers/chatCore.ts 4939->4955 (#8613); open-sse/handlers/imageGeneration.ts 3100->3101 ((sem PR — teto do #8585)); open-sse/handlers/videoGeneration.ts 1038->1063 (#8581); open-sse/services/accountFallback.ts 1965->1966 (#8628); tests/unit/models-catalog-route.test.ts 1608->1636 (#8610)",
"frozen": {
"_rebaseline_2026_08_20_10878_10799_provider_health_probes": "PRs #10878 (unsupported OpenAI-like validation probes stay neutral) + #10799 (preserve credential health on inconclusive NVIDIA-timeout/Antigravity-400 probes) own growth: src/app/api/providers/[id]/test/route.ts 946->1025 (+79, sum of both boarded together). Both add narrowly-scoped classification branches at the existing test-route dispatch chokepoint (unsupported-capability skip, credential-inconclusive detection) rather than new files, mirroring the prior 2026_06_27_5193 rebaseline of the same file. Covered by tests/unit/provider-validation-unsupported-neutral.test.ts + tests/unit/provider-health-inconclusive-probes.test.ts.",
"src/app/api/providers/[id]/test/route.ts": 1025,
"_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
"_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\"tool\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
"_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \"headroom\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, <cap) and the async orderer orderTargetsByHeadroom is appended to the existing open-sse/services/combo/quotaStrategies.ts (<cap) next to its sibling reset-aware/reset-window orderers (reuses their connection-expansion machinery). headroom = 1 - max(util_5h, util_7d) from getSaturation (src/lib/quota/saturationSignals.ts), prefers the connection with the most free capacity. Only the dispatch wiring is irreducible at the existing combo strategy chokepoint (mirrors the reset-aware/reset-window/context-optimized branches); not extractable without hiding the call site. fill-first stays default; all existing strategies untouched. Covered by tests/unit/combo-headroom-ranking.test.ts (pure helper) + tests/unit/combo-headroom-strategy.test.ts (orderer, saturation injected). Structural shrink of combo.ts tracked in #3501.",
@@ -447,7 +449,7 @@
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
"src/lib/modelCapabilities.ts": 1006,
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
"open-sse/config/imageRegistry.ts": 1019,
"open-sse/config/imageRegistry.ts": 1034,
"src/sse/handlers/chatHelpers.ts": 1017,
"src/shared/middleware/chatBodyAdmission.ts": 1005
},
@@ -615,5 +617,7 @@
"_rebaseline_2026_08_12_modelcapabilities_snapshot_routing": "Base-reds round 3 (#9985): modelCapabilities.ts crossed the new-file cap at 1006 (+~10) when the context/max-input-token override lookups were routed through the #9199 bulk snapshot (fixing 323 per-model SQLite reads per catalog prepare — auto-combo-context-advertising guard); cohesive change at the existing resolution chokepoints, not extractable. Covered by tests/unit/auto-combo-context-advertising.test.ts + model-capability-resolution-snapshot-9199.test.ts.",
"_rebaseline_2026_08_14_imagetotext_servicekinds": "Image-to-Text category (#10275/#10291): gateways.ts grew 1250→1255 by data lines only — the serviceKinds: [\"llm\", \"imageToText\"] declarations on the openrouter and chutes catalog entries, plus the 3-line comment recording why chutes needs no static dots.ocr entry (passthroughModels discovery). No new logic or branching; the file is a provider catalog of declarative metadata. Splitting a catalog for five lines would be worse than the growth (semantic-families rule).",
"_rebaseline_2026_08_18_imageregistry_merge_train": "merge-train 2026-08-18 (owner-authorized, /merge-prs batch of 84): open-sse/config/imageRegistry.ts crossed the 1000-line new-file cap for the first time purely from combining three independent, already-legitimate provider registrations boarded in the same local merge-train — #10542 (aihorde optional-key image catalog), #10494 (gemini-web image generation), #10594 (freepik/magnific provider rename + validation). 996 on release tip -> 1019 on the train tip. Each PR individually adds a small, additive IMAGE_PROVIDERS registry entry at the existing chokepoint; none crosses the cap alone. Not modularized as part of this train's gate fix (out of scope for a merge reconciliation, not a feature change). Covered by each PR's own focused tests (aihorde-image-catalog/generation, gemini-web image tests, freepik/magnific provider tests).",
"_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session)."
"_rebaseline_2026_08_20_v3850_merge_train_batch1": "Merge-train batch1 (2026-08-19/20, 30 PRs boarded onto release/v3.8.50): gateways.ts 1255->1268 = PR #10722 (Token Kiosk OpenAI-compatible provider gateway catalog entry, +13 declarative lines, same god-file no-split rationale as prior gateways.ts rebaselines); chatHelpers.ts (uncapped, not previously frozen) new 1017 = PR #10797 (relay/bifrost error normalization, +23/-2, own-PR growth, existing file already near cap from accumulated chokepoint wiring per its own rebaseline history above); chatBodyAdmission.ts (uncapped) new 1005 = pre-existing base-red on the pure release tip (1004>1000 before this train boarded anything, no PR in this batch touches this file) — frozen here at its current size, not authorizing further growth. Owner-authorized rebaseline (2026-08-19 merge-prs session).",
"_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
"_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature."
}

View File

@@ -0,0 +1,350 @@
---
title: "Adaptive Routing: Routing Events, Quality Feedback & Explainability"
version: 3.8.50
lastUpdated: 2026-08-20
---
# Adaptive Routing: Routing Events, Quality Feedback & Explainability
This document describes the feedback-driven adaptive routing foundation added to
OmniRoute. It is deliberately small: it introduces a typed routing-outcome
channel, an online quality signal that feeds the existing auto-combo scorer, an
optional OpenTelemetry exporter, and an explainability endpoint. It does **not**
replace the existing resilience stack (circuit breaker, connection cooldown,
model lockout, health matrix, autopilot) — it complements it.
## 1. Architectural context
OmniRoute is a data plane with a **request hot path** and a **control/intelligence
plane**. The hot path must stay fast, memory-efficient, asynchronous, resilient and
predictable. Evaluation, quality scoring, experiments and historical analysis belong
to the control plane.
```
AI Agent / IDE
┌─────────────────────┐
│ OmniRoute │ data plane (fast, sync, in-memory)
│ routing / failover │
│ health / guardrail │
│ cache / streaming │
└──────────┬──────────┘
│ RoutingEvent (fire-and-forget, ~0.2µs)
┌─────────────────────┐
│ Feedback sinks │ control plane (async, best-effort)
│ quality tracker │
│ OTel exporter │
│ explain store │
└──────────┬──────────┘
▼ quality score
auto-combo scorer
```
### What was already there (audited, not duplicated)
| Concept | Existing implementation |
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
| Availability (can we send traffic?) | Circuit breaker (CLOSED/DEGRADED/OPEN/HALF_OPEN, DB-persisted), connection cooldown, model lockout |
| Health reporting | `providerHealthMatrix.ts`, `providerHealthAutopilot.ts` |
| Shadow traffic | `open-sse/services/combo/shadowRouting.ts` |
| Guardrails | `src/lib/guardrails/` (pre/post hooks) |
| Exact cache | `src/lib/semanticCache.ts` (signature-based) |
| Evaluators / eval-driven routing | `src/lib/evals/`, `open-sse/services/evalRouting.ts` |
| Combo decision explainability | `open-sse/services/combo/decisionTrace.ts` |
| Dashboard real-time events | `src/lib/events/eventBus.ts` (UI notification channel, `unknown` payloads, 100-entry history) |
The routing-event layer is **not** a re-implementation of `eventBus`: that bus is
the dashboard's real-time notification channel (typed _event names_, opaque
payloads, UI consumers). `RoutingEvent` is a typed _outcome_ struct
(latency/tokens/cost/outcome/finish-reason) consumed by the control plane's
feedback sinks (quality tracker, OTel exporter, explain store).
### What was missing (added here)
1. A **typed routing-outcome event + sink abstraction** (`RoutingEvent` /
`RoutingEventSink`). `decisionTrace` is combo-scoped and in-memory-only;
`comboMetrics` are cumulative counters; `call_logs` is raw async persistence.
None is a typed, sink-based outcome channel that a quality tracker, an OTel
exporter, or a Future-AGI-style evaluator can subscribe to.
2. An **online quality signal** (EWMA) for output quality — the scorer previously
proxied "quality" only through static task fitness and opt-in eval pass-rates.
3. An **optional, dependency-free OTel exporter** using GenAI semantic conventions.
4. An **explainability endpoint** returning the real routing decisions + quality state.
## 2. Routing Events (feedback foundation)
Files: `open-sse/services/routing/events.ts`, `.../index.ts`
A `RoutingEvent` carries only routing metadata:
```ts
interface RoutingEvent {
requestId: string;
provider: string;
model: string;
strategy: string; // "auto" | "priority" | "direct" | ...
latencyMs: number;
ttftMs: number | null;
inputTokens: number | null;
outputTokens: number | null;
cost: number | null;
retries: number;
fallbackUsed: boolean;
outcome: RoutingOutcome; // allowlisted union
status: number | null;
finishReason: string | null;
connectionId: string | null;
ts: number;
}
```
`RoutingEventSink` is a `Send+Sync`-style trait in TypeScript:
```ts
interface RoutingEventSink {
readonly name: string;
record(event: RoutingEvent): void; // must be O(1), no sync I/O
}
```
The hot path calls `emitRoutingEvent(event)` once per completed request
(the streaming-completion callback, the non-streaming success path, and the
malformed-200 failure path in `handleChatCore`). Dispatch is synchronous fan-out
to registered sinks, but each sink only enqueues/updates in-memory state. **No
synchronous database writes, no network I/O on the hot path.**
Default sinks:
- `MemoryRoutingEventStore` — bounded (500) ring buffer, newest-first, for the
explain endpoint.
- `QualityTracker` consumer — updates the EWMA quality estimate.
- `OtlpHttpsEventSink` — optional, enabled only when `OMNIROUTE_OTEL_ENDPOINT`
(or `OTEL_EXPORTER_OTLP_ENDPOINT`) is set.
### Measured overhead (honest comparison)
`npm run bench:routing-events` on this workstation (100k iterations; sub-µs ops
measured as aggregate µs/op because per-op percentiles are below
`performance.now()` timer resolution):
| Scenario | µs/op | ops/s |
| --------------------------------- | ------ | ------ |
| baseline (scoring only) | ~0.045 | ~22 M |
| baseline + RoutingEvent (2 sinks) | ~0.168 | ~5.9 M |
| baseline + event + OTel enqueue | ~0.163 | ~6.1 M |
| concurrent (8 interleaved bursts) | ~0.18 | — |
The event-dispatch delta over baseline scoring is ~0.12 µs/request; the OTel sink
only enqueues (O(1) buffer push), adding nothing measurable. These numbers are
machine-specific and relative — not a production guarantee. The v1 "~0.2 µs"
figure was an aggregate estimate; this methodology separates the scoring baseline
from the event-dispatch cost.
## 3. Quality Signal (feedback-driven provider state)
Files: `open-sse/services/routing/quality.ts`
v2 separates **operational** from **semantic** quality:
- **Operational** — derived from the routing hot path (HTTP 4xx/5xx, connection
failures, 429s, malformed responses, stream interruptions, `finish_reason=length`,
zero-output successes, latency/TTFT EWMA). A 200 is NOT treated as semantic
quality.
- **Semantic** — the actual value of the generated output. ONLY ever produced by
an evaluator via `setSemanticQuality()`. It is `null` until one provides it and
never leaks into the operational score.
Per-(provider, model) state (EWMA + bounded counters):
- `successEwma` — EWMA (α=0.2) of outcome success.
- `latencyEwma` / `ttftEwma` — EWMA of latency (α=0.1).
- `samples`, `anomalies`, `rateLimited`, `semantic`, `semanticConfidence`.
- `recencyMs` — how recently the model was last observed.
### Confidence / sample awareness
`confidence = clamp01(samples / 50)`, and the score returned to the scorer is
blended toward the neutral midpoint:
```
score = 0.5 + confidence * (operational - 0.5)
```
Consequences (verified by tests):
- A cold provider (0 samples) scores **0.5** — not unfairly penalized, but
unable to dominate a provider with thousands of solid observations.
- A provider with 7 lucky successes is pulled toward 0.5 (never dominates from
optimistic initialization).
- A provider with 50+ samples converges to its true operational score.
- Degradation and recovery are gradual (EWMA), and one isolated failure does
not destroy a healthy provider.
`ProviderQuality` exposes `{ operational, semantic, confidence, samples, anomalies,
rateLimited, successEwma, latencyEwmaMs, ttftEwmaMs, recencyMs }`.
This feeds the auto-combo scorer as the `quality` scoring factor:
- `ScoringFactors.quality` / `ScoringWeights.quality` in
`open-sse/services/autoCombo/scoring.ts`.
- `DEFAULT_WEIGHTS`: `health` 0.1905 → 0.1605, `quality` 0.03. Sum stays 1.0.
- `buildAutoCandidates` populates `candidate.quality` from the tracker; candidates
without data default to neutral **0.5** (a cold candidate is neither boosted nor
penalized).
The closed loop:
```
RoutingEvent → QualityTracker → getQualityScore → auto-combo quality factor
↑ │
└────── request outcome (handleChatCore) ←────────────┘
```
### Hard exclusion vs soft penalty
The quality signal is a **soft adaptive preference** only. Hard exclusion stays
with the existing resilience stack: circuit breaker OPEN, quota exhausted,
auth failure, model lockout — none of these are affected by the quality score.
A provider whose quality score dips temporarily is de-preferenced, never
hard-disabled.
## 3b. Canonical stream timing (TTFT / ITL)
Files: `open-sse/utils/streamTiming.ts`
`createStreamTiming()` is the single instrumentation seam for the streaming path,
wired into `createSSEStream` (open-sse/utils/stream.ts):
- `markByte()` — first upstream chunk received.
- `markForward()` — first chunk forwarded to the client (used for TTFT).
- `markInterrupted()` — stream timeout/abort/error before a clean finish.
- `ttft()` = first-forwarded-SSE-chunk latency. **This is NOT token-level TTFT**
a single SSE chunk may carry zero/one/many tokens. Documented precisely.
- `avgItlMs()` = mean inter-chunk gap (a chunk-latency proxy for ITL).
TTFT/ITL/interrupted flow into the `RoutingEvent` (`ttftMs`, `itlMs`) and are
exported as GenAI/OmniRoute span attributes by the OTel sink.
## 4. OpenTelemetry / GenAI observability
Files: `open-sse/services/routing/otel.ts`
- Dependency-free OTLP/HTTP JSON exporter (uses global `fetch`, no
`@opentelemetry/*` SDK).
- Spans follow GenAI semantic conventions (`gen_ai.provider.name`,
`gen_ai.request.model`, `gen_ai.usage.input_tokens/output_tokens`,
`gen_ai.completion.finish_reason`, `gen_ai.system`) plus OmniRoute routing
attributes (outcome, status, ttft, retries, fallback).
- `record()` only enqueues into a bounded buffer (O(1)); a background timer
flushes via `POST {endpoint}/v1/traces` asynchronously. Under overload the
oldest events are dropped (`dropped` counter) — never backpressure the data
plane.
- **Disabled unless configured.** `OMNIROUTE_OTEL_ENDPOINT` (or
`OTEL_EXPORTER_OTLP_ENDPOINT`) must be set; otherwise the sink is not
registered and zero OTel code runs.
## 5. Explainability
- `GET /v1/explain/routing` returns the recent `RoutingEvent`s (the real
decisions, newest first) and the per-provider/model quality snapshot.
- Auth mirrors `/v1/combos` (Bearer API key or dashboard session; anonymous on
single-user local deployments with `REQUIRE_API_KEY=false`).
- Combo-level per-invocation traces remain available via the existing
`decisionTrace.ts` (header `X-OmniRoute-Combo-Trace`).
- Safety: events carry only routing metadata, never prompts/bodies/credentials.
## 6. Evaluation-plane integration (Future AGI readiness)
OmniRoute treats Future AGI (or any evaluator) as a **potential
intelligence/evaluation backend, not a dependency**. The seams:
- A `RoutingEventSink` can forward events to an evaluator asynchronously.
- The `MemoryRoutingEventStore` + quality snapshot give an evaluator the raw
decision stream.
- A future `Evaluator` (deterministic, local judge, HTTP, WASM) would consume
events/traces and return a `QualityScore` that feeds the same
`getQualityScore`/quality-factor path.
- Existing eval-driven routing (`open-sse/services/evalRouting.ts`) already
re-orders combo targets by `eval_runs` pass-rates when enabled.
No evaluation runs synchronously on the request path, and the gateway operates
fully with the evaluator absent.
## 7. Final architectural review
1. **What remains on the synchronous hot path?** Routing/scoring, guardrail
pre-checks, cache lookup, and one `emitRoutingEvent` fan-out (~0.12 µs over
baseline scoring) to in-memory sinks.
2. **What moved to asynchronous processing?** OTel export (timer + fetch),
`call_logs`/usage persistence, semantic-cache writes, quality is in-memory
and O(1) (no async needed).
3. **How does a routing outcome become feedback?** `handleChatCore` emits a
`RoutingEvent``QualityTracker` updates EWMA state → `getQualityScore`
feeds the auto-combo `quality` factor.
4. **How does quality influence future routing?** A low quality score reduces
the weighted score of that provider/model in `scoreAutoTargets`, so degraded
models are gradually de-preferenced and recover as their EWMA improves.
5. **How can Future AGI integrate without becoming a dependency?** Via the
`RoutingEventSink` interface / a future `Evaluator` adapter — no hardcoded
dependency.
6. **What happens when the evaluator is unavailable?** Routing is unaffected;
quality falls back to neutral (1.0) for models with no observed signal.
7. **What happens when telemetry is unavailable?** The OTel sink simply isn't
registered; the rest of the routing layer runs unchanged.
8. **What happens under overload?** The OTel buffer drops oldest events; quality
and the ring buffer are bounded by construction; no backpressure.
9. **How does provider state recover after degradation?** EWMA re-converges as
successes accumulate; warmup keeps cold models neutral; the circuit breaker
independently recovers via HALF_OPEN probes.
10. **Which proposed features were intentionally NOT implemented, and why?**
- Shadow traffic / experiments — already implemented
(`combo/shadowRouting.ts`); not re-built.
- Guardrails — already implemented (`src/lib/guardrails/`); not duplicated.
- Semantic cache — already implemented (`src/lib/semanticCache.ts`); not
duplicated.
- A full experiment-management platform, dataset tooling, prompt-optimization
platform, vector DB, or mandatory external OTel infrastructure — out of
scope for a lean data plane.
- A Rust `RoutingEvent` struct — the data plane is TypeScript; the TS type
is the adapted equivalent.
## 8. Configuration reference
| Variable | Default | Effect |
| ----------------------------- | ----------- | ------------------------------------------------------------------------------- |
| `OMNIROUTE_OTEL_ENDPOINT` | unset | When set, enables the OTLP/HTTP traces exporter (e.g. `http://collector:4318`). |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Fallback alias for the OTLP endpoint. |
| `OTEL_SERVICE_NAME` | `omniroute` | `service.name` resource attribute. |
## 9. Tests
- `tests/unit/routing-events.test.ts` — event normalization, status
classification, bounded ring buffer, sink fan-out + isolation.
- `tests/unit/routing-quality.test.ts` — EWMA warmup, failure/success recovery,
anomaly penalties, 429 transient handling, snapshot, reset.
- `tests/unit/routing-scoring-quality.test.ts` — weight integrity, neutral
default, quality factor ranking.
- `tests/unit/routing-otel.test.ts` — enable gating, GenAI span payload, async
flush, drop-under-overload.
- `tests/unit/routing-events-concurrency.test.ts` — thousands of events, ring
buffer boundedness, throwing-sink isolation, interleaved async bursts,
reset-during-inserts.
- `tests/unit/routing-adaptive-e2e.test.ts` — deterministic end-to-end loop via
the real `scoreAutoTargets` scorer: healthy → degrade → recover → blip, plus
cold-start and lucky-cold-provider scenarios.
- `tests/unit/stream-timing.test.ts` — TTFT (first-forwarded-chunk), ITL,
first-byte vs first-forward, interruption, malformed/empty chunk safety.
## 10. Pre-existing issues status (Phase 18)
| Issue | Status | Notes |
| ----------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `omniglyph` export mismatch | **FIXED (environmental)** | `node_modules` was out of sync with `package-lock.json` (installed 1.3.1 vs locked 1.4.0). Running `npm install omniglyph@1.4.0` restored the locked version; type errors dropped to 0. Manifests unchanged. |
| Stale `getKnownContextOverflow` tests | **KNOWN — not fixed** | `combo-context-overflow-compression-probe.test.ts` imports a function that no longer exists in `open-sse/services/combo.ts` (only comments reference it). Fixing requires re-implementing or re-writing those tests — unrelated architectural churn. |
| `combo-runtime-unit-concurrency.test.ts` DB isolation | **KNOWN — not fixed** | Test-harness SQLite-isolation assertion fails when run directly; fails identically on the base branch. |
| i18n `llm.txt` drift | **KNOWN — not fixed** | `docs/i18n/*/llm.txt` differ from root; pre-existing, blocks the docs-sync pre-commit gate. |
Environmental vs code issues are kept distinct; no unrelated failures are hidden
behind changed test filters.

View File

@@ -89,7 +89,6 @@ src/
├── i18n/ Locale bundles
├── instrumentation.ts Next.js instrumentation hook
├── instrumentation-node.ts
├── server-init.ts Process-level bootstrap (env, DB, jobs, sync)
└── proxy.ts Top-level proxy bootstrap helper
```

View File

@@ -133,7 +133,6 @@ src/
├── types/ # Shared TS type files
├── instrumentation.ts # Next.js telemetry hook (browser + edge)
├── instrumentation-node.ts # Node-only instrumentation
├── server-init.ts # Server bootstrap (DB migrations, jobs, cleanup)
└── proxy.ts # HTTP-proxy entry shim
```

View File

@@ -12,6 +12,7 @@
"ROUTER_BACKENDS",
"admission-lanes",
"cluster-decisions",
"persistence-backend-boundary"
"persistence-backend-boundary",
"ADAPTIVE_ROUTING"
]
}

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (342 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (343 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
<desc>Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen.</desc>
<defs><clipPath id="tickerClip"><rect x="12" y="304" width="1176" height="40"/></clipPath><clipPath id="tw0"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;31;61;92;122;153;184;214;245;245" keyTimes="0;0.012;0.018;0.024;0.030;0.036;0.042;0.048;0.054;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw1"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;26;51;76;102;128;153;178;204;204" keyTimes="0;0.345;0.351;0.357;0.363;0.369;0.375;0.381;0.387;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw2"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;20;41;61;82;102;122;143;163;163" keyTimes="0;0.678;0.684;0.690;0.696;0.702;0.708;0.714;0.720;1" dur="18s" repeatCount="indefinite"/></rect></clipPath></defs>
<rect width="1200" height="350" fill="#0d1117"/>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 342 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 109 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 343 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 109 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
<defs>
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 342 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 342 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 109 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 343 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 343 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 109 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
<desc>Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.</desc>
<defs>
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
@@ -21,7 +21,7 @@
<line x1="150" y1="53" x2="1160" y2="53" stroke="#232b38" stroke-width="1.5"/>
</g>
<g>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">342 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">343 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
</g>
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
@@ -38,7 +38,7 @@
<line x1="3.9" y1="3.9" x2="18.1" y2="18.1"/>
</g>
<text x="102" y="170" font-size="18" font-weight="800" fill="#74b9ff">Never hit limits</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 342 providers in</text>
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 343 providers in</text>
<text x="66" y="226" font-size="13.5" fill="#a1a1aa">milliseconds. Quota out? The next provider</text>
<text x="66" y="248" font-size="13.5" fill="#a1a1aa">takes over — zero downtime.</text>
</g>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,4 +1,4 @@
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 342 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 342 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
<svg viewBox="0 0 1200 548" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="OmniRoute hero: Never stop coding. Every AI tool to 343 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot and Antigravity into free Claude, GPT and Gemini with auto-fallback. RTK + Caveman stacked compression saves 15 to 95 percent of tokens — about 89 percent average on tool-heavy sessions — so you never hit limits. Stats: 343 AI providers, 90+ free tiers, about 1.51B free tokens per month, 15 to 95 percent token savings, 19 routing strategies, zero dollars to start.">
<desc>Animated hero card: a pulse travels the divider line and a compression bar demo repeatedly shrinks a prompt by up to 95 percent; all headline content is static and readable on the first frame.</desc>
<defs>
<pattern id="gridPaperH" width="32" height="32" patternUnits="userSpaceOnUse">
@@ -28,7 +28,7 @@
<text x="48" y="138" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="60" font-weight="800" fill="#e9edf3">Never stop coding<tspan fill="#a855f7">.</tspan></text>
<!-- subheadline -->
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">342 providers</tspan><tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
<text x="48" y="184" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="25" font-weight="600" fill="#c9d1d9">Every AI tool → <tspan fill="#a78bfa" font-weight="800">343 providers</tspan><tspan fill="#7ee787" font-weight="800">90+ free</tspan> — through one endpoint.</text>
<!-- plug line -->
<text x="48" y="222" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="16.5" fill="#a1a1aa">Claude Code · Codex · Cursor · Cline · Copilot · Antigravity&#160;&#160;&#160;&#160;<tspan fill="#7ee787" font-weight="700">FREE</tspan> Claude / GPT / Gemini · auto-fallback</text>

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -347,6 +347,8 @@ per-key path take precedence once it is. stdio has no per-caller identity (see
| `OMNIROUTE_MCP_SCOPES` | (empty) | Comma-separated allowlist of scopes considered "available" by default (used when caller does not provide its own scopes) |
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | (unset = on) | When set to `0/false/off/no`, disables MCP description compression at registration time |
| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | (unset = on) | Alternate alias for the same toggle as above |
| `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` | `10000` | Abort budget for internal management reads (health, resilience, combos, quota, usage) |
| `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS` | `60000` | Abort budget for hops that wait on a provider (`route_request`, `web_search`, `web_fetch`) |
| `MCP_TOOL_DENY` | (unset = no filter) | Comma-separated tool names to drop from `tools/list` (tool-cardinality reduction — see below) |
| `MCP_TOOL_ALLOW` | (unset = no filter) | Comma-separated tool names to keep exclusively (allow-list mode — see below) |
| `DATA_DIR` | `~/.omniroute` | Heartbeat file is written to `${DATA_DIR}/runtime/mcp-heartbeat.json` |

View File

@@ -52,6 +52,8 @@ safely retry only the failures after a partial result.
- **Pollinations** — Free GPT-5, Claude, Gemini (no key needed)
- **LongCat** — 10M tokens free (one-time grant, requires account + KYC)
- **Cloudflare AI** — 50+ models, 10K neurons/day
- **MLX Gemma 26B** — Local Apple Silicon model (~38.5 tok/s, ~15.9GB RAM)
- **MLX Qwen 3.8 27B** — Local Apple Silicon model (~9.1 tok/s, ~13.1GB RAM)
4. Click **Connect**
5. Done! You now have free AI access.
@@ -79,6 +81,94 @@ safely retry only the failures after a partial result.
5. Login with your account
6. Done! You now have access to your subscription models.
### Option D: Local MLX Models (Apple Silicon)
For Apple Silicon Macs with unified memory, OmniRoute supports connecting to local MLX models running via `mlx-lm.server` as regular OpenAI-compatible local providers.
#### Prerequisites
- **Apple Silicon Mac** (M1/M2/M3/M4) with 24GB+ unified memory recommended
- **uv** package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- **mlx-lm**: `uv pip install mlx-lm`
#### Quick Start
1. **Install dependencies**:
```bash
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install mlx-lm
uv pip install mlx-lm
```
2. **Start MLX servers manually** (in separate terminals):
```bash
# Terminal 1: Gemma 4 26B A4B IT-QAT (port 11435)
uv run mlx_lm.server --model mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned --port 11435 --host 127.0.0.1
# Terminal 2: Qwen 3.8 27B MLX Mixed (port 11436)
uv run mlx_lm.server --model maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw --port 11436 --host 127.0.0.1
```
3. **Connect in OmniRoute Dashboard**:
- Go to **Providers** → **Add Provider**
- Select **MLX Gemma 26B** or **MLX Qwen 3.8 27B**
- Click **Connect** (no API key needed)
4. **Use with OpenCode**:
```bash
# Configure OpenCode to use OmniRoute
opencode config set api.base_url http://localhost:20128/v1
opencode config set api.key <your-omniroute-api-key>
# Use MLX models
opencode run --model mlx-gemma/gemma-4-26b
opencode run --model mlx-qwen/qwen3.8-27b
```
#### Memory Management
**Important**: With 24GB unified memory, only **one large MLX model can run at a time**.
- Gemma 26B: ~15.9GB peak memory
- Qwen 3.8 27B: ~13.1GB peak memory
You must manage this manually:
- Run only one MLX server at a time, or
- Run both on separate machines, or
- Stop one before starting the other
OmniRoute does not automatically manage MLX server processes — it only routes requests to the OpenAI-compatible endpoints you configure.
#### Tool Calling Support
Both models support OpenAI-compatible tool calling. Test with:
```bash
curl -X POST http://localhost:20128/v1/chat/completions \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{
"model": "mlx-gemma/gemma-4-26b",
"messages": [{"role": "user", "content": "What is 2+2? Use the calculator tool."}],
"tools": [{"type": "function", "function": {"name": "calculator", "description": "Calculate", "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}}}]
}'
```
#### Troubleshooting
| Issue | Solution |
| ------------------ | ----------------------------------------------------------------------------- |
| Server won't start | Check `uv run mlx_lm.server --help` and verify model IDs |
| Out of memory | Ensure only one model runs; close other apps; check Activity Monitor |
| Connection refused | Verify server is running on correct port (11435/11436) |
| Slow responses | First request loads model into memory (~30-60s); subsequent requests are fast |
| Tool calling fails | Ensure model supports tools; check OmniRoute logs for translation errors |
---
## Best Free Providers
@@ -239,3 +329,7 @@ Go to Providers → click on the provider → click **Disconnect**.
- **[Free Tiers Guide](./FREE-TIERS-GUIDE.md)** — Get free AI with no credit card
- **[Troubleshooting](../guides/TROUBLESHOOTING.md)** — Fix common issues
- **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** — Full list of 226 providers
## Cursor images
Cursor plan images use `IMAGE_PROVIDERS.cursor` (`cursor-agent-image`). See [CURSOR_IMAGE.md](../providers/CURSOR_IMAGE.md).

View File

@@ -26,6 +26,8 @@ npm install -g omniroute
docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest
```
`:latest` is the highest **published** stable SemVer. It does **not** track git `main`. Pin `diegosouzapw/omniroute:X.Y.Z` for GitOps. See [Image Tags / Release Channels](../guides/DOCKER_GUIDE.md#release-channels).
### Option C: From Source
```bash

View File

@@ -22,6 +22,7 @@ lastUpdated: 2026-06-28
- [Docker Compose with Caddy (HTTPS)](#docker-compose-with-caddy-https-auto-tls)
- [Cloudflare Quick Tunnel](#cloudflare-quick-tunnel)
- [Image Tags](#image-tags)
- [Availability: default SQLite is single-replica](#availability-default-sqlite-is-single-replica)
- [Important Notes](#important-notes)
---
@@ -341,13 +342,15 @@ For orchestrators (Kubernetes, Nomad, etc.):
| Probe | Prefer | Avoid |
| --- | --- | --- |
| Liveness | TCP on the main port (`PORT`, default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` as liveness |
| Liveness | HTTP `GET /livez`, or TCP on the main port (`PORT`, default `20128`) | `/api/monitoring/health` as liveness |
| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
| Deep / blackbox | `/api/monitoring/health` | — |
`/healthz` only reports process lifecycle (`ok` / `starting` / `stopping`). It still
runs on the same Node event loop as request handling, so CPU-bound catalog or
compression work can delay it — busy ≠ dead. Full probe guidance:
`/healthz` reports process lifecycle (`ok` / `starting` / `stopping`). `/livez` is
process-alive only (200 whenever the handler can run; it does not wait for
readiness). Both still run on the same Node event loop as request handling, so
CPU-bound catalog or compression work can delay them — busy ≠ dead. Prefer TCP
liveness if HTTP probes time out. Full probe guidance:
[Monitoring guide — Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations).
## Docker Compose with Caddy (HTTPS Auto-TLS)
@@ -409,8 +412,8 @@ Endpoint tunnel panels (Cloudflare, Tailscale, ngrok) can be shown or hidden fro
| Image | Tag | Size | Description |
| ------------------------ | -------- | ------ | --------------------- |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Current version |
| `diegosouzapw/omniroute` | `latest` | ~250MB | Highest **published** stable SemVer (not git `main`) |
| `diegosouzapw/omniroute` | `3.8.0` | ~250MB | Pin this class of tag for GitOps |
Multi-platform manifest: `linux/amd64` + `linux/arm64` native (Apple Silicon, AWS Graviton, Raspberry Pi). Docker selects the matching architecture automatically; pass `--platform linux/amd64` if you need to force AMD64 emulation on ARM hosts.
@@ -421,7 +424,7 @@ OmniRoute publishes separate Docker channels for stable releases, active release
| Channel | Source | Mutability | Recommended use |
| ------------------------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- |
| `:<version>` / `:<version>-web` | Signed/versioned release | Immutable | Production deployments that pin an exact release |
| `:latest` / `:latest-web` | Highest stable release | Mutable stable pointer | Production deployments that intentionally follow stable releases |
| `:latest` / `:latest-web` | Highest **published** stable SemVer | Mutable stable pointer | Follows stable releases **after** a SemVer publish job — does **not** track `main` or unreleased `release/v*` commits |
| `:next` / `:next-web` | Current default `release/v*` branch | Mutable pre-release pointer | Testing fixes that have landed on the active release branch but are not yet in a stable release |
| `:main` / `:main-web` | `main` branch | Mutable development pointer | Development and integration testing only |
@@ -465,6 +468,37 @@ docker compose up -d
A release-branch build can never move `latest`; only an eligible stable semantic version may promote the stable pointer. The `next` images retain the release image inspection and blocking CRITICAL-vulnerability gate.
**`latest` is not a currency guarantee for git.** Merged fixes on `main` or on the active `release/v*` branch are **not** in `:latest` until a stable SemVer image is published and the publish job promotes `:latest` (same digest as that SemVer). If `latest` looks frozen while GitHub already shows the fix, pull `:next` to test the release branch or wait for the SemVer tag.
| You want | Use |
| --- | --- |
| GitOps / production that must not drift | Pin `:X.Y.Z` (or the image digest) |
| Follow published stables and accept a recreate on each release | `:latest` |
| Test unreleased `release/v*` commits | `:next` (not production) |
| Test `main` | `:main` (not production) |
## Availability: default SQLite is single-replica
Stock Docker / Kubernetes OmniRoute is **one Node process + one SQLite writer**. High availability is **not supported** on that topology.
| Constraint | Consequence |
| --- | --- |
| Single writer | Do **not** run multiple replicas against the same SQLite file. That corrupts the DB. |
| Recreate / restart / HEALTHCHECK kill | **Full outage** of in-flight SSE, dashboard sessions, and in-memory state. Every connected client drops. |
| Same event loop as `/healthz` | A busy catalog or compression tick can delay probes; a short timeout then restarts the **only** replica. |
**Probe matrix** (see also [Kubernetes probe recommendations](../ops/MONITORING_GUIDE.md#kubernetes-probe-recommendations)):
| Probe | Target | Do not use |
| --- | --- | --- |
| Liveness | TCP on `PORT` (default `20128`), or soft HTTP `/healthz` | `/api/monitoring/health` |
| Readiness | HTTP `GET /healthz` | Tight timeouts that treat event-loop busy as dead |
| Deep / humans | `/api/monitoring/health` | Automated kubelet liveness |
**Upgrades:** expect every session to drop. Drain clients if you can; there is no rolling update on default SQLite. Compose `restart: unless-stopped` plus Docker `HEALTHCHECK` will also replace the only process when the container is Unhealthy — same blast radius.
External Postgres / multi-writer HA is **not** a documented stock path. If you need HA, keep a single replica or run a topology the project has tested and documented separately.
## Important Notes
- **SQLite WAL Mode:** `docker stop` should be allowed to finish so OmniRoute can checkpoint the latest changes back into `storage.sqlite`. The bundled Compose files already set a 40s stop grace period. If you run the image directly, keep `--stop-timeout 40`.

View File

@@ -169,7 +169,7 @@ stale data.
The sync runs **on by default**:
- It runs once at server startup and then on a periodic timer
(`src/lib/arenaEloSync.ts`, wired from `src/server-init.ts`).
(`src/lib/arenaEloSync.ts`, wired from `src/instrumentation-node.ts`).
- It is **non-blocking and never fatal** — if the upstream fetch fails, OmniRoute keeps
running and the rankings simply show the last good data (or an empty state).

View File

@@ -1,47 +1,159 @@
---
title: "Management Authentication"
version: 3.8.50
lastUpdated: 2026-08-05
lastUpdated: 2026-08-20
---
# Management Authentication
OmniRoute uses four distinct credential families for management access. This guide
distinguishes them by purpose, scope, and locality.
OmniRoute has **four credential families** that can authorize management routes.
They are not interchangeable. Inference API keys (`sk-…`) do **not** manage the
server unless they were explicitly granted `manage` or `admin` scope.
| Credential | Scope | Locality | Use Case |
|-------------------------|--------------------|---------------|-----------------------------------|
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
| Manage-scope API key | `manage` scope | External | Management API calls |
Canonical implementation: `src/lib/api/requireManagementAuth.ts`.
## Dashboard JWT Session
| Credential | Typical form | Created where | Intended use | Management capability |
|---|---|---|---|---|
| Dashboard session | `auth_token` cookie | Dashboard login | Browser UI | Full dashboard management, subject to CSRF, locality, and always-protected-route rules |
| Local CLI machine token | internal / local | CLI bootstrap (`omniroute` on the same machine) | Local CLI | Local management only |
| Scoped Access Token | `oma_live_…` | **Settings → Access Tokens** or `omniroute connect` | Remote CLI and management API | Must satisfy the route's required `read`, `write`, or `admin` scope |
| Inference API key | `sk-…` (and other API-key prefixes) | **API Manager / API Keys** | `/v1/*` inference | **None** unless the key metadata includes `manage` or `admin` |
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
Valid for the session duration. Cannot be used from external hosts.
`oma_` credentials are management/CLI credentials. They are **not** inference API keys.
## CLI Machine-ID Token
If login/API-key auth is disabled for the server, some management routes may
accept unauthenticated calls. Local-only and always-protected routes still apply
their own rules. Presenting one of these credentials is therefore not universally
mandatory, and possessing one is not universally sufficient without the required
scope and route locality.
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
Used by the CLI for all management operations. Tied to the machine identity.
Related: [Remote Mode](./REMOTE-MODE.md) (how `oma_live_…` is minted for a remote CLI).
## Scoped `oma_` Access Token
---
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
## Scope matrices
## Manage-Scope API Key
These two scope vocabularies are **different**. Do not mix them.
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
Used for management API calls from external hosts.
### Access Token scopes (`oma_live_…`)
## Header Examples
| Scope | Typical operations |
|---|---|
| `read` | List/status GETs that the token is allowed to see |
| `write` | Mutations (create/update/delete) below admin |
| `admin` | Full remote CLI / connect token (password bootstrap defaults here) |
```
Authorization: Bearer oma_abc123def456
Authorization: Bearer <standard-api-key-with-manage-scope>
Cookie: omniroute_session=<jwt-token>
A token with `read` cannot call a `write` route. Runtime message shape:
`Access token scope '<have>' is insufficient; '<need>' required.`
### API-key management scopes
| Scope | Meaning |
|---|---|
| (none) | Inference only. Management routes return 403. |
| `manage` | Management API (same gate as `requireManagementAuth` API-key branch) |
| `admin` | Also satisfies `hasManageScope` (treated as management-capable) |
Enable `manage` on the key in the API Keys / API Manager UI. Do not reuse a
chat client key for automation unless you deliberately granted that scope.
---
## How to create and revoke
### Dashboard session
1. Open `/login`, sign in with the management password (`INITIAL_PASSWORD` on first boot).
2. Cookie `auth_token` is HttpOnly. Browser dashboard uses it automatically.
3. Log out via `/api/auth/logout`. There is no long-lived secret to copy.
### Local CLI machine token
1. Run `omniroute` on the **same host** as the server (loopback).
2. The CLI bootstraps a machine-id token under `~/.omniroute/` (chmod 600).
3. This does **not** work from another machine. Use an Access Token for remote CLI.
### Scoped Access Token (`oma_live_…`)
1. Dashboard: **Settings → Access Tokens** → create (name + scope). **The secret is shown once.**
2. Or CLI: `omniroute connect <host>` (password → token). See [Remote Mode](./REMOTE-MODE.md).
3. Header: `Authorization: Bearer oma_live_…`
4. Revoke from the same Access Tokens page (or delete the CLI context).
5. Server stores only a hash. Treat the plaintext like a password.
### Manage-scoped API key
1. Dashboard: **API Manager / API Keys** → create or edit a key → enable `manage` (or `admin`).
2. Header: `Authorization: Bearer sk-…` (the key's actual prefix).
3. Revoke or strip `manage` in the same UI.
4. Least privilege for automation that is not the CLI: prefer a `read` Access Token for GET-only jobs; use `manage` on an API key only when the caller must also speak `/v1` and management.
---
## Header format
```http
Authorization: Bearer oma_live_<secret>
Authorization: Bearer sk-<secret>
Cookie: auth_token=<dashboard-jwt>
```
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.
Do not put management credentials in the URL path or query string. Management
auth is header/cookie only.
---
## Copy-paste examples
Read-only (list providers). Use a `read` Access Token:
```bash
curl -sS "$OMNIROUTE_URL/api/providers" \
-H "Authorization: Bearer oma_live_<read-token>"
```
Modifying (create a provider connection). Use `write`/`admin` Access Token or a
manage-scoped API key:
```bash
curl -sS -X POST "$OMNIROUTE_URL/api/providers" \
-H "Authorization: Bearer oma_live_<write-or-admin-token>" \
-H "Content-Type: application/json" \
-d '{"provider":"openai","apiKey":"<upstream-key>"}'
```
Inference (not management). Ordinary API key, no `manage` required:
```bash
curl -sS "$OMNIROUTE_URL/v1/models" \
-H "Authorization: Bearer sk-<inference-key>"
```
---
## Current runtime errors (do not echo secrets)
| Situation | Typical status | Message (sanitized) |
|---|---|---|
| No credential | 401 | `Authentication required` |
| Invalid/expired `oma_live_…` | 401 | `Invalid or expired access token` |
| Valid API key without `manage`/`admin` | 403 | `API key lacks 'manage' scope. Enable it in the API Keys dashboard.` |
| Invalid ordinary API key on a management route | 403 | `Invalid management token` |
| Access Token scope too low | 403 | `Access token scope '<have>' is insufficient; '<need>' required.` |
"Invalid management token" means the bearer was **not** accepted as a management
credential. It does **not** tell you which family to mint. Use the table above:
inference keys need `manage` scope; remote CLI needs `oma_live_…`; the dashboard
uses the session cookie.
---
## Recommended least-privilege choice
| Caller | Use |
|---|---|
| Browser | Dashboard session |
| CLI on the server host | Machine token |
| CLI on a laptop talking to a remote server | `oma_live_…` from `omniroute connect` |
| CI / scripts (management only) | `oma_live_…` with the smallest scope that works |
| CI that must call both `/v1` and `/api` | API key with `manage` **or** two credentials |

View File

@@ -2069,6 +2069,39 @@ paths:
description: Created combo
/api/combos/{id}:
get:
tags: [Combos]
summary: Get combo by ID
parameters:
- $ref: "#/components/parameters/ResourceId"
responses:
"200":
description: Combo details
"404":
description: Combo not found
put:
tags: [Combos]
summary: Update combo
description: >-
Partial update: the body is merged onto the stored combo, so a field left out keeps
its current value. An array that IS sent replaces the stored one outright.
parameters:
- $ref: "#/components/parameters/ResourceId"
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
"200":
description: Updated combo
"400":
description: Invalid body, or the resulting combo fails validation
"404":
description: Combo not found
"409":
description: Name already taken, or the combo is quota-share managed
patch:
tags: [Combos]
summary: Update combo
@@ -7329,12 +7362,18 @@ components:
BearerAuth:
type: http
scheme: bearer
description: API key obtained from the OmniRoute dashboard
description: >
Two bearer families are accepted. Inference API keys (typically `sk-…`)
authorize `/v1/*`. Management routes also accept `oma_live_…` Access Tokens
(Settings → Access Tokens / `omniroute connect`) and API keys whose metadata
includes `manage` or `admin` scope. See docs/guides/MANAGEMENT-AUTH.md.
Bearer credentials are accepted on management routes that use this scheme;
they are not rejected solely for being Bearer.
ManagementSessionAuth:
type: apiKey
in: cookie
name: auth_token
description: Dashboard management session cookie for protected management routes
description: Dashboard management session cookie (auth_token) for protected management routes. Distinct from Bearer Access Tokens and API keys. See docs/guides/MANAGEMENT-AUTH.md.
parameters:
ResourceId:

View File

@@ -453,26 +453,47 @@ Run monthly during low-traffic windows. (WAL mode reduces the need, but doesn't
`src/lib/db/healthCheck.ts` provides **DB-level health diagnostics**:
````bash
GET /api/db/health
Both verbs require authentication (`401` otherwise). `GET` diagnoses only; `POST` runs the
same check with `autoRepair` enabled.
Returns:
```bash
GET /api/db/health # diagnose
POST /api/db/health # diagnose + repair
```
The response is the `DbHealthCheckResult` produced by `runDbHealthCheck()`
(`src/lib/db/healthCheck.ts`):
```json
{
"status": "healthy",
"checks": {
"writable": { "status": "pass" },
"integrity": { "status": "pass", "result": "ok" },
"foreign_keys": { "status": "pass", "violations": 0 },
"orphaned_artifacts": { "status": "warn", "count": 12 },
"table_sizes": {
"usage_history": { "rows": 12345, "size_mb": 12.3 },
"call_logs": { "rows": 567, "size_mb": 2.1 }
"isHealthy": false,
"issues": [
{
"type": "broken_reference",
"table": "domain_budgets",
"description": "Domain budgets referenced API keys that no longer exist.",
"count": 2
}
}
],
"repairedCount": 0,
"backupCreated": false,
"autoRepair": false,
"checkedAt": "2026-08-18T09:00:00.000Z",
"driver": { "name": "better-sqlite3", "degraded": false }
}
````
```
| Field | Meaning |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `isHealthy` | `true` when `issues` is empty. `driver` never influences it. |
| `issues[].type` | One of `integrity_check_failed`, `broken_reference`, `stale_snapshot`, `invalid_state`. |
| `repairedCount` | Rows repaired during this run; always `0` when `autoRepair` is false. |
| `backupCreated` | Whether a backup was taken before repairing. |
| `checkedAt` | ISO timestamp shared by the run and by any repair note it writes. |
| `driver.name` | SQLite driver serving the checked database. |
| `driver.degraded` | `true` when writes are not durably backed by the database file — the `sql.js` WASM fallback (whole-file persistence) or an in-memory database. |
The same payload is returned by the `omniroute_db_health_check` MCP tool.
Run `PRAGMA integrity_check` to detect corruption:

View File

@@ -157,13 +157,13 @@ Response:
### Kubernetes probe recommendations
OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets `/api/monitoring/health` — that is **too heavy** for kubelet liveness intervals.
OmniRoute is a **single Node process** (one event loop). Stock Docker `HEALTHCHECK` targets lightweight `/healthz`. `/api/monitoring/health` is **too heavy** for kubelet liveness intervals.
| Probe | Recommended target | Notes |
| --- | --- | --- |
| **Startup** | HTTP `GET /healthz` with a long `failureThreshold` (or large `startPeriod`) | Cold start + SQLite migration can exceed a few seconds |
| **Readiness** | HTTP `GET /healthz` | Remove endpoints while starting/stopping; still flaps if the loop is CPU-blocked |
| **Liveness** | **TCP** on the main service port (`PORT`, default `20128`), **or** HTTP `/healthz` with soft thresholds | Do **not** kill the pod on short event-loop stalls; busy ≠ dead |
| **Readiness** | HTTP `GET /healthz` | Lifecycle `ok` / `starting` / `stopping` (200 vs 503). Still flaps if the loop is CPU-blocked. A **200 in multiple seconds is not healthy** (#10303) — it means the event loop was starved before the 3-byte handler ran |
| **Liveness** | HTTP `GET /livez`, **or TCP** on the main service port (`PORT`, default `20128`) | `/livez` is process-alive only (always 200 if the handler runs). It still shares the event loop — busy ≠ dead, and it does not detect event-loop starvation (#10303) any better than TCP does. Prefer **TCP** if HTTP probes time out under catalog/compression load; do **not** kill the pod on short event-loop stalls either way |
| **Deep health** | `GET /api/monitoring/health` from an external checker | Not for kubelet `livenessProbe` / tight `readinessProbe` |
Example shape (adjust thresholds to your cold-start and compression load):
@@ -186,17 +186,27 @@ readinessProbe:
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
tcpSocket:
httpGet:
path: /livez
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
# Under event-loop stall HTTP /livez can still time out. TCP is the
# conservative alternative:
# tcpSocket:
# port: http
```
**Do not** point kubelet **liveness** at `/api/monitoring/health`. That path does real DB/monitoring work and will false-positive under load.
Related: [#10052](https://github.com/diegosouzapw/OmniRoute/issues/10052) (probes while the event loop is busy), [#9685](https://github.com/diegosouzapw/OmniRoute/issues/9685) / [#10055](https://github.com/diegosouzapw/OmniRoute/pull/10055) (catalog pricing hog), [#10117](https://github.com/diegosouzapw/OmniRoute/issues/10117) (compression token-count hog).
### Optional request-path work (memory, skills, token refresh)
Memory extraction, skills injection, and OAuth token refresh share the **main Node event loop** with `/healthz`. They are dashboard-toggle features (`memoryEnabled`, `skillsEnabled`), not a worker pool. See [Environment — event-loop cost](../reference/ENVIRONMENT.md#event-loop-cost-of-memory-skills-and-token-refresh-10349).
### Provider Health
> **No REST endpoint.** Provider health data is available via the MCP tool `observability_snapshot` or the dashboard `/dashboard/providers` page.

View File

@@ -66,6 +66,15 @@ as the default reflex (minutes, reversible); `npm unpublish` only inside the 72h
window and never as the first move. Docker: never rewrite a version tag — rollback is
repointing `latest` to the last good digest.
**Docker Hub `latest` (required on every stable SemVer publish):** the
`docker-publish` workflow must tag **both** `X.Y.Z` and, when
`should-promote-latest.sh` agrees this is the highest stable SemVer, `:latest`
with the **same digest**. After the job: Hub `latest` digest equals the new
SemVer digest and `last_updated` moved. Do not leave `:latest` on an older
build while release notes talk about fixes that only exist on git. Compose
quickstarts use `:latest`; GitOps should keep pinning `X.Y.Z`. See
[Docker release channels](../guides/DOCKER_GUIDE.md#release-channels) and #10317.
## Hotfix Fast-Lane (label `hotfix`)
A PR labeled `hotfix` skips the heavy CI matrix (9-shard E2E, coverage ratchet,

View File

@@ -80,3 +80,17 @@ Implementation:
- `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`)
- `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up)
- `src/lib/db/core.ts``ensureDbInitialized()` / `getDriverInfo()` exports
## Single-writer topology (HA unsupported)
The driver fallback chain above still runs in **one process**. Default SQLite
OmniRoute is a **single writer**:
- Do not attach two OmniRoute replicas to the same `storage.sqlite` file.
- A container restart, Recreate deploy, OOM kill, or HEALTHCHECK restart drops
every in-flight SSE session. There is no session drain on the stock path.
- Orchestrator liveness that treats a slow `/healthz` as dead will kill the only
replica. Prefer TCP liveness + HTTP `/healthz` readiness. See
[Docker Guide — availability](../guides/DOCKER_GUIDE.md#availability-default-sqlite-is-single-replica)
and [Kubernetes probe recommendations](./MONITORING_GUIDE.md#kubernetes-probe-recommendations).

View File

@@ -0,0 +1,75 @@
---
title: "Cursor Image Generation"
version: 3.8.49
lastUpdated: 2026-07-23
---
# Cursor Image Generation
OmniRoute exposes Cursor plan **image generation** on `POST /v1/images/generations` through the same provider id as chat: `cursor` (alias `cu`).
| Field | Value |
|-------|--------|
| `IMAGE_PROVIDERS` id | `cursor` |
| Format | `cursor-agent-image` |
| Auth | Same OAuth / API-key connection as chat (`provider_connections.provider = "cursor"`) |
| Models | `cursor/auto`, `cursor/composer-2`, `cursor/composer-2.5` |
## Why the Agent CLI
Cursor chat in OmniRoute uses `agent.v1.AgentService/Run` (protobuf). That path **rejects** built-in client tools (shell, write, …). Image generation is a Cursor-native tool executed by the **`agent` CLI** against the seat. The image handler therefore spawns `agent` with a locked prompt and a per-request temp workspace (same shape as community seat bridges), then returns OpenAI-compatible `b64_json`.
## Access restriction (Hard Rules #15 + #17)
This is the only `IMAGE_PROVIDERS` format that spawns a child process (the `agent`
binary). Because `POST /v1/images/generations` is shared by ~40 other, non-spawning
image providers that remote callers legitimately use, the whole route is **not**
classified `LOCAL_ONLY` — instead `handleCursorAgentImageGeneration` enforces its own
gate using the trusted `AUTHZ_HEADER_PEER_LOCALITY` verdict the authz pipeline stamps
on every request (from the real TCP peer, never the spoofable `Host` header): only
`loopback` and `lan` callers may reach the spawn; everything else (including a leaked
API key replayed over a public tunnel) gets `403` before any credential lookup or
process spawn happens. See `src/server/authz/policies/management.ts` for the same
policy applied to the rest of the `LOCAL_ONLY` tier.
## Concurrency gate is module-level (single-instance limitation)
`CURSOR_IMG_MAX_CONCURRENT` is enforced by an in-memory counter/queue scoped to the
Node module instance (`open-sse/handlers/imageGeneration/providers/cursorAgentImage.ts`).
It correctly limits concurrent `agent` spawns within one OmniRoute process, but does
**not** coordinate across multiple processes/instances sharing the same Cursor seat
(e.g. a multi-replica deployment) — each instance enforces its own independent limit.
For a single-instance deployment (the default) this is exact; horizontally scaled
deployments should keep `CURSOR_IMG_MAX_CONCURRENT` conservative per instance or route
Cursor image traffic to a single instance.
## Requirements
1. A connected Cursor account in the dashboard (OAuth or `crsr_…` API key).
2. The Cursor Agent binary available to the OmniRoute process:
- env `CURSOR_AGENT_BIN=/path/to/agent`, or
- `~/.local/bin/agent`, or
- `providerSpecificData.agentBin` on the Cursor connection.
Optional tuning:
| Env | Default | Meaning |
|-----|---------|---------|
| `CURSOR_IMG_TIMEOUT_MS` | `210000` | Per-image wall clock |
| `CURSOR_IMG_MAX_CONCURRENT` | `2` | Shared-seat concurrency gate |
| `CURSOR_IMG_MODEL` | (request model / `auto`) | Override CLI `--model` |
## Example
```bash
curl -sS https://<host>/v1/images/generations \
-H "Authorization: Bearer <omni-api-key>" \
-H "Content-Type: application/json" \
-d '{"model":"cursor/auto","prompt":"a lantern in fog","size":"1024x1024"}'
```
Generation typically takes 12 minutes. Prefer an internal network path; edge proxies with ~100s timeouts will fail.
## LiteLLM
Register an image model with `mode: image_generation`, `api_base: http://omniroute:20128/v1`, and `model: openai/cursor/auto` (or bare `cursor/auto` depending on your LiteLLM version).

View File

@@ -103,7 +103,7 @@ The manual import endpoint can also be called directly:
```
POST /api/providers/zed/manual-import
Content-Type: application/json
Authorization: Bearer <management-token>
Authorization: Bearer <oma_live_or_manage_scoped_api_key>
{
"provider": "openai",

View File

@@ -703,6 +703,10 @@ X-OmniRoute-No-Cache: true
## Dashboard & Management
Management routes (`/api/*` except public auth/login) are **not** authorized by
ordinary inference API keys. Credential families, scopes, and curl examples:
[Management Authentication](../guides/MANAGEMENT-AUTH.md).
### Authentication
| Endpoint | Method | Description |
@@ -1668,9 +1672,14 @@ See [Security > Guardrails](../security/GUARDRAILS.md) for full details.
## Authentication
See [Management Authentication](../guides/MANAGEMENT-AUTH.md) for the four
credential families (dashboard session, local CLI token, `oma_live_…` Access
Token, manage-scoped API key) and how they differ from inference keys.
- Dashboard routes (`/dashboard/*`) use `auth_token` cookie
- Login uses saved password hash; fallback to `INITIAL_PASSWORD`
- `requireLogin` toggleable via `/api/settings/require-login`
- `/v1/*` routes optionally require Bearer API key when `REQUIRE_API_KEY=true`
- "management token" / "management-scoped API key" in this reference means one of the families in that guide — not an undefined extra secret type
> **Breaking change (v3.8.0)** — `/api/v1/agents/tasks/*` and the cooldown management endpoints now require **management auth** (dashboard `auth_token` cookie or a management-scoped API key). Clients that previously called these routes unauthenticated will receive `401 Unauthorized`. See commit `588a0333` (`fix(auth): require management auth for agent and cooldown APIs`).

View File

@@ -91,7 +91,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`<dir>/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). |
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips automatic + pre-write SQLite file backups (startup, models.dev pricing save/clear, settings writes). Manual and pre-restore backups still run. Non-manual backups are also **throttled to at most once per 60 minutes** so hourly models.dev sync does not copy the whole DB on every pricing write. Dashboard **Settings → Storage** can disable auto-backup independently. |
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
@@ -153,6 +153,8 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. |
| `OMNIROUTE_USE_TURBOPACK` | `1` (Turbopack — code default) | `package.json` / Next.js 16 | Turbopack is the default bundler for `npm run dev` and `npm run build` (2-3× faster builds, benchmarked). Set to `0` to fall back to webpack on Windows, when running into native binding / bundler-compat incompatibilities, **or on RAM-constrained machines** — Turbopack production builds on this Next.js version line (16.2.x) are known upstream to peak far higher in memory than webpack on large module graphs (Next 16.3's Turbopack memory-eviction fix is not yet stable); webpack fallback peaks much lower. See #6409. |
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. |
| `NOTIFY_SOCKET` | _(unset)_ | systemd (sd_notify protocol) | Set by systemd when the process runs under a service unit with sd_notify integration; OmniRoute reads it (see `OMNIROUTE_DISABLE_SD_NOTIFY`) to send READY/WATCHDOG notifications. Never set by the user. |
| `OMNIROUTE_DISABLE_SD_NOTIFY` | _(unset)_ | `scripts/dev/systemd-notify.mjs` | Set to `1` to disable systemd sd_notify (Type=notify / WatchdogSec=) even when running under a systemd unit. The notifier is a no-op outside systemd regardless. |
| `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). |
| `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. |
| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. Search providers (SEARCH_VALIDATOR_CONFIGS in `src/lib/providers/validation/searchProviders.ts`, e.g. `tavily-search`) are always excluded from the sweep — their "validation" is a real billed upstream query, so they are never health-checked on a timer (#9970). |
@@ -347,6 +349,7 @@ Route upstream LLM provider calls through an HTTP or SOCKS5 proxy for egress con
| `HTTP_PROXY` | _(unset)_ | Node.js standard | HTTP proxy for upstream calls. |
| `HTTPS_PROXY` | _(unset)_ | Node.js standard | HTTPS proxy for upstream calls. |
| `ALL_PROXY` | _(unset)_ | Node.js standard | Universal proxy (supports `socks5://`). |
| `OMNIROUTE_PROXY_ECHO_URL` | _(unset)_ | `src/lib/proxyEchoTarget.ts` | Pins the echo-IP target used by proxy egress probes to a single URL. Unset, the probe tries `api64.ipify.org` then `api4.ipify.org` so IPv4-only tunnels are not reported dead (#9694). |
| `NO_PROXY` | _(unset)_ | Node.js standard | Comma-separated hostnames/IPs to bypass the proxy. |
| `OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS` | `32` | `open-sse/utils/proxyDispatcher.ts` | Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher. Long-lived SSE streams such as Codex `/v1/responses` need more than one connection when several requests share the same account-level proxy. Values above `256` are capped. |
| `SOCKS_HANDSHAKE_TIMEOUT_MS` | `10000` | `open-sse/utils/socksConnectorWithFamily.ts` | SOCKS5 handshake (connect) timeout in ms. Raise it when a single residential gateway host is hit by high concurrency (e.g. 100 simultaneous requests) — the real handshake can exceed 10s under a saturated pool even though the proxy is reachable, which otherwise surfaces as a false `[Proxy Fast-Fail] Proxy unreachable`. Capped at `120000`. |
@@ -502,7 +505,7 @@ detection above).
| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | `false` | `open-sse/mcp-server/descriptionCompressor.ts` | Compress MCP tool descriptions before serializing the manifest. Enable values: `1`, `true`, `on`. |
| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. |
| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. |
| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/server-init.ts` | Provider rate-limit and quota polling interval. |
| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/lib/usage/providerLimits.ts` | Provider rate-limit and quota polling interval. |
| `PROVIDER_LIMITS_SYNC_SPACING_MS` | `1500` | `src/lib/usage/providerLimits.ts` | Gap (ms) between consecutive OAuth quota fetches in a bulk sync; OAuth connections are fetched one at a time to avoid bursting an upstream. `0` opts out (concurrent). |
| `OMNIROUTE_QUOTA_FETCH_MIN_INTERVAL_MS` | `250` | `open-sse/services/quotaFetchThrottle.ts` | Min interval (ms) between consecutive upstream quota fetches on the per-request preflight/monitor path; spaces concurrent network calls so many accounts on one IP don't burst the upstream. Wired into the Codex (`/wham/usage`), DeepSeek, Bailian (both fetch sites), OpenCode, and Crof quota fetchers (#6009, #6911). The generic `usage.ts::getUsageForProvider` dispatch path (github/glm/minimax/nanogpt/xai/etc.) is not yet covered — tracked separately. Cache hits unaffected. `0` disables; clamped `0..5000`. |
| `PROVIDER_LIMITS_POST_USAGE_REFRESH_DELAY_MS` | `5000` | `src/lib/usage/providerLimits.ts` | Delay (ms) before refreshing provider limits after a real usage event, giving the upstream quota API time to register consumption. |
@@ -729,6 +732,7 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_AGENT_GOAL_STREAM_RECOVERY` | `true` | Enable early stream recovery automatically for detected `/goal` agent runs. Set `false`/`0`/`off` to disable the goal-specific opt-in. This can only ADD recovery on top of the operator default — it never overrides an explicit `STREAM_RECOVERY_ENABLED`/DB settings opt-out. |
| `OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS` | _(off)_ | Strip non-standard `codex.*` SSE events (e.g. `codex.rate_limits`) that break the OpenAI SDK's `responses.stream()` with a 502. Set `true`/`1`/`yes` to enable. |
| `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive response headers. |
| `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS` | `30000` (30s) | Maximum response-start wait (ms) for each direct no-proxy attempt. A timeout retries once on a fresh socket; set `0` to disable the bound and retain the previous behavior. |
| `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive the full response body. |
| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | TCP connection establishment timeout. |
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive socket idle timeout. |
@@ -752,6 +756,7 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). |
| `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_PPLX_SEARCH_HINT` | `0` (off) | Appends "You have built-in web search. Answer questions directly using search results." to the caller's system message (`perplexity-web/protocol.ts`). Off by default — Perplexity searches anyway, and the sentence leaks into replies as meta-commentary for coding clients. Set `1`/`true`/`yes`/`on` to restore. |
| `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`grokTlsClient.ts`). |
| `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. |
| `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`notionTlsClient.ts`); the `notion-web` executor raises it per-request to `180000` for long generations. |
@@ -841,7 +846,7 @@ The logging system writes to both stdout and rotated log files. All configuratio
| Variable | Default | Description |
| -------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OMNIROUTE_MEMORY_MB` | _auto_ | Runtime V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. Set explicitly to override. Docker standalone and `omniroute serve` use it to set `--max-old-space-size`. |
| `OMNIROUTE_MEMORY_MB` | _auto_ | **Recommended** Docker/standalone V8 heap limit (MB). When unset, calibrated dynamically (~35% of system RAM, clamped to `[512, 4096]`); `512` is only the floor when total memory can't be read. On `run-standalone.mjs` (Docker CMD), an **explicit** value is appended as `--max-old-space-size` and **wins** over a conflicting NODE_OPTIONS heap flag (V8 last-flag). `omniroute serve` still prefers an existing NODE_OPTIONS heap (#5238). Do not set both to different numbers — the process logs a warn naming both values and the winner. |
| `PROMPT_CACHE_MAX_SIZE` | `50` | Max cached system prompt entries. |
| `PROMPT_CACHE_MAX_BYTES` | `2097152` (2 MB) | Max total prompt cache size. |
| `PROMPT_CACHE_TTL_MS` | `300000` (5 min) | Prompt cache entry TTL. |
@@ -860,6 +865,19 @@ The logging system writes to both stdout and rotated log files. All configuratio
### Memory Engine (plan 21)
### Event-loop cost of memory, skills, and token refresh (#10349)
OmniRoute is a **single Node process**. Memory extraction/retrieval, skills injection, and provider token refresh run on that **same event loop** as `GET /healthz` and the dashboard. They are not a worker thread.
| Work | Code | Default | Operator control |
| --- | --- | --- | --- |
| Memory extraction / retrieval | `src/lib/memory/` | Dashboard **memoryEnabled** (default on) | Turn off **Settings → Memory**. There is no separate env kill switch beyond disabling the feature in settings. |
| Skills injection | `src/lib/skills/injection.ts` | Dashboard **skillsEnabled** (default on) | Turn off **Settings → Memory/Skills** (`skillsEnabled`). Sandbox knobs below only bound execution after injection is already on. |
| Token refresh | `src/sse/services/tokenRefresh.ts` | On for connected OAuth/web providers | Disconnect the provider or let tokens stay valid; there is no `TOKEN_REFRESH=0` env today. |
If `/healthz` is slow on a quiet box, disable memory + skills first, then check catalog/compression load (#10303, #9685). These features yield at `await` points but still compete for the one thread.
Embedding layer, vector store and reranking knobs for the persistent memory subsystem (`src/lib/memory/`).
| Variable | Default | Description |
@@ -968,7 +986,7 @@ desktop install.
| Variable | Default | Source File | Description |
| ----------------------------------- | ------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. |
| `MODELS_DEV_SYNC_ENABLED` | _(unset)_ | `src/lib/modelsDevSync.ts` | Hard override for models.dev pricing sync. Unset = honor Settings > AI (`modelsDevSyncEnabled`). `0`/`false`/`off`/`no` **wins over the DB** and skips both periodic sync and `getModelsDevPricing()` SQL/JSON scans (recovery when the dashboard is wedged on the same event loop). `1`/`true`/`on`/`yes` forces sync on. Pricing save/clear still call `backupDbFile("pre-write")`, which is no-op under the 60-minute throttle or `DISABLE_SQLITE_AUTO_BACKUP`. |
| `MODELS_DEV_SYNC_INTERVAL` | `86400` (24h) | `src/lib/modelsDevSync.ts` | Development-time model catalog sync interval in seconds. |
| `CONTEXT_WINDOW_RECONCILE_INTERVAL` | `86400` (24h) | `src/lib/contextWindowResolver.ts` | Interval (seconds) for the self-correcting context-window reconciler (5004): pins provider-declared windows from `/models` discovery as `auto:discovery` overrides when they diverge from the catalog. Set to `0` to disable. Reuses already-synced data (no new fetch); never overwrites `manual` overrides. |
@@ -1030,7 +1048,10 @@ Anthropic-compatible provider instead.
| `PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS` | `2000` | `src/lib/proxyHealth.ts` | Cache TTL for failed proxy health probes. Keep this shorter than `PROXY_HEALTH_CACHE_TTL_MS` so transient proxy timeouts under high concurrency retry quickly without disabling fast-fail for truly dead proxies. |
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/probeTarget.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
| `PROXY_HEALTH_TEST_CONCURRENCY` | `10` | `src/lib/proxyHealth/probeTarget.ts` | Probes started at once per batch, shared by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Floored at 1 and capped at 50. |
| `PROXY_HEALTH_TEST_STAGGER_MS` | `100` | `src/lib/proxyHealth/probeTarget.ts` | Delay in ms between two probe departures inside a batch. Without it the whole batch leaves at the same moment and a shared egress IP can trip a rate-limited target. Set to `0` to disable the spacing; capped at 5000. |
| `PROXY_HEALTH_USE_PROVIDER_TARGET` | `true` | `src/lib/proxyHealth/providerProbeTarget.ts` | Set "false" to stop probing the real host of a proxy's assigned provider (`GET /models`, no API key) and always use `PROXY_HEALTH_TEST_URL` instead. |
| `PROXY_HEALTH_AUTO_DEACTIVATE` | `false` | `src/lib/proxyHealth/statusPolicy.ts` | When `false` (default), automated reachability probes (the scheduler + the `/api/settings/proxies/auto-test` "Test All" button) are **read-only** and never write a proxy's status — only the operator sets active/inactive, so a flaky probe can't strand an assigned proxy (#6246). Set `true` to restore the legacy test-and-set behaviour. |
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |

View File

@@ -1,16 +1,16 @@
---
title: "Provider Reference"
version: 3.8.50
lastUpdated: 2026-08-19
lastUpdated: 2026-08-20
---
# Provider Reference
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-08-19
> **Last generated:** 2026-08-20
Total providers: **342**. See category breakdown below.
Total providers: **343**. See category breakdown below.
## Categories
@@ -62,8 +62,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `clinepass` | `cp` | ClinePass | OAuth | [link](https://cline.bot/cline-pass) | ClinePass is Cline's $9.99/mo subscription bundling 10 open coding models. Sign in with your Cline account (same login as the Cline CLI/IDE), or paste a direct ClinePass API key (app.cline.bot → Settings → API Keys). A ClinePass subscription unlocks the cline-pass/* models. Reuses the Cline WorkOS OAuth flow. |
| `codebuddy-cn` | `cbcn` | CodeBuddy CN | OAuth | [link](https://copilot.tencent.com) | Tencent CodeBuddy CN (copilot.tencent.com). Sign in via the official CLI device-code flow, or paste a direct API key (sent as Authorization: Bearer). Catalog: GLM / Kimi / MiniMax / DeepSeek / Hunyuan. |
| `codex` | `cx` | OpenAI Codex | OAuth | — | — |
| `cursor` | `cu` | Cursor IDE | OAuth | — | — |
| `devin-cli` | `dv` | Devin CLI | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
| `cursor` | `cu` | Cursor IDE | OAuth, image | — | Image via Agent CLI (`CURSOR_AGENT_BIN`); same seat as chat |
| `devin-cli` | `dv` | Devin CLI (Official) | OAuth | [link](https://cli.devin.ai) | Requires the Devin CLI binary. Run `devin auth login` to authenticate, or provide your WINDSURF_API_KEY. Install: https://cli.devin.ai |
| `devin-desktop` | — | Devin Desktop | OAuth | [link](https://devin.ai) | Paste an existing Devin API key from an authenticated Devin session. Key export availability and steps vary by Devin version and account. |
| `ghe-copilot` | `ghe-copilot` | GitHub Enterprise Copilot | OAuth | — | Enter your GHE instance URL (e.g., https://ghe.company.com) in provider settings, then authenticate via device flow. |
| `github` | `gh` | GitHub Copilot | OAuth | — | — |
@@ -120,7 +120,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zai-web` | `zw` | Z.ai Web | Web cookie | [link](https://chat.z.ai) | Copy the "token" value from chat.z.ai → DevTools → Application → Local Storage. Do not copy cookies; OmniRoute handles the per-request CAPTCHA through its browser transport. | — |
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. | — |
## API Key Providers (paid / paid-with-free-credits) (229)
## API Key Providers (paid / paid-with-free-credits) (230)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -323,9 +323,9 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `thebai` | `thebai` | TheB.AI | API key, aggregator | [link](https://theb.ai) | Bearer API key for the TheB.AI OpenAI-compatible gateway. |
| `tinyfish` | `tf` | TinyFish Fetch | API key | [link](https://docs.tinyfish.ai/fetch-api) | X-API-Key from agent.tinyfish.ai/api-keys |
| `together` | `together` | Together AI | API key, video | [link](https://www.together.ai) | — |
| `token-kiosk` | `tk` | Token Kiosk | API key | [link](https://agent-router.gaib.ai) | Use your Token Kiosk API key in Authorization: Bearer <key>. Fully OpenAI-compatible gateway. API base URL: https://agent-router.gaib.ai/v1. |
| `tokenreply` | `tokenreply` | TokenReply | API key, aggregator | [link](https://www.tokenreply.com) | Free-tagged models have model- and campaign-specific daily limits; no fixed global free quota is published. |
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer *** Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
| `token-kiosk` | `tk` | Token Kiosk | API key | [link](https://agent-router.gaib.ai) | Use your Token Kiosk API key in Authorization: Bearer *** Fully OpenAI-compatible. API base URL: https://agent-router.gaib.ai/v1. |
| `tokenrouter` | `trk` | TokenRouter | API key | [link](https://tokenrouter.com) | Use your TokenRouter API key in Authorization: Bearer <key>. Fully OpenAI-compatible. API base URL: https://api.tokenrouter.com/v1. |
| `topaz` | `topaz` | Topaz | API key, image | [link](https://topazlabs.com) | — |
| `typhoon` | `typhoon` | Typhoon | API key | [link](https://docs.opentyphoon.ai) | Free API key with a 5 req/s and 200 req/m rate limit. |
| `udio` | `udio` | Udio | API key | [link](https://udio.com) | Paste session cookie from udio.com (Supabase auth) |
@@ -355,7 +355,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `zerolimitai` | `zerolimitai` | ZeroLimitAI | API key, aggregator | [link](https://www.zerolimitai.com) | Temporary free trial is advertised, but official pages conflict between 3 and 7 days; a 100-calls/day claim is not treated as permanent. |
| `zylo-api` | `zylo` | Zylo API | API key, aggregator | [link](https://zyloai.net) | Basic plan: 10 RPM, 7,200 requests/day and 200,000 tokens/day; limited to Basic text models. |
## Local Providers (12)
## Local Providers (14)
| ID | Alias | Name | Tags | Website | Notes |
|----|-------|------|------|---------|-------|
@@ -365,6 +365,8 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
| `llama-cpp` | `llamacpp` | llama.cpp | Local, self-hosted | [link](https://github.com/ggml-org/llama.cpp) | API key optional (use any value, e.g. sk-no-key-required). Configure the llama-server OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). Note: if Llamafile is also installed, both default to port 8080 — run only one at a time or override the port. |
| `llamafile` | `llamafile` | Llamafile | Local, self-hosted | [link](https://github.com/Mozilla-Ocho/llamafile) | API key optional. Configure the local Llamafile OpenAI-compatible base URL (default: http://127.0.0.1:8080/v1). |
| `lm-studio` | `lmstudio` | LM Studio | Local, self-hosted | [link](https://lmstudio.ai) | API key optional. Configure the local LM Studio OpenAI-compatible base URL (default: http://localhost:1234/v1). |
| `mlx-gemma` | `mlx-gemma` | MLX Gemma 26B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11435. Requires `uv` and `mlx-lm` installed. Model: `mlx-community/gemma-4-26B-A4B-it-qat-q4_0-mlx-aligned` (~15.9GB peak memory). |
| `mlx-qwen` | `mlx-qwen` | MLX Qwen 3.8 27B | Local, self-hosted | [link](https://github.com/ml-explore/mlx) | No API key required. Runs mlx-lm server locally on port 11436. Requires `uv` and `mlx-lm` installed. Model: `maglun/Qwen3.8-27B-MLX-Mixed-3.80bpw` (~13.1GB peak memory). |
| `ollama-local` | `ollama` | Ollama | Local, self-hosted | [link](https://ollama.com) | No API key required. Ollama runs locally — configure its OpenAI-compatible base URL (default: http://localhost:11434/v1) and make sure Ollama is running before connecting. |
| `oobabooga` | `ooba` | oobabooga | Local, self-hosted | [link](https://github.com/oobabooga/text-generation-webui) | API key optional. Configure the local oobabooga OpenAI-compatible base URL (default: http://localhost:5000/v1). |
| `sdwebui` | `sdwebui` | SD WebUI | Local | [link](https://github.com/AUTOMATIC1111/stable-diffusion-webui) | No API key required. Configure the local WebUI base URL (default: http://localhost:7860). |

View File

@@ -38,7 +38,7 @@ this service has been disabled in this account (Antigravity)
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
> treat the block above as a snapshot.
Two adjacent, **separate** signal tables live in the same file and are *not* part
Two adjacent, **separate** signal tables live in the same file and are _not_ part
of banned-keyword detection:
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
@@ -70,7 +70,7 @@ upstream error response
narrower **`deactivated`** label (`isActive=false` when the connection has no
spare API keys) is written by the inline `chatCore.ts` path on **HTTP 401 / 403**
(classified via `classifyProviderError``ACCOUNT_DEACTIVATED`). Note the
`markAccountUnavailable()` path writes a *different* terminal status —
`markAccountUnavailable()` path writes a _different_ terminal status —
**`expired`** — for the same `ACCOUNT_DEACTIVATED` signal (via
`resolveTerminalConnectionStatus`), so the same ban can surface as either
`deactivated` or `expired` depending on which path handled the response. (The
@@ -86,7 +86,7 @@ every failed upstream request flows through — it is **not** gated to
OAuth/subscription scrapers. The resulting terminal state is per **connection**,
not per provider.
That said, the built-in *strings* are oriented toward subscription/OAuth
That said, the built-in _strings_ are oriented toward subscription/OAuth
providers with real ban risk (ChatGPT Web, Claude Web, Codex, Muse Spark,
Antigravity). An API-key provider will only trip the detector if its error body
literally contains one of the substrings.
@@ -136,14 +136,62 @@ There is no separate "clear ban flag" button — recovery is re-test, re-auth, o
re-enable, matching the general terminal-state rule in
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md).
## Probe isolation (model test-all)
A **probe-origin failure** (model test-all / health-check dispatches executed
inside `runAsProbe`) never removes a connection from the pool (#9817): it is
**recorded for visibility** (`last_error`, `last_error_type`, `error_code`,
`last_error_at`) but skips **every** routing mutation — cooldowns, terminal
status (`banned` / `deactivated` / `credits_exhausted`), per-model lockouts,
the provider circuit breaker, the 5-minute quota cache, OAuth token refresh
and auto-disable. Only a real request-path failure deactivates. The recorded
error is what makes a flagged account visible in the dashboard while it stays
serving traffic.
The single decision point is `shouldIsolateProbeFailures()`
(`src/shared/utils/probeOrigin.ts`), consulted by **every** site that could
mutate routing state from a probe-origin failure:
- `markAccountUnavailable` (`auth.ts`) — record-only (`lastError` raw text,
`lastErrorType`, `errorCode`, `lastErrorAt`; deliberately **no**
`backoffLevel`, which would trigger the selection-time auto-decay and wipe
the record)
- `maybeAutoDisableBannedAccount` — no auto-disable
- `chatCore` — FORBIDDEN, ACCOUNT_DEACTIVATED, QUOTA_EXHAUSTED (record-only,
no terminal `credits_exhausted`), GEO_BLOCKED (no 24h exclusion),
MODEL_NOT_FOUND (no `lockModel`), the codex 429 account-rotation failover
(no `markCodexScopeRateLimited`, no persisted `rate_limited_until`, no
session-affinity clear), `persistCodexQuotaState` (no quota-state write,
no cache invalidation), `recordKeyHealthStatus` (key-health rotator
untouched)
- OAuth refresh — both the proactive refresh in the executor base
(`base.ts` `execute()`, no refresh-token rotation consumed) and the
reactive 401/403 path in `chatCore` (no `expired` deactivation)
- `chat.ts` — provider circuit breaker and the 5-minute quota cache
(`markAccountExhaustedFrom429`) never degraded
The recorded error is what makes a flagged account visible in the dashboard
while it stays serving traffic. Note: the probe record stores the **raw**
(unsliced) error text, unlike the real path's `slice(0,100)` truncation.
Operators who use test-all as a maintenance tool can restore the historical
behavior (probe counts as a real generation) via either:
- the `probeCanDisable` setting (`POST /api/settings` with
`{"probeCanDisable": true}`, or a direct `key_value` DB edit), or
- feature flag **`PROBE_CAN_DISABLE=true`** (env or DB override; wins over the
setting).
Fail-safe: if the flag or settings lookup throws, isolation stays ON.
## Source files
| Concern | File |
| --- | --- |
| Signal tables + match | `open-sse/services/accountFallback.ts` |
| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
| Auto-disable scope | `src/shared/utils/autoDisableBanned.ts`, `src/sse/services/autoDisableBannedAccount.ts` |
| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
| Concern | File |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Signal tables + match | `open-sse/services/accountFallback.ts` |
| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
| Auto-disable scope | `src/shared/utils/autoDisableBanned.ts`, `src/sse/services/autoDisableBannedAccount.ts` |
| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |

View File

@@ -20,21 +20,26 @@ password on every invocation.
(falls back to an empty string on failure, disabling CLI auth).
2. It computes `HMAC-SHA256(machine_id, salt)` and returns the full 64-char
hex digest — a deterministic, non-reversible token tied to this machine.
3. The CLI sends the token as `x-omniroute-cli-token` on every request to
`http://localhost:<port>/api/...`.
3. The CLI sends the token as `x-omniroute-cli-token` only when the resolved
destination is an explicit loopback URL (`localhost`, `127.0.0.0/8`, or
loopback IPv6). Requests carrying the token use `redirect: error`, so a local
redirect cannot forward it to another origin. Remote contexts use scoped
access tokens instead. If derivation is unavailable, the CLI omits the header
and `omniroute doctor` reports the failure instead of treating an empty token
as valid.
4. The server (`src/server/authz/policies/management.ts`) recomputes the
expected token with the same salt and compares via `timingSafeEqual` to
prevent timing-based extraction.
## Security properties
| Property | Detail |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Loopback-only** | Accepted only when `Host` is `localhost`, `127.0.0.1`, or `::1`. |
| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
| **Non-reversible** | HMAC output cannot recover the machine-id. |
| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
| **Non-exportable** | Token is never written to disk or logged. |
| Property | Detail |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Loopback-only** | Accepted only when the server's trusted peer-locality stamp (derived from the real TCP peer address) says loopback. The client-controlled `Host` header is never trusted for locality. |
| **Constant-time compare** | `crypto.timingSafeEqual` prevents timing attacks. |
| **Non-reversible** | HMAC output cannot recover the machine-id. |
| **No `always`-protected bypass** | `isAlwaysProtectedPath()` is evaluated before the CLI token check. `/api/shutdown` and `/api/settings/database` always require JWT. |
| **Non-exportable** | Token is never written to disk or logged. |
## Salt rotation

View File

@@ -114,7 +114,7 @@ Two separate retention windows are honoured:
| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Row-cap trim for `proxy_logs` |
`cleanupExpiredLogs()` runs the retention pass. It is invoked on server startup
from `src/server-init.ts` and `src/instrumentation-node.ts`. Each run logs a
from `src/instrumentation-node.ts`. Each run logs a
`compliance.cleanup` audit event with the per-table delete counts. Proxy/call
log trimming is batched (`BATCH_SIZE = 5000`) to avoid long write locks.

View File

@@ -5,8 +5,8 @@ const path = require("path");
/**
* remoteServerPreferences.js — pure read/write helpers for the small JSON
* preferences file that persists the operator-configured remote server URL
* across app restarts (see resolveRemoteServerUrl.js for how it's consumed).
* preferences file that persists desktop-shell choices needed before the
* server-owned settings database is available.
*
* Deliberately a plain flat JSON file rather than the app's SQLite database:
* this preference must be readable before deciding whether to spawn (or even
@@ -18,19 +18,20 @@ const path = require("path");
* @param {string} prefsPath - absolute path to electron-preferences.json
* @param {(p: string) => boolean} [existsSync]
* @param {(p: string, enc: string) => string} [readFileSync]
* @returns {{remoteServerUrl: string|null}}
* @returns {{remoteServerUrl: string|null, closeBehavior: "keep-loaded"|"unload"}}
*/
function readPreferences(prefsPath, existsSync = fs.existsSync, readFileSync = fs.readFileSync) {
if (!existsSync(prefsPath)) return { remoteServerUrl: null };
if (!existsSync(prefsPath)) return { remoteServerUrl: null, closeBehavior: "keep-loaded" };
try {
const parsed = JSON.parse(readFileSync(prefsPath, "utf8"));
const remoteServerUrl =
typeof parsed.remoteServerUrl === "string" && parsed.remoteServerUrl.trim()
? parsed.remoteServerUrl.trim()
: null;
return { remoteServerUrl };
const closeBehavior = parsed.closeBehavior === "unload" ? "unload" : "keep-loaded";
return { remoteServerUrl, closeBehavior };
} catch {
return { remoteServerUrl: null };
return { remoteServerUrl: null, closeBehavior: "keep-loaded" };
}
}
@@ -72,4 +73,35 @@ function writeRemoteServerUrl(
}
}
module.exports = { readPreferences, writeRemoteServerUrl };
/** Persist whether closing the dashboard hides it or unloads its renderer. */
function writeCloseBehavior(
prefsPath,
closeBehavior,
{
existsSync = fs.existsSync,
readFileSync = fs.readFileSync,
writeFileSync = fs.writeFileSync,
mkdirSync = fs.mkdirSync,
} = {}
) {
try {
const dir = path.dirname(prefsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const current = readPreferences(prefsPath, existsSync, readFileSync);
const next = {
...current,
closeBehavior: closeBehavior === "unload" ? "unload" : "keep-loaded",
};
writeFileSync(prefsPath, JSON.stringify(next, null, 2) + "\n", "utf8");
} catch (err) {
console.error(
`[remoteServerPreferences] Failed to write preferences to ${prefsPath}:`,
err instanceof Error ? err.message : String(err)
);
}
}
module.exports = { readPreferences, writeRemoteServerUrl, writeCloseBehavior };

View File

@@ -0,0 +1,26 @@
"use strict";
const CLOSE_BEHAVIOR_KEEP_LOADED = "keep-loaded";
const CLOSE_BEHAVIOR_UNLOAD = "unload";
function normalizeCloseBehavior(value) {
if (value === CLOSE_BEHAVIOR_KEEP_LOADED || value === CLOSE_BEHAVIOR_UNLOAD) return value;
return null;
}
function resolveRendererUrl(currentUrl, serverUrl) {
try {
const current = new URL(currentUrl);
const server = new URL(serverUrl);
return current.origin === server.origin ? current.href : server.href;
} catch {
return serverUrl;
}
}
module.exports = {
CLOSE_BEHAVIOR_KEEP_LOADED,
CLOSE_BEHAVIOR_UNLOAD,
normalizeCloseBehavior,
resolveRendererUrl,
};

View File

@@ -0,0 +1,28 @@
/** Pure helpers for deciding and driving the Electron dashboard window lifecycle. */
function shouldStartHidden({ argv = [], loginItemSettings = {} } = {}) {
return (
argv.includes("--hidden") ||
argv.includes("--minimized") ||
loginItemSettings.wasOpenedAsHidden === true
);
}
function showOrCreateWindow({ appReady, getWindow, createWindow }) {
if (!appReady) return null;
const currentWindow = getWindow();
if (!currentWindow || currentWindow.isDestroyed()) {
return createWindow();
}
if (currentWindow.isMinimized()) currentWindow.restore();
currentWindow.show();
currentWindow.focus();
return currentWindow;
}
module.exports = {
shouldStartHidden,
showOrCreateWindow,
};

View File

@@ -38,8 +38,19 @@ const { killProcessTree } = require("./processTree");
const { resolveServerEntry } = require("./lib/resolveServerEntry");
const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
const { resolveRemoteServerUrl, isValidHttpUrl } = require("./lib/resolveRemoteServerUrl");
const { writeRemoteServerUrl } = require("./lib/remoteServerPreferences");
const {
readPreferences,
writeRemoteServerUrl,
writeCloseBehavior,
} = require("./lib/remoteServerPreferences");
const { buildReadinessUrl, waitForServer } = require("./lib/serverReadiness");
const { shouldStartHidden, showOrCreateWindow } = require("./lib/windowLifecycle");
const {
CLOSE_BEHAVIOR_KEEP_LOADED,
CLOSE_BEHAVIOR_UNLOAD,
normalizeCloseBehavior,
resolveRendererUrl,
} = require("./lib/windowClosePolicy");
// ── Single Instance Lock ───────────────────────────────────
const gotTheLock = app.requestSingleInstanceLock();
@@ -49,11 +60,12 @@ if (!gotTheLock) {
}
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
const isHeadless =
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
if (isHeadless) return;
showMainWindow();
});
// ── Environment Detection ──────────────────────────────────
@@ -71,6 +83,8 @@ let nextServer = null;
let serverPort = 20128;
let isServerStopped = false;
let remoteServerPromptWindow = null;
let keepAliveWithoutWindows = false;
let lastRendererUrl = null;
// ── Remote Server Mode ──────────────────────────────────────
// Lets the desktop shell attach to an already-running OmniRoute server (e.g. a
@@ -81,6 +95,8 @@ const REMOTE_SERVER_PREFS_PATH = path.join(
resolveDataDir(null, process.env),
"electron-preferences.json"
);
const electronPreferences = readPreferences(REMOTE_SERVER_PREFS_PATH);
let closeBehavior = electronPreferences.closeBehavior;
let remoteServerUrl = resolveRemoteServerUrl({
env: process.env,
prefsPath: REMOTE_SERVER_PREFS_PATH,
@@ -365,14 +381,18 @@ function setupContentSecurityPolicy() {
}
// ── Create Window ──────────────────────────────────────────
function createWindow() {
function createWindow({ showWhenReady = true } = {}) {
if (mainWindow && !mainWindow.isDestroyed()) return mainWindow;
const rendererStartedAt = Date.now();
// Platform-conditional options (#9)
const platformWindowOptions =
process.platform === "darwin"
? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 16 } }
: { titleBarStyle: "default" };
mainWindow = new BrowserWindow({
const window = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
@@ -390,28 +410,28 @@ function createWindow() {
backgroundColor: "#0a0a0a",
...platformWindowOptions,
});
mainWindow = window;
// Load the Next.js app
mainWindow.loadURL(getServerUrl());
window.loadURL(resolveRendererUrl(lastRendererUrl, getServerUrl()));
if (isDev) {
mainWindow.webContents.openDevTools({ mode: "detach" });
window.webContents.openDevTools({ mode: "detach" });
}
// Show window when ready (unless starting minimized/hidden in tray)
mainWindow.once("ready-to-show", () => {
const startHidden =
process.argv.includes("--hidden") ||
process.argv.includes("--minimized") ||
app.getLoginItemSettings().wasOpenedAsHidden;
if (!startHidden) {
mainWindow.show();
// Hidden startup (createWindow({ showWhenReady: false })) skips the initial
// show(); the window stays created (so tray/dock interactions work) but the
// renderer only becomes visible on the next explicit showMainWindow() call.
window.once("ready-to-show", () => {
console.log(`[Electron] Renderer ready in ${Date.now() - rendererStartedAt}ms`);
if (showWhenReady) {
window.show();
} else {
console.log("[Electron] Launched hidden in background tray");
}
});
// Handle external links — validate URL protocol to prevent RCE
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
window.webContents.setWindowOpenHandler(({ url }) => {
try {
const parsedUrl = new URL(url);
if (["http:", "https:"].includes(parsedUrl.protocol)) {
@@ -425,18 +445,44 @@ function createWindow() {
return { action: "deny" };
});
// Handle window close — minimize to tray
mainWindow.on("close", (event) => {
// Keep the server alive while either hiding the renderer for a fast reopen or
// unloading it to reclaim memory, according to the persisted tray preference.
window.on("close", (event) => {
if (!app.isQuitting) {
event.preventDefault();
mainWindow.hide();
lastRendererUrl = resolveRendererUrl(window.webContents.getURL(), getServerUrl());
if (closeBehavior === CLOSE_BEHAVIOR_UNLOAD) {
console.log("[Electron] Dashboard renderer unloaded; server remains running");
window.destroy();
} else {
console.log("[Electron] Dashboard hidden; renderer kept loaded");
window.hide();
}
}
return false;
});
mainWindow.on("closed", () => {
mainWindow = null;
window.on("closed", () => {
if (mainWindow === window) mainWindow = null;
});
return window;
}
function showMainWindow() {
return showOrCreateWindow({
appReady: app.isReady(),
getWindow: () => mainWindow,
createWindow,
});
}
function setCloseBehavior(nextBehavior) {
const normalized = normalizeCloseBehavior(nextBehavior);
if (!normalized || normalized === closeBehavior) return;
closeBehavior = normalized;
writeCloseBehavior(REMOTE_SERVER_PREFS_PATH, closeBehavior);
createTray();
}
// ── System Tray ────────────────────────────────────────────
@@ -465,12 +511,7 @@ function createTray() {
const contextMenu = Menu.buildFromTemplate([
{
label: "Open OmniRoute",
click: () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
},
click: () => showMainWindow(),
},
{
label: "Open Dashboard",
@@ -504,6 +545,23 @@ function createTray() {
},
],
},
{
label: "When Dashboard Closes",
submenu: [
{
label: "Keep Loaded (Faster Reopen)",
type: "radio",
checked: closeBehavior === CLOSE_BEHAVIOR_KEEP_LOADED,
click: () => setCloseBehavior(CLOSE_BEHAVIOR_KEEP_LOADED),
},
{
label: "Unload Renderer (Lower Memory)",
type: "radio",
checked: closeBehavior === CLOSE_BEHAVIOR_UNLOAD,
click: () => setCloseBehavior(CLOSE_BEHAVIOR_UNLOAD),
},
],
},
{ type: "separator" },
{
label: "Check for Updates",
@@ -522,12 +580,7 @@ function createTray() {
tray.setToolTip("OmniRoute");
tray.setContextMenu(contextMenu);
tray.on("double-click", () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
tray.on("double-click", () => showMainWindow());
}
// ── Change Port (#3: now restarts server) ──────────────────
@@ -549,6 +602,7 @@ async function changePort(newPort) {
await waitForServer(getServerReadinessUrl());
// Reload window and update tray
lastRendererUrl = getServerUrl();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
}
@@ -613,6 +667,7 @@ async function setRemoteServerUrl(nextUrl) {
remoteServerUrl = normalized;
writeRemoteServerUrl(REMOTE_SERVER_PREFS_PATH, remoteServerUrl);
lastRendererUrl = getServerUrl();
startNextServer();
try {
@@ -1094,9 +1149,20 @@ app.whenReady().then(async () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
const startHidden =
!isHeadless &&
shouldStartHidden({
argv: process.argv,
loginItemSettings: app.getLoginItemSettings(),
});
keepAliveWithoutWindows = startHidden;
// Fix #1: Start server and WAIT for readiness before showing window
startNextServer();
if (!isHeadless) {
createTray();
}
let serverReady = true;
if (!isDev) {
// Probe the lightweight auth-exempt endpoint instead of aggregating full monitoring state.
@@ -1105,9 +1171,10 @@ app.whenReady().then(async () => {
if (isHeadless) {
console.log("[Electron] Headless mode active — UI window and tray icon skipped");
} else if (startHidden) {
console.log("[Electron] Launched hidden in background tray without a renderer");
} else {
createWindow();
createTray();
showMainWindow();
}
setupIpcHandlers();
@@ -1115,7 +1182,7 @@ app.whenReady().then(async () => {
// If readiness timed out (e.g. very long first-launch migrations), don't leave the
// window stuck on a hanging connection — keep polling and reload once it responds (#2460).
if (!isDev && !serverReady && !isHeadless) {
if (!isDev && !serverReady && !isHeadless && !startHidden) {
void waitForServer(getServerReadinessUrl(), 300000).then((ready) => {
if (ready && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.loadURL(getServerUrl());
@@ -1133,11 +1200,7 @@ app.whenReady().then(async () => {
// macOS: recreate window when dock icon clicked
app.on("activate", () => {
if (isHeadless) return;
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
} else if (mainWindow) {
mainWindow.show();
}
showMainWindow();
});
});
@@ -1147,7 +1210,12 @@ app.on("window-all-closed", () => {
process.argv.includes("--headless") ||
process.argv.includes("--cli") ||
process.env.OMNIROUTE_HEADLESS === "true";
if (process.platform !== "darwin" && !isHeadless) {
if (
process.platform !== "darwin" &&
!isHeadless &&
!keepAliveWithoutWindows &&
closeBehavior !== CLOSE_BEHAVIOR_UNLOAD
) {
app.quit();
}
});

View File

@@ -64,9 +64,11 @@
"remoteServerPromptRenderer.js",
"lib/resolveServerEntry.js",
"lib/resolveNodeHelper.js",
"lib/windowLifecycle.js",
"lib/resolveRemoteServerUrl.js",
"lib/remoteServerPreferences.js",
"lib/serverReadiness.js",
"lib/windowClosePolicy.js",
"assets/remoteServerPrompt.html",
"package.json",
"node_modules/**/*"

View File

@@ -1,6 +1,6 @@
# OmniRoute
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 342 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 343 AI providers — all through a single OpenAI-compatible endpoint. Includes a built-in MCP Server (109 tools), A2A v0.3 protocol, Memory/Skills systems, Cloud Agents (codex, cursor, devin, jules), Guardrails framework, and an Electron desktop app.
## Overview
@@ -165,7 +165,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
│ │ └── manager.ts # MITM proxy manager
│ ├── shared/ # Shared utilities, components, and constants
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, Sidebar, ProviderIcon, etc.)
│ │ ├── constants/ # Provider definitions (342), model lists, pricing, routing strategies, MCP scopes
│ │ ├── constants/ # Provider definitions (343), model lists, pricing, routing strategies, MCP scopes
│ │ ├── contracts/ # Shared API contracts
│ │ ├── hooks/ # React hooks
│ │ ├── middleware/ # Shared middleware utilities
@@ -277,7 +277,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
## Key Features (v3.8.50)
### Core Proxy
- **342 AI providers** with automatic format translation
- **343 AI providers** with automatic format translation
- **Provider categories**: Free (90+ free tiers), OAuth, API Key, Self-Hosted, Custom (OpenAI/Anthropic-compatible)
- **18 routing strategies**: priority, weighted, round-robin, fill-first, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline
- **4-tier fallback**: Subscription → API Key → Cheap → Free
@@ -475,7 +475,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
## v3.8.x Highlights
- **342-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **343-provider catalog** with 90+ free tiers, one-click account imports, and bulk key add
- **19 routing strategies** — including `fusion` (parallel panel + judge synthesis), `pipeline`, `reset-aware`, `reset-window`, `headroom`, and `context-relay`
- **14-factor Auto-Combo scoring** with bandit exploration and progressive cooldown
- **MCP server expanded to 109 tools / 33 scopes** (canonical + memory/skill/agentSkill/githubSkill/pool/notion/obsidian/localCorpus/gamification/plugin modules)

View File

@@ -41,6 +41,15 @@ export const AGY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.7-flash-tiered",
name: "Gemini 3.7 Flash (Tiered)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Gemini 3.1 Pro
{
id: "gemini-pro-agent",

View File

@@ -29,6 +29,15 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
supportsVision: true,
toolCalling: true,
},
{
id: "gemini-3.7-flash-tiered",
name: "Gemini 3.7 Flash (Tiered)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
},
// Gemini 3.1 Pro budget tiers. Live streamGenerateContent validation uses
// `gemini-pro-agent` for High; the separately advertised `gemini-3.1-pro-high`
// discovery slot currently returns HTTP 400 and is intentionally not public.
@@ -91,6 +100,13 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([
]);
export const ANTIGRAVITY_MODEL_ALIASES = Object.freeze({
// Gemini 3.7 Flash tiers map to the upstream tiered endpoint model; the thinking
// budget is steered via generationConfig.thinkingConfig.thinkingBudget.
"gemini-3.7-flash": "gemini-3.7-flash-tiered",
"gemini-3.7-flash-high": "gemini-3.7-flash-tiered",
"gemini-3.7-flash-medium": "gemini-3.7-flash-tiered",
"gemini-3.7-flash-low": "gemini-3.7-flash-tiered",
"gpt-oss-120b": "gpt-oss-120b-medium",
// gemini-3.1-pro-low is not aliased: the upstream accepts it verbatim.
// gemini-3.1-pro-high: the discovery slot returns HTTP 400 on v1internal;
// the live upstream id is gemini-pro-agent (see ANTIGRAVITY_PUBLIC_MODELS).

View File

@@ -81,6 +81,10 @@ export const COOLDOWN_MS = {
// account, so re-probe only after a long window (or when the operator routes
// egress through a supported-region proxy).
geoBlocked: 24 * 60 * 60 * 1000,
// Antigravity BYOP (GCP_PROJECT_REQUIRED): nothing changes on the account
// until the operator enters a Project ID, so keep the connection excluded
// from selection for a long window (mirrors the geo-blocked treatment).
gcpProjectRequired: 24 * 60 * 60 * 1000,
};
/**

View File

@@ -16,9 +16,11 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
* rewrites file timestamps on every deploy, which would report a months-old
* catalog as "updated today". Bump this whenever the entries below change.
*/
export const FREE_CATALOG_CURATED_AT = "2026-08-16";
export const FREE_CATALOG_CURATED_AT = "2026-08-18";
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free", displayName: "GPT-5.6 Luna (Free)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
{ provider: "chatgpt-web", modelId: "gpt-5.6-luna-free-thinking", displayName: "GPT-5.6 Luna (Free, Think)", monthlyTokens: 0, creditTokens: 0, freeType: "recurring-uncapped", poolKey: "chatgpt-web-free", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },

View File

@@ -18,6 +18,35 @@ export const GLM_ANTHROPIC_DEFAULT_BASE_URLS = Object.freeze({
});
export const GLM_SHARED_MODELS = Object.freeze([
{
// GLM-5.3 (2026-08-14): one upstream id; effort is the reasoning_effort
// param (low|high|max, default max) — the -high/-low entries below are
// OmniRoute aliases resolved by GlmExecutor::parseGlmEffortTier.
// Default context window not yet published by Z.ai; 1M mirrored from
// GLM-5.2 (same base model). https://z.ai/blog/glm-5.3
id: "glm-5.3",
name: "GLM 5.3",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5.3-high",
name: "GLM 5.3 High",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5.3-low",
name: "GLM 5.3 Low",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
},
{
id: "glm-5.2",
name: "GLM 5.2",

View File

@@ -11,6 +11,7 @@ export const GROK_BUILD_TOKEN_URL = `${GROK_BUILD_OAUTH_ISSUER}/oauth2/token`;
export const GROK_BUILD_DEFAULT_CLIENT_VERSION = "0.2.106";
export const GROK_BUILD_DEFAULT_CONTEXT_WINDOW = 256_000;
export const GROK_BUILD_DEFAULT_REASONING_EFFORT = "high";
export const GROK_BUILD_SUPPORTED_REASONING_EFFORTS = Object.freeze(["low", "medium", "high"]);
export const GROK_BUILD_CLIENT_IDENTIFIER = "grok-shell";
export const GROK_BUILD_TOKEN_AUTH = "xai-grok-cli";
export const GROK_BUILD_REASONING_INCLUDE = "reasoning.encrypted_content";

View File

@@ -210,6 +210,7 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
authHeader: "bearer",
format: "openai", // native OpenAI format
models: [
{ id: "dall-e-3", name: "DALL·E 3" },
{ id: "gpt-image-2", name: "GPT Image 2" },
{ id: "gpt-image-1.5", name: "GPT Image 1.5" },
{ id: "gpt-image-1-mini", name: "GPT Image 1 Mini" },
@@ -267,6 +268,25 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
},
// Cursor plan image generation via the Agent CLI native `generateImage` tool.
// Reuses the same OAuth/API-key connection as chat (`provider: "cursor"`).
// Requires the `agent` binary (CURSOR_AGENT_BIN) — see cursorAgentImage handler.
cursor: {
id: "cursor",
alias: "cu",
// Sentinel: execution is local Agent CLI, not an HTTP image API.
baseUrl: "agent://cursor-agent",
authType: "oauth",
authHeader: "bearer",
format: "cursor-agent-image",
models: [
{ id: "auto", name: "Cursor Auto (Image)" },
{ id: "composer-2", name: "Composer 2 (Image)" },
{ id: "composer-2.5", name: "Composer 2.5 (Image)" },
],
supportedSizes: ["1024x1024", "1024x1792", "1792x1024", "1024x1536", "1536x1024"],
},
"microsoft-designer-web": {
id: "microsoft-designer-web",
alias: "msdesigner",
@@ -898,9 +918,9 @@ export function parseImageModel(modelStr) {
}
}
// No provider prefix — try to find the model in every provider
// No provider prefix — try to find the model in every provider, excluding cookie-auth (web) bridges
for (const [providerId, config] of Object.entries(IMAGE_PROVIDERS)) {
if (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr)) {
if (config.authHeader !== "cookie" && (config.routingAliases?.includes(modelStr) || config.models.some((m) => m.id === modelStr))) {
return { provider: providerId, model: modelStr };
}
}

View File

@@ -3,6 +3,8 @@ import { unorouterProvider } from "./registry/unorouter/index.ts";
import { aimlapiProvider } from "./registry/aimlapi/index.ts";
import { byteplusProvider } from "./registry/byteplus/index.ts";
import { mlxGemmaProvider } from "./registry/mlx/index.ts";
import { mlxQwenProvider } from "./registry/mlx/index.ts";
import { ollama_cloudProvider } from "./registry/ollama-cloud/index.ts";
import { syntheticProvider } from "./registry/synthetic/index.ts";
import { ideogramProvider } from "./registry/ideogram/index.ts";
@@ -264,6 +266,8 @@ import { helixmindProvider } from "./registry/helixmind/index.ts";
export const REGISTRY: Record<string, RegistryEntry> = {
aimlapi: aimlapiProvider,
"mlx-gemma": mlxGemmaProvider,
"mlx-qwen": mlxQwenProvider,
"ollama-cloud": ollama_cloudProvider,
synthetic: syntheticProvider,
ideogram: ideogramProvider,

View File

@@ -9,12 +9,83 @@ export const chatgpt_webProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "gpt-5.6-pro", name: "GPT-5.6 Pro", toolCalling: false }, // pro tier only, standard effort
{ id: "gpt-5.6-thinking", name: "GPT-5.6 Thinking", toolCalling: false }, // plus, pro tier
{ id: "gpt-5.5-pro-extended", name: "GPT-5.5 Pro Extended", toolCalling: false }, // pro tier only, extended effort
{ id: "gpt-5.5-pro", name: "GPT-5.5 Pro", toolCalling: false }, // pro tier only, standard effort
{ id: "gpt-5.5-thinking", name: "GPT-5.5 Thinking", toolCalling: false }, // plus, pro tier
{ id: "gpt-5.5", name: "GPT-5.5 Instant", toolCalling: false }, // free, plus, pro tier
{ id: "o3", name: "o3", toolCalling: false }, // plus ~ tier
{
id: "gpt-5.6-sol-pro",
name: "GPT-5.6 Sol (Pro)",
liveCatalogIds: ["gpt-5-6-pro"],
toolCalling: false,
},
{
id: "gpt-5.6-sol-xhigh",
name: "GPT-5.6 Sol (Xhigh)",
liveCatalogIds: ["gpt-5-6-thinking"],
toolCalling: false,
},
{
id: "gpt-5.6-sol-high",
name: "GPT-5.6 Sol (High)",
liveCatalogIds: ["gpt-5-6-thinking"],
toolCalling: false,
},
{
id: "gpt-5.6-sol-medium",
name: "GPT-5.6 Sol (Medium)",
liveCatalogIds: ["gpt-5-6-thinking"],
toolCalling: false,
},
{
id: "gpt-5.6-sol-instant",
name: "GPT-5.6 Sol (Instant)",
liveCatalogIds: ["gpt-5-6"],
toolCalling: false,
},
{
id: "gpt-5.6-luna-free-thinking",
name: "GPT-5.6 Luna (Free, Think)",
liveCatalogIds: ["gpt-5-6"],
toolCalling: false,
},
{
id: "gpt-5.6-luna-free",
name: "GPT-5.6 Luna (Free)",
liveCatalogIds: ["gpt-5-6"],
toolCalling: false,
},
{
id: "gpt-5.5-pro-extended",
name: "GPT-5.5 (Pro Extended)",
liveCatalogIds: ["gpt-5-5-pro"],
toolCalling: false,
},
{
id: "gpt-5.5-pro",
name: "GPT-5.5 (Pro)",
liveCatalogIds: ["gpt-5-5-pro"],
toolCalling: false,
},
{
id: "gpt-5.5-xhigh",
name: "GPT-5.5 (Xhigh)",
liveCatalogIds: ["gpt-5-5-thinking"],
toolCalling: false,
},
{
id: "gpt-5.5-high",
name: "GPT-5.5 (High)",
liveCatalogIds: ["gpt-5-5-thinking"],
toolCalling: false,
},
{
id: "gpt-5.5-medium",
name: "GPT-5.5 (Medium)",
liveCatalogIds: ["gpt-5-5-thinking"],
toolCalling: false,
},
{
id: "gpt-5.5-instant",
name: "GPT-5.5 (Instant)",
liveCatalogIds: ["gpt-5-5"],
toolCalling: false,
},
],
};

Some files were not shown because too many files have changed in this diff Show More