From a7ae9550bd88ced182ed61946355b0b4d6d89e28 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:17:40 -0300 Subject: [PATCH] Release v3.8.36 (#4854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(release): open v3.8.36 development cycle * refactor(chatCore): extrai resolveCompressionSettings (#3501) (#4826) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 1/13) * refactor(chatCore): extrai predicados puros de combo de compressão (#3501) (#4824) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 2/13) * refactor(chatCore): extrai emitOutputStyleTelemetry (#3501) (#4811) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 3/13) * refactor(chatCore): extrai writeCompressionAnalytics (bloco analytics completo, #3501) (#4817) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 4/13) * refactor(chatCore): extrai runPluginOnRequestHook (#3501) (#4827) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 5/13) * refactor(chatCore): extrai applyClientUsageBuffer (buffer/estimate de usage non-streaming, #3501) (#4832) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 6/13) * refactor(chatCore): extrai buildPostCallGuardrailContext (contexto guardrail post-call, #3501) (#4831) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 7/13) * refactor(chatCore): extrai storeSemanticCacheResponse (cache-store non-streaming, #3501) (#4828) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 8/13) * refactor(chatCore): extrai buildNonStreamingResponseHeaders (headers de resposta non-streaming, #3501) (#4835) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 9/13) * refactor(chatCore): extrai maybeConvertJsonBodyToSse (#3089 JSON→SSE streaming, #3501) (#4833) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 10/13) * refactor(chatCore): extrai assembleStreamingResponseHeaders (headers de resposta streaming, #3501) (#4836) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 11/13) * refactor(chatCore): extrai storeStreamingSemanticCacheResponse (cache-store streaming, #3501) (#4829) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 12/13) * refactor(chatCore): extrai assembleStreamingPipeline (chain de transforms streaming, #3501) (#4837) Integrated into release/v3.8.36 (#3501 chatCore extraction stack 13/13) * ci(quality): shift heavy validations to the PR→release fast-path (release-acceleration) (#4857) * feat(quality): add check:test-runner-api gate (vitest-only dirs must use vitest API) * feat(release): reusable CHANGELOG i18n-mirror sync script * chore(ops): add prune-stale-worktrees.sh (dry-run by default) * ci(quality): run test-runner-api + docs-all + vitest + full unit suite on PR->release fast-path --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(quota): cota exclusiva lista qtSd/ no /v1/models (#4806) + limite EPSILON não bloqueia (#4830) Integrated into release/v3.8.36 — quota-exclusive qtSd/ listing (#4806) + EPSILON placeholder no longer blocks; rebuilt from stale base (3 defining commits cherry-picked clean over release tip) * feat(sse): add Google Flow video-generation provider (#4569) (#4769) Integrated into release/v3.8.36 — Google Flow video-generation provider (#4569), release-green validated (typecheck + 21 tests + file-size) * fix(api): auth on compression run-telemetry + document OMNIROUTE_EVAL_CREDENTIALS (#4694, #4720) (#4796) Integrated into release/v3.8.36 — auth on compression run-telemetry + OMNIROUTE_EVAL_CREDENTIALS doc, release-green validated (typecheck + 3 tests + env-doc-sync) * fix(translator): strip top-level client_metadata on the OpenAI passthrough (port from 9router#1157) (#4624) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(translator): normalize `developer` role to `system` for OpenAI-format providers (#4625) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(translator): emit close marker for Anthropic thinking blocks (#4633) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(translator): normalize tools to Anthropic-native shape for non-Anthropic providers (#4650) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(gemini): preserve `pattern` in antigravity tool schema sanitizer (#4651) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(perplexity): validate API keys via /v1/models endpoint (#4654) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(image): prevent compatible nodes from shadowing provider aliases (#4656) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(cli-tools): tolerate JSONC (comments, trailing commas) in tool settings (#4659) Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated) * fix(security): validate kiro region to prevent SSRF (GHSA-6mwv-4mrm-5p3m) (#4629) Integrated into release/v3.8.36 — kiro region SSRF guard (GHSA-6mwv-4mrm-5p3m), port rebuilt clean over release tip * fix(cli): harden the systray2 tray runtime (port of 9router#1080) (#4628) Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated * fix(test): validate anthropic-compatible connections via POST /v1/messages (#4657) Integrated into release/v3.8.36 — anthropic-compat validation via POST /v1/messages (port 584cf66a), rebuilt clean + baseline; release-green * fix(executors): strip params unsupported by the target provider/model (#4658) Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated * fix(claude-oauth): respect 429 backoff on usage endpoint to reduce spam (#4655) Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated * feat(api/v1): include alias-backed models in /v1/models listing (#4630) Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated * chore(quality): rebaseline catalog.ts 1574->1577 (#4630 aliases sobre quota-exclusive da release) (#4879) rebaseline * feat(compression): Kiro/CodeWhisperer tool-result compression engine (#4635) Integrated into release/v3.8.36 — port rebuilt clean, release-green * fix(security): don't trust loopback socket as local when behind reverse proxy (#4632) Integrated into release/v3.8.36 — port rebuilt clean, release-green * fix(opencode): preserve DeepSeek reasoning content in streamed responses (#4631) Integrated into release/v3.8.36 — DeepSeek reasoning_content injection (port #1099); release-green * fix(copilot,antigravity): cap maxOutputTokens at 16384 to stop "Invalid Argument" 400 (#4636) Integrated into release/v3.8.36 — cap maxOutputTokens 16384 antigravity (port #779); release-green * fix(dashboard): show custom vision models in LLM selector (#4653) Integrated into release/v3.8.36 — custom vision models in LLM selector (port 5e5e78d3); release-green * fix(claude): omit adaptive thinking + output_config.effort for haiku (#4661) Integrated into release/v3.8.36 — haiku adaptive-thinking omit (port); release-green * feat(provider): CodeBuddy CN (copilot.tencent.com) — full stack (#4664) Integrated into release/v3.8.36 — CodeBuddy CN provider (port efd20be8); usage.ts import + public-creds allowlist line reconciled; release-green * feat(combo): Fusion strategy — parallel panel + judge synthesis (16th strategy) (#4652) Integrated into release/v3.8.36 — Fusion combo strategy (16th, port 87e5c1c6); combo.ts baseline reconciled; release-green * feat(proxy-pool): Deno Deploy relays + group action buttons (#4643) Integrated into release/v3.8.36 — Deno Deploy relays (port #1437); proxies.ts baseline reconciled + env docs restored; release-green * fix(security): pin image fetch DNS resolution to prevent SSRF rebinding (GHSA-cmhj-wh2f-9cgx) (#4634) Integrated into release/v3.8.36 — pin DNS for image fetch SSRF rebinding guard (GHSA-cmhj-wh2f-9cgx, port c7d07448); caller DNS stubs + test-file baseline reconciled; release-green * fix(github): route Copilot Codex models to /responses (port from 9router#102) (#4626) Integrated into release/v3.8.36 — route Copilot Codex models to /responses (port #102); release-green * fix(copilot): never route Gemini/Claude variants to /responses (chat-completions only) (#4627) Integrated into release/v3.8.36 — never route Gemini/Claude to /responses (port #1536); fused with #4626 codex routing via supportsResponsesEndpoint gate; release-green * docs(ops): add canonical incident response runbook (#4868) Integrated into release/v3.8.36 * docs(perf): add per-endpoint p50/p95/p99 latency + cost budgets (#4867) Integrated into release/v3.8.36 * fix(proxy): fan out direct dispatcher streams (#4803) Integrated into release/v3.8.36 * fix(antigravity): exclude standard Gemini rate limit message from quota exhaustion keywords (#4810) Integrated into release/v3.8.36 * fix(sse): skip third-party tool-name cloak for Anthropic server tools (#4808) Integrated into release/v3.8.36 * fix(install): make transformers optional for CUDA-host installs (#4807) Integrated into release/v3.8.36 * fix(combo): propagate selected connection ID to fallback error responses for correct model lockout (#4809) Integrated into release/v3.8.36 * fix db storage tuning settings (#4834) Integrated into release/v3.8.36 * fix(sse): drop ccp pin when pinned provider is durably unhealthy (failover + anti-flap) (#4864) Integrated into release/v3.8.36 * fix(claude): skip mcp__ tool-name cloak + guard missing connectionId (#4861) Integrated into release/v3.8.36 * chore(quality): reconcile env-doc + file-size base-reds in release/v3.8.36 (#4886) - env-doc-sync: document PIN_DROP_BACKOFF_LEVEL / PIN_DROP_GRACE_MS (added by the ccp-pin health gate #4864) in .env.example + ENVIRONMENT.md. - file-size: rebaseline image-generation-handler.test.ts 1996 -> 2019 to its actual size (pre-existing drift). Co-authored-by: Diego Rodrigues de Sa e Souza * fix(codex): drop non-standard codex.* events that break responses.stream (env-gated, #4602) (#4715) Integrated into release/v3.8.36 * feat(routing): honor X-Route-Model header to override body.model (#4863) Integrated into release/v3.8.36 * feat(live-ws): allow non-loopback clients via LIVE_WS_ALLOWED_HOSTS (closes #4873) (#4877) Integrated into release/v3.8.36 (live-ws + combo-api commits; Tailscale CGNAT commit held pending opt-in/opt-out decision) * chore(claude,codex): bump pinned CLI identity — Claude 2.1.158→2.1.187, Codex 0.132.0→0.142.0 (#4883) Integrated into release/v3.8.36 * fix(security): SSRF allowlist bypass via x-relay-path nos relays Deno/Vercel (#4899) Integrated into release/v3.8.36 * feat(quota): recuperação proativa de conexões em cooldown (cron heal) [Fase 3 #8] (#4900) Integrated into release/v3.8.36 * fix(quota): policy inválida não vaza allow + guard connectionIds vazio [Fase 3 #10] (#4901) Integrated into release/v3.8.36 * feat(quota): saturação real do Claude no fair-share via /api/oauth/usage (#4885) Integrated into release/v3.8.36 * chore(dashboard): rename Qoder display label from "Qoder AI" to "Qoder" (#4733) Integrated into release/v3.8.36 * fix(ci): include coverage/lcov.info in coverage-report artifact for SonarQube (#4670) Integrated into release/v3.8.36 * fix(cli): bump better-sqlite3 runtime pin to 12.10.1 for Node 26 (#4685) Integrated into release/v3.8.36 * docs: clarify Kiro is ~50 credits/month per account, not unlimited (#4690) Integrated into release/v3.8.36 * docs(agentbridge): document Electron NODE_EXTRA_CA_CERTS, real model IDs, identity caveat (#4718) Integrated into release/v3.8.36 * docs(ops): document the release-green family (green-prs, check:release-green, babysit, nightly) (#4679) Integrated into release/v3.8.36 * fix(translator): replay reasoning_content on plain Xiaomi MiMo turns (port from 9router#1321) (#4639) Integrated into release/v3.8.36 * feat(opencode-go): advertise glm-5.2 and kimi-k2.7-code (align with official Go endpoints) (#4711) Integrated into release/v3.8.36 * feat(db): track API endpoint dimension on usage_history (#4676) Integrated into release/v3.8.36 (migration renumbered 103→105; endpoint plumbed through extracted usage-stats helpers) * fix(cli): SIGKILL systray child PID before IPC close to avoid macOS NSStatusItem orphan (#4732) Integrated into release/v3.8.36 * feat(proxy-pool): Cloudflare Workers proxy deployer + pool integration (#4640) Integrated into release/v3.8.36 (relay type added to RELAY_TYPES set; dropdown UX preserved + Cloudflare item added; proxies.ts file-size rebaselined 1057→1060) * chore(quality): conserta base-red de release/v3.8.36 (gates + 7 testes + build MDX) (#4915) A base tinha base-red sistêmica herdada de PRs de outras sessões, bloqueando TODOS os PRs do ciclo (o TIA roda a suíte full em fail-safe p/ diffs hub). 4 Fast Quality Gates: - test-discovery (#4877): live-server-allowlist.test.ts em tests/unit/server/ (não-coletado) + vitest → nunca rodava. Convertido p/ node:test em tests/unit/security/. - any-budget:t11 (#4664): 3 explicit-any em tokenRefresh.ts tipados (sem crescer file-size). - docs-symbols (#4868): rotas inexistentes → /api/system/version e PUT /api/providers/{id} {isActive:false}. - docs-all fabricated-claim (#4868 + #4718): 5 bin/*.sh reais criados (rollback, snapshot-data, restore-data, restore-policies, cold-start-bench) + _ops-common.sh (snapshot VACUUM INTO, guards de confirmação/TTY, testes de contrato); NODE_EXTRA_CA_CERTS (env de runtime Node) na allowlist do checker. 7 testes unit base-red (de features alheias à quota): - oauth-providers-config (#4664): teste alinhado ao provider codebuddy-cn do registry. - antigravity-model-aliases (#4636): maxOutputTokens esperado 32769→16384 (cap intencional). - provider-request-capture #4091 (#4861): exemplo do teste trocado de mcp__ (que #4861 isenta de cloak por causa dos 400s de assimetria de histórico) para um tool de terceiro cloakável — preserva o invariante de #4091 SEM reverter #4861. - combo-error-response: convertido de vitest p/ node:test (era coletado pelo glob node:test e crashava); api/** e server/** removidos do vitest.config (config morta). Build MDX (dast-smoke, #4679): - docs/ops/RELEASE_GREEN.md não tinha frontmatter `title` → fumadocs-mdx rejeitava no webpack compile ("invalid frontmatter: title expected string"), quebrando o next build (e o deploy). Frontmatter title adicionado (único doc do collection sem ele). 17/17 Fast Quality Gates + suíte unit completa (17737 testes, 0 fail) + vitest verdes localmente. Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): saturação proativa por headers de tokens (universal) [Fase 3 #2] (#4907) storeRateLimitHeaders só capturava os headers de REQUESTS (RPM/min), que não refletem a pressão de TOKENS. Agora também parseia os headers de tokens (em toda resposta, sucesso também) para throttle proativo antes do 429: - Anthropic: anthropic-ratelimit-tokens-{limit,remaining,reset} (+ input/output), RFC3339. - OpenAI: x-ratelimit-{limit,remaining,reset}-tokens, reset em duração (6m0s). saturation = 1 − remaining/limit; resetAt normalizado a epoch (parse de duração ReDoS-safe). getTokenHeaderSaturation por (provider, connectionId). fetchGeneric- Saturation passa a usar esse sinal (complementa o oauth/usage do #1, que segue primário p/ Claude). Fail-open, cache mantido, request-path inalterado. 16 testes novos + regressão (oauth/usage #1 8/8, signals 6/6) = 30/30; typecheck:core + eslint limpos. Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): estratégia de combo "headroom" — seleção por folga de cota [Fase 3 #4] (#4908) Nova estratégia de roteamento que escolhe a conexão com MAIS folga de plano: headroom = 1 − max(util_5h, util_7d) (técnica do dario), via getSaturation (melhorado p/ Claude no #1). Proativo em vez de só fill-first reativo. - Helper PURO headroomRanking.ts (computeHeadroom + rankByHeadroom; saturação injetada, não-mutante, tie-break estável, fail-open). - Orderer async em combo/quotaStrategies.ts (reusa a maquinaria reset-aware de expansão de conexões + concorrência limitada; seam injetável). - Registrada como "headroom" em routingStrategies (combo-only); fill-first segue default — nenhuma estratégia existente tocada. - baseline file-size combo.ts 3168->3180 (só +12L de dispatch; lógica fora do god-file). 16 testes novos + combo-strategies 15/15 = 31/31; typecheck:core + eslint + file-size limpos. Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): cap per-(key,model) — quota_allocation_model_caps [Fase 3 #7] (#4927) * feat(quota): cap per-(key,model) com tabela quota_allocation_model_caps [Fase 3 #7] Fecha o buraco onde uma API key pode drenar o pool inteiro consumindo um único modelo. Tabela nova: quota_allocation_model_caps(pool_id, api_key_id, model, cap_value, cap_unit) PK composta (pool_id, api_key_id, model). cap_unit alinhado ao QuotaUnit existente. Comportamento: keyA acima do cap para modelo M → bloqueada somente em M; ainda permitida em qualquer outro modelo no mesmo pool. Cap <= EPSILON → ignorado (seed). Consumo por-(key,model) usa bucket segregado no quota_consumption existente (poolId mangled ':model:') com window fixa 'hourly'; nenhuma nova tabela ou método de store necessário. Módulo novo: src/lib/db/quotaModelCaps.ts (getModelCap/setModelCap/deleteModelCap/listModelCaps) enforce.ts ganha o pre-check em enforceQuotaShare + recording em recordConsumption. EnforceInput e RecordConsumptionInput ganham model?: string (backward-compatible). localDb.ts re-exporta os 4 helpers (Hard Rule #2). TDD: tests/unit/quota-per-key-model.test.ts — 4 cenários (bloqueia em M, permite em M2, sem cap → sem bloqueio, EPSILON → ignorado). Todos os gates de qualidade passam. * feat(quota): plumba model resolvido no hot path para ativar o per-(key,model) cap [Fase 3 #7] A tabela/enforce do commit anterior estavam INERTES: o hot path não passava `model` ao enforce nem ao record, então nenhum model-cap disparava em produção. Plumbagem (model resolvido = mesma var usada no log/roteamento, pós background-redirect/alias): - chatCore.ts: enforceQuotaShare ganha `model`; scheduleQuotaShareConsumption recebe `model`. - chatCore/quotaShareConsumption.ts: threade `model` no RecordConsumptionInput (non-streaming). - spendRecorder.ts: recordStreamingConsumption já recebia `model` — agora o coloca no RecordConsumptionInput (streaming accrue por-modelo). - embeddings.ts: enforce + record ganham `model`. Namespace do cap = id do modelo RESOLVIDO (o mesmo de modelForScope/pendingScope/getUnsupportedParams), não o requestedModel cru nem o finalModelToUpstream (sem prefixo de provider). Operador configura o cap contra esse id. `model || undefined` em todos os pontos: vazio/null → check pulado (fail-safe, zero latência — só um campo no objeto). Teste de integração novo (tests/unit/quota-per-key-model-hotpath.test.ts): prova end-to-end que N consumos via scheduleQuotaShareConsumption({model}) → enforceQuotaShare({model}) bloqueia, e que outro modelo no mesmo pool ainda passa; + guard de que enforce SEM model nunca dispara model-cap. --------- Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5] (#4929) * feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5] Adiciona stickiness de sessão ao roteamento de combo: uma conversa multi-turno é roteada para a MESMA conexão enquanto ela permanecer saudável, evitando a perda do prompt-cache do provider (custo 5-10× sem stickiness, efeito conhecido no dario/clewdr). Implementação: - `open-sse/services/combo/sessionStickiness.ts` (novo, <800 linhas): mapa em memória (messageHash → connectionId) com TTL 15 min + cap 500 entradas; `applySessionStickiness` promove a conexão sticky ao índice 0 dos targets ordenados pelo strategy, guardado por `computeHeadroom > 0.15` (threshold); quando saturada (headroom ≤ 0.15), o binding é limpo e a seleção normal reage. Hash da sessão = SHA-256 dos primeiros chars da 1ª mensagem user → 16 hex chars. Seam de teste: `__setStickinessHeadroomFetcherForTests`. - `open-sse/services/combo.ts`: import + 2 pontos de integração (pré-eval-scores e pós-success), dentro do orçamento congelado de 3180 linhas. - `tests/unit/combo-session-stickiness.test.ts`: 19 testes node:test + assert/strict, todos via injeção de fetcher (zero rede/DB). Threshold 0.15: conexão a >85% de utilização está a um burst de rate-limit; o benefício de cache não compensa manter-se numa conexão degradada. Valor alinhado com a zona de soft-penalty do restante do engine de quota-share. * test(combo): isola combo-strategies da session stickiness (#5) selectedConnectionFor reusa o mesmo body, então o sticky map (#5) fixava a connection após a 1ª chamada e quebrava o round-robin tie-break do teste reset-aware. Limpa o sticky map no início da helper — a stickiness tem suíte própria (combo-session-stickiness). Sem enfraquecer asserts. --------- Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): buckets multi-janela por conexão (5h/7d/per-model) [Fase 3 #3] (#4928) Co-authored-by: Diego Rodrigues de Sa e Souza * refactor(providers): decompõe catálogo providers.ts em módulos de dados (godfile sweep, #3501) (#4917) Integrado em release/v3.8.36 (godfile sweep providers.ts, #3501) * refactor(pricing): decompõe pricing.ts em shared-tiers + DEFAULT_PRICING particionado (godfile sweep, #3501) (#4918) Integrado em release/v3.8.36 (godfile sweep pricing.ts, #3501) * refactor(api): extrai camada-folha pura de validation.ts (URL/headers/transport) (#4921) Integrado em release/v3.8.36 (validation.ts split fatia 1 — leaf layer) * refactor(api): extrai validators web-cookie + Meta AI de validation.ts (#4922) Integrado em release/v3.8.36 (validation.ts split fatia 2 — web-cookie + Meta AI) * refactor(api): extrai validators enterprise-cloud + probe compartilhado de validation.ts (#4923) Integrado em release/v3.8.36 (validation.ts split fatia 3 — enterprise-cloud + probe) * refactor(api): extrai validators áudio/speech + misc apikey de validation.ts (#4930) Integrado em release/v3.8.36 (validation.ts split fatia 4 — áudio/speech + misc apikey) * feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] (#4939) * feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] Estratégia interna "quota-share" isolada num módulo dedicado — NÃO toca a seleção/ fair-share genérica (decisão do dono: não mexer no que já funciona). Os combos qtSd/ (quotaCombos.ts) passam de fill-first para essa strategy; combo.ts ganha só 1 branch de dispatch que delega 100% ao módulo (nenhum case existente alterado). - quotaShareStrategy.ts: gating per-model (isBucketSaturated do #3) + DRR (quantum proporcional ao weight) + P2C sobre carga in-flight. - quotaShareInflight.ts: contador in-flight com TTL/lease de 120s — fallback do decrement-on-abort sem precisar instrumentar o combo genérico. - "quota-share" registrada como strategy INTERNA (não exposta na UI). - testes de síntese (quota-combo-balancing, quota-multiprovider) alinhados: a strategy esperada dos combos qtSd/ passa de "fill-first" para "quota-share" (alinhamento ao novo comportamento intencional, não mascaramento — os 73 testes de qtSd/ seguem verdes). * test(quota-share): alinha 2 scope-guards ao godfile sweep (base-reds que bloqueavam o CI) Dois testes de "arquivo contém X" quebraram por decomposições de godfile que outras sessões mergearam no release DURANTE a validação de #9 — NÃO são regressão de #9 (que não toca validation/oauth). Alinhados ao novo layout, asserts preservados: - proxy-bypass-scope-guard #3226: bypassProxyPatch foi extraído de validation.ts para validation/headers.ts (split #4921–#4930) → o teste lê a camada de validação. - sse-error-passthrough #3324: a windsurf authHint foi extraída de providers.ts para providers/oauth.ts → o teste lê o novo local. --------- Co-authored-by: Diego Rodrigues de Sa e Souza * refactor(api): extrai validators search + embedding/rerank de validation.ts (#4932) Integrated into release/v3.8.36 * refactor(api): extrai format-validators (OpenAI/Anthropic) de validation.ts (#4933) Integrated into release/v3.8.36 * refactor(db): extrai model-permission matching de db/apiKeys.ts (#4936) Integrated into release/v3.8.36 * refactor(db): extrai row-parsers + tipos compartilhados de db/apiKeys.ts (#4943) Integrated into release/v3.8.36 * refactor(db): extrai column-mapping (snake↔camel) de db/core.ts (#4947) Integrated into release/v3.8.36 * refactor(db): extrai schema-column reconciliation de db/core.ts (#4948) Integrated into release/v3.8.36 * refactor(sse): extrai scalar/format helpers de services/usage.ts (#4949) Integrated into release/v3.8.36 * refactor(sse): extrai quota-core (UsageQuota + builders) de services/usage.ts (#4950) Integrated into release/v3.8.36 * fix(translator): regroup parallel tool results adjacent to their assistant (#4714) (#4882) Integrated into release/v3.8.36 (fixes #4714) * fix(qoder): exchange PAT for jt-* job token before Cosy chat (#4683) (#4884) Integrated into release/v3.8.36 (fixes #4683) * refactor(sse): dedup fallback tool_call id helper (#4736) Integrated into release/v3.8.36 * refactor(open-sse): extract safeParseJSON util, dedup tryParseJSON (#4735) Integrated into release/v3.8.36 * fix(compression): eliminate ReDoS in math_inline preservation pattern (#4795) (#4838) Integrated into release/v3.8.36 (fixes #4795) * fix(combo): fetch models dynamically from custom provider endpoints (#4860) Integrated into release/v3.8.36 * feat(providers): update volcengine-ark model list with DeepSeek V4 (#4905) Integrated into release/v3.8.36 * fix(translator): provider thinking compatibility (DeepSeek/Gemini) (#4946) Integrated into release/v3.8.36 * feat(combo): task-aware routing strategy (#4945) Integrated into release/v3.8.36 * refactor(sse): extrai a família MiniMax de services/usage.ts (#4952) Integrated into release/v3.8.36 * refactor(sse): extrai a família GLM de services/usage.ts (#4953) Integrated into release/v3.8.36 * refactor(sse): extrai a família Antigravity de services/usage.ts (#4956) Integrated into release/v3.8.36 * fix(dashboard): show custom provider given-name instead of internal id across dashboard pages (#4603) (#4960) Integrated into release/v3.8.36 (fixes #4603) * fix(api): evict stale in-memory rate-limit windows to stop slow heap leak (#4041) (#4957) Integrated into release/v3.8.36 (fixes #4041) * fix(api): parse /v1/responses body once instead of 3-4x on the hot path (#4041) (#4958) Integrated into release/v3.8.36 (fixes #4041) * fix(translator): preserve legitimate empty-string tool arguments in openai-to-claude streaming (#4951) (#4959) Integrated into release/v3.8.36 (fixes #4951) * chore(quality): reconcile file-size baseline for #4960 provider-display-name (#4961) Integrated into release/v3.8.36 * fix(dashboard): restore home provider-topology card hidden by #4596 default (#4963) Integrated into release/v3.8.36 — restores home topology card (#4596 regression) * fix(build): drop @omniroute/open-sse from optimizePackageImports (build OOM) (#4968) Integrated into release/v3.8.36 — fixes build OOM (optimizePackageImports open-sse) * fix(quota): migração 107 ativa estratégia quota-share nos combos qtSd/ existentes [Fase 3 #9] (#4962) Integrated into release/v3.8.36 * feat(quota): respeita max_concurrent por conexão no roteamento (#4965) Integrated into release/v3.8.36 * feat(quota): combo quota-share espera cooldown curto e re-despacha (Variante A) (#4967) Integrated into release/v3.8.36 * fix(quality): resolve base-reds da release — db-rules allowlist + task-aware router precedence (#4973) Dois base-reds pré-existentes que reprovavam o CI da release v3.8.36 (Fast Quality Gates + Unit Tests fast-path), independentes de qualquer feature em voo: 1. check:db-rules / allowlist: os módulos db-internal caseMapping (#4947) e schemaColumns (#4948), extraídos de db/core.ts e importados só por ele, não estavam em INTENTIONALLY_INTERNAL. Registrados na allowlist (correção canônica — são internos legítimos, não re-exportados pelo localDb). 2. auto-strategy honra LKGP/cost (combo-routing-engine.test.ts, 2 testes): o task-aware reordering (#4945, reorderByTaskWeight) roda para strategy "auto" e era aplicado DEPOIS do router explícito (selectWithStrategy: lkgp/cost), sobrescrevendo o orderedTargets[0] que o operador escolheu. Instrumentação provou: post-filter [0]=claude (LKGP) → post-task [0]=gpt-oss. Correção: quando o auto usa router explícito, preserva o [0] dele e deixa o task-aware refinar só a cauda de fallback. gpt-oss-120b PERMANECE tool-capable (não é mudança de catálogo; o model-capabilities-registry test segue verde). Validado: 121 testes (combo-routing-engine + combo-task-aware + registry) verdes, red-check confirmado, db-rules/file-size/typecheck/lint/prettier OK. Co-authored-by: Diego Rodrigues de Sa e Souza * feat(quota): serializa concorrência por conexão no caminho quota-share (FASE 2.1) (#4970) O gating de quota-share em selectQuotaShareTarget é fail-open: uma conexão at-cap só é despriorizada, nunca bloqueada. Com 1 conexão por conta de assinatura (caso comum), chamadas concorrentes ainda floodam a conta (→ 429 + cooldown) — provado live na .15: 3 chamadas concorrentes com max_concurrent=1 despacharam todas em 94ms. Adiciona um semáforo POR CONEXÃO em torno do dispatch quota-share: chamadas excedentes esperam na fila em vez de floodar (key qsconn:, cap = max_concurrent da conexão). Fail-open em fila saturada/timeout para nunca piorar disponibilidade. Gated por strategy===quota-share + kill-switch resilienceSettings.quotaShareConcurrencyLimit (default on; UI no ResilienceTab). Lógica extraível isolada no leaf puro combo/quotaShareConcurrency.ts (unit-testado: estabilidade da key, no-op sem cap, serialização real, fail-open). Settings + schema + UI espelham comboCooldownWait. Co-authored-by: Diego Rodrigues de Sa e Souza * docs(resilience): document Quota-Share Concurrency Control (max_concurrent + serialization + cooldown-wait) (#4980) Documents the v3.8.36 quota-share concurrency layers in RESILIENCE_GUIDE.md: per-connection max_concurrent cap, the quota-share request serialization semaphore (FASE 2.1, qsconn:, fail-open, kill-switch), and the combo cooldown-aware retry — so operators know how to cap a subscription account's concurrency and why the routing gate alone cannot contain a single-connection flood. Co-authored-by: Diego Rodrigues de Sa e Souza * fix(dashboard): proxy-pool success gating, sync timestamp, opt-in Redis (#4878) (#4988) Co-authored-by: Diego Rodrigues de Sa e Souza * fix(sse): fail over on 400 responses carrying rate-limit text (#4976) (#4986) * fix(sse): fail over on 400 responses carrying rate-limit text (#4976) * chore(quality): rebaseline accountFallback.ts file-size for #4976 fix --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(compression): stop RTK over-truncating file-read tool results (#4559) (#4987) * fix(compression): stop RTK over-truncating file-read tool results (#4559) * chore(quality): trim #4559 comment to keep rtk/index.ts within size cap --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954) (#4989) * fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954) * chore(quality): rebaseline auth.ts file-size for #4954 (+39: synthetic no-auth providerSpecificData hydration of fingerprints/accountProxies; irreducible credential-path wiring, covered by opencode-proxy-rotation-4954.test.ts + 159 auth/noauth regression) --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540) (#4990) * fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540) * chore(quality): document STATUS_SOFT_DEPRIORITIZE_FACTOR + rebaseline combo.ts for #4540 --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(dashboard): switch to visible filter after auto-hiding failed models in test-all (#4887) (#4991) * fix(dashboard): switch to visible filter after auto-hiding failed models in OAuth provider test-all (#4887) * test(dashboard): move #4887 test into tests/unit/ui so a CI runner collects it --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(pollinations): only enable jsonMode when JSON output is requested (#3981) (#5009) Co-authored-by: Diego Rodrigues de Sa e Souza * fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003) (#5008) * fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003) * docs(changelog): restore #3981 pollinations entry eaten by merge --------- Co-authored-by: Diego Rodrigues de Sa e Souza * fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665) (#5010) * fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665) MODEL_MAP was missing the advertised catalog ids gpt-5.5, gpt-5.5-pro, gpt-5.4-pro and gpt-5.2-pro, so MODEL_MAP[model] ?? model sent the dot-form id verbatim to the ChatGPT backend-api, which silently rejected it and served the default Plus model. Map each to its dash-form slug. gpt-4-5 is already dash-form and falls through correctly, so it is intentionally left unmapped. Extends the executor MODEL_MAP test with the four ids and adds a drift guard asserting every advertised dot-form catalog id reaches the backend in dash-form (never verbatim), guarding future catalog<->map drift. file-size: tests/unit/chatgpt-web.test.ts frozen baseline 2809->2855 (+46) for the added test cases and drift-guard test; executor source unchanged in baseline. * docs(changelog): restore #3981/#5003 entries eaten by merge --------- Co-authored-by: Diego Rodrigues de Sa e Souza * feat(combos): add editable per-combo description field persisted via /api/combos (#5005) (#5011) * feat(combos): add editable per-combo description field persisted via /api/combos (#5005) * docs(changelog): restore #3981/#5003/#4665 entries eaten by merge --------- Co-authored-by: Diego Rodrigues de Sa e Souza * Fix Ollama Cloud max reasoning effort (#4993) Integrated into release/v3.8.36 * fix(copilot): replace execSync with execFile to prevent command injection (#5024) Integrated into release/v3.8.36 * fix(plugin): auth.json dual-key fallback for auto-prefix migration (#5027) Integrated into release/v3.8.36 * feat(endpoint): per-endpoint custom system prompt injection (#5022) Integrated into release/v3.8.36 * fix(headroom): translate openai-responses input through OpenAI for compression (#5023) Integrated into release/v3.8.36 * docs(changelog): add entries for #4993, #5024, #5027 (release notes credit) * fix(api): stop /api/system/env/repair 500 on packaged install (#5006) (#5028) * fix(api): stop /api/system/env/repair 500 on packaged install — lazy createRequire in sync-env.mjs (#5006) scripts/dev/sync-env.mjs ran createRequire(import.meta.url) at module top-level. When webpack bundles it into the standalone env-repair route, import.meta.url is frozen to the build-machine path (file:///home/runner/...) and createRequire throws during module evaluation, so the whole route module fails to load and every GET returns HTTP 500 — breaking the onboarding wizard on packaged/global installs. - Move createRequire into the guarded better-sqlite3 block (only place that needs it); a bad import.meta.url now returns the safe default. - resolveRootDir() falls back to process.cwd() when fileURLToPath throws. - route.ts passes an explicit rootDir (process.cwd()) so the helper never derives the root from the frozen import.meta.url, matching the .env target used by createEnvBackup(). - Regression guard: assert sync-env.mjs has no top-level createRequire + getEnvSyncPlan(oauth) works with explicit rootDir without throwing. * docs(changelog): restore #4993/#5023/#5024/#5027 + custom-system-prompt/headroom entries eaten by release merge * chore(quality): rebaseline 3 inherited base-reds from release merge Files NOT touched by this PR — grew on release/v3.8.36 via --admin merges and inherited here through 'git merge origin/release': - open-sse/executors/base.ts 1414->1416 (#4993 Ollama Cloud max-effort) - src/lib/db/settings.ts 1149->1151 (#5023 custom system prompt) - src/app/(dashboard)/.../endpoint/EndpointPageClient.tsx 2570->2612 (custom system prompt UI) * chore(release): finalize v3.8.36 CHANGELOG + docs (2026-06-25) --------- Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com> Co-authored-by: Makcim Ivanov Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com> Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com> Co-authored-by: Demiurge The Single Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Éder Costa Co-authored-by: Jefferson Felizardo Co-authored-by: Arthur Bodera Co-authored-by: Hamsa_M <116961508+hamsa0x7@users.noreply.github.com> Co-authored-by: Hernan Javier Ardila Sanchez --- .env.example | 73 +- .github/workflows/ci.yml | 1 + .github/workflows/quality.yml | 48 + @omniroute/opencode-plugin/src/index.ts | 20 +- .../opencode-plugin/tests/config-shim.test.ts | 59 + CHANGELOG.md | 176 + README.md | 69 +- bin/_ops-common.sh | 70 + bin/cli/runtime/nativeDeps.mjs | 2 +- bin/cli/runtime/sqliteRuntime.mjs | 2 +- bin/cli/runtime/trayRuntime.ts | 54 +- bin/cli/tray/tray.ts | 8 +- bin/cli/tray/traySystray.mjs | 24 + bin/cold-start-bench.sh | 82 + bin/restore-data.sh | 64 + bin/restore-policies.sh | 77 + bin/rollback.sh | 102 + bin/snapshot-data.sh | 61 + config/quality/file-size-baseline.json | 73 +- config/quality/quality-baseline.json | 3 +- docs/INCIDENT_RESPONSE.md | 174 + docs/PERF_BUDGETS.md | 222 + docs/architecture/RESILIENCE_GUIDE.md | 40 + docs/compression/COMPRESSION_ENGINES.md | 11 +- docs/frameworks/AGENTBRIDGE.md | 49 + docs/guides/USER_GUIDE.md | 4 +- docs/i18n/ar/CHANGELOG.md | 175 + docs/i18n/az/CHANGELOG.md | 175 + docs/i18n/bg/CHANGELOG.md | 175 + docs/i18n/bn/CHANGELOG.md | 175 + docs/i18n/cs/CHANGELOG.md | 175 + docs/i18n/da/CHANGELOG.md | 175 + docs/i18n/de/CHANGELOG.md | 175 + docs/i18n/es/CHANGELOG.md | 175 + docs/i18n/fa/CHANGELOG.md | 175 + docs/i18n/fi/CHANGELOG.md | 175 + docs/i18n/fr/CHANGELOG.md | 175 + docs/i18n/gu/CHANGELOG.md | 175 + docs/i18n/he/CHANGELOG.md | 175 + docs/i18n/hi/CHANGELOG.md | 175 + docs/i18n/hu/CHANGELOG.md | 175 + docs/i18n/id/CHANGELOG.md | 175 + docs/i18n/in/CHANGELOG.md | 175 + docs/i18n/it/CHANGELOG.md | 175 + docs/i18n/ja/CHANGELOG.md | 175 + docs/i18n/ko/CHANGELOG.md | 175 + docs/i18n/mr/CHANGELOG.md | 175 + docs/i18n/ms/CHANGELOG.md | 175 + docs/i18n/nl/CHANGELOG.md | 175 + docs/i18n/no/CHANGELOG.md | 175 + docs/i18n/phi/CHANGELOG.md | 175 + docs/i18n/pl/CHANGELOG.md | 175 + docs/i18n/pt-BR/CHANGELOG.md | 175 + docs/i18n/pt/CHANGELOG.md | 175 + docs/i18n/ro/CHANGELOG.md | 175 + docs/i18n/ru/CHANGELOG.md | 175 + docs/i18n/sk/CHANGELOG.md | 175 + docs/i18n/sv/CHANGELOG.md | 175 + docs/i18n/sw/CHANGELOG.md | 175 + docs/i18n/ta/CHANGELOG.md | 175 + docs/i18n/te/CHANGELOG.md | 175 + docs/i18n/th/CHANGELOG.md | 175 + docs/i18n/tr/CHANGELOG.md | 175 + docs/i18n/uk-UA/CHANGELOG.md | 175 + docs/i18n/ur/CHANGELOG.md | 175 + docs/i18n/vi/CHANGELOG.md | 175 + docs/i18n/zh-CN/CHANGELOG.md | 175 + docs/openapi.yaml | 2 +- docs/ops/DATABASE_GUIDE.md | 5 +- docs/ops/RELEASE_CHECKLIST.md | 21 +- docs/ops/RELEASE_GREEN.md | 89 + docs/ops/meta.json | 1 + docs/providers/AGENTROUTER.md | 2 +- docs/reference/ENVIRONMENT.md | 23 +- docs/security/STEALTH_GUIDE.md | 6 +- electron/package-lock.json | 4 +- electron/package.json | 2 +- next.config.mjs | 12 +- open-sse/config/anthropicHeaders.ts | 2 +- open-sse/config/codexClient.ts | 2 +- open-sse/config/providers/index.ts | 2 + .../providers/registry/codebuddy-cn/index.ts | 151 + .../providers/registry/opencode/go/index.ts | 6 + .../providers/registry/perplexity/index.ts | 5 + open-sse/config/providers/shared.ts | 3 + open-sse/config/videoRegistry.ts | 17 + open-sse/executors/antigravity.ts | 41 +- open-sse/executors/base.ts | 13 +- open-sse/executors/chatgpt-web.ts | 4 + open-sse/executors/claudeIdentity.ts | 2 +- open-sse/executors/codebuddy-cn.ts | 48 + open-sse/executors/codex.ts | 103 +- open-sse/executors/default.ts | 12 + open-sse/executors/github.ts | 32 +- open-sse/executors/index.ts | 4 + open-sse/executors/opencode.ts | 175 +- open-sse/executors/pollinations.ts | 8 +- open-sse/executors/qoder.ts | 7 +- open-sse/handlers/chatCore.ts | 492 +- .../handlers/chatCore/clientUsageBuffer.ts | 54 + .../chatCore/compressionAnalyticsWrite.ts | 113 + .../chatCore/compressionComboPredicates.ts | 41 + .../handlers/chatCore/compressionSettings.ts | 35 + open-sse/handlers/chatCore/failureUsage.ts | 2 + open-sse/handlers/chatCore/jsonBodyToSse.ts | 68 + .../chatCore/nonStreamingResponseHeaders.ts | 46 + .../chatCore/nonStreamingUsageStats.ts | 2 + .../handlers/chatCore/outputStyleTelemetry.ts | 53 + open-sse/handlers/chatCore/pluginOnRequest.ts | 65 + .../chatCore/postCallGuardrailContext.ts | 47 + .../chatCore/quotaShareConsumption.ts | 3 + .../handlers/chatCore/semanticCacheStore.ts | 73 + .../handlers/chatCore/streamingPipeline.ts | 99 + .../chatCore/streamingResponseHeaders.ts | 39 + .../chatCore/streamingSemanticCacheStore.ts | 95 + .../handlers/chatCore/streamingUsageStats.ts | 2 + open-sse/handlers/embeddings.ts | 4 + open-sse/handlers/videoGeneration.ts | 5 + .../handlers/videoGeneration/googleFlow.ts | 219 + .../videoGeneration/googleFlowHandler.ts | 145 + open-sse/package.json | 2 +- open-sse/services/accountFallback.ts | 24 + open-sse/services/antigravity429Engine.ts | 1 - open-sse/services/ccBridgeTransforms.ts | 2 +- open-sse/services/claudeCodeCompatible.ts | 4 +- open-sse/services/claudeCodeToolRemapper.ts | 33 + open-sse/services/claudeHaikuConstraints.ts | 88 + open-sse/services/claudeUsageCooldown.ts | 51 + open-sse/services/combo.ts | 368 +- open-sse/services/combo/autoStrategy.ts | 17 + open-sse/services/combo/comboCooldownRetry.ts | 237 + open-sse/services/combo/comboPredicates.ts | 17 + open-sse/services/combo/concurrencyCaps.ts | 76 + open-sse/services/combo/headroomRanking.ts | 91 + .../services/combo/quotaShareConcurrency.ts | 74 + open-sse/services/combo/quotaShareInflight.ts | 133 + open-sse/services/combo/quotaShareStrategy.ts | 349 + open-sse/services/combo/quotaStrategies.ts | 144 + open-sse/services/combo/sessionStickiness.ts | 281 + open-sse/services/combo/types.ts | 12 + open-sse/services/compression/bodyAdapter.ts | 196 +- .../services/compression/engines/rtk/index.ts | 33 +- open-sse/services/compression/preservation.ts | 9 +- open-sse/services/fusion.ts | 327 + open-sse/services/qoderCli.ts | 109 +- open-sse/services/systemPrompt.ts | 53 + open-sse/services/taskAwareRouting.ts | 553 ++ open-sse/services/tokenRefresh.ts | 73 + open-sse/services/usage.ts | 1459 +-- open-sse/services/usage/antigravity.ts | 794 ++ open-sse/services/usage/codebuddy-cn.ts | 199 + open-sse/services/usage/glm.ts | 182 + open-sse/services/usage/minimax.ts | 346 + open-sse/services/usage/quota.ts | 85 + open-sse/services/usage/scalars.ts | 75 + open-sse/translator/helpers/claudeHelper.ts | 27 + open-sse/translator/helpers/geminiHelper.ts | 16 +- open-sse/translator/helpers/jsonUtil.ts | 18 + open-sse/translator/helpers/openaiHelper.ts | 8 + open-sse/translator/helpers/toolCallHelper.ts | 6 + open-sse/translator/index.ts | 16 +- open-sse/translator/paramSupport.ts | 60 + .../translator/request/claude-to-openai.ts | 93 +- .../translator/request/openai-to-claude.ts | 12 +- .../translator/request/openai-to-gemini.ts | 14 +- .../translator/response/claude-to-openai.ts | 8 +- .../translator/response/kiro-to-openai.ts | 3 +- .../translator/response/openai-responses.ts | 5 +- .../translator/response/openai-to-claude.ts | 19 +- open-sse/utils/proxyDispatcher.ts | 105 +- open-sse/utils/proxyFetch.ts | 22 +- open-sse/utils/reasoningContentInjector.ts | 73 + package-lock.json | 155 +- package.json | 6 +- scripts/check/check-db-rules.mjs | 3 + scripts/check/check-fabricated-docs.mjs | 1 + scripts/check/check-known-symbols.ts | 8 +- scripts/check/check-public-creds.mjs | 8 +- scripts/check/check-test-runner-api.mjs | 59 + scripts/dev/peer-stamp.mjs | 31 +- scripts/dev/sync-env.mjs | 27 +- scripts/ops/prune-stale-worktrees.sh | 31 + scripts/release/sync-changelog-i18n.mjs | 57 + .../(dashboard)/dashboard/HomePageClient.tsx | 13 +- .../dashboard/analytics/ComboHealthTab.tsx | 14 +- .../analytics/CompressionAnalyticsTab.tsx | 4 +- .../analytics/ProviderUtilizationTab.tsx | 8 +- .../analytics/RouteExplainabilityTab.tsx | 15 +- src/app/(dashboard)/dashboard/cache/page.tsx | 6 +- .../dashboard/combos/live/comboFlowModel.ts | 7 +- src/app/(dashboard)/dashboard/combos/page.tsx | 29 + .../dashboard/costs/CostOverviewTab.tsx | 5 +- .../dashboard/endpoint/EndpointPageClient.tsx | 48 + .../health/ProviderHealthAutopilotCard.tsx | 4 +- src/app/(dashboard)/dashboard/health/page.tsx | 32 +- .../(dashboard)/dashboard/homeAppearance.ts | 18 + .../dashboard/provider-stats/page.tsx | 53 +- .../components/modals/EditConnectionModal.tsx | 44 +- .../[id]/hooks/useModelVisibilityHandlers.ts | 9 + .../dashboard/runtime/RuntimePageClient.tsx | 12 +- .../settings/components/ResilienceTab.tsx | 216 + .../settings/components/SystemStorageTab.tsx | 8 +- .../components/proxy/CloudflareRelayModal.tsx | 165 + .../components/proxy/DenoRelayModal.tsx | 154 + .../settings/components/proxy/FreePoolTab.tsx | 6 +- .../components/proxy/ProxyPoolTab.tsx | 127 +- src/app/api/cli-tools/_lib/jsoncConfig.ts | 51 + .../api/cli-tools/claude-settings/route.ts | 18 +- src/app/api/cli-tools/cline-settings/route.ts | 28 +- src/app/api/cli-tools/droid-settings/route.ts | 17 +- src/app/api/cli-tools/kilo-settings/route.ts | 16 +- .../api/cli-tools/openclaw-settings/route.ts | 17 +- src/app/api/combos/[id]/route.ts | 148 +- src/app/api/combos/route.ts | 20 +- .../api/internal/codex-responses-ws/route.ts | 1 + .../api/oauth/[provider]/[action]/route.ts | 10 +- src/app/api/providers/[id]/test/route.ts | 7 + src/app/api/resilience/route.ts | 18 +- .../compression/run-telemetry/route.ts | 9 +- .../free-proxies/[id]/add-to-pool/route.ts | 16 +- .../api/settings/free-proxies/sync/route.ts | 8 +- .../settings/proxy/cloudflare-deploy/route.ts | 178 + .../api/settings/proxy/deno-deploy/route.ts | 369 + .../api/settings/proxy/vercel-deploy/route.ts | 22 +- src/app/api/system/env/repair/route.ts | 11 +- src/app/api/v1/messages/route.ts | 7 +- src/app/api/v1/models/aliasBackedModels.ts | 50 + src/app/api/v1/models/catalog.ts | 145 +- src/app/api/v1/responses/route.ts | 62 +- src/app/api/v1/videos/generations/route.ts | 7 +- src/hooks/useLiveDashboard.ts | 9 +- src/i18n/messages/ar.json | 5 +- src/i18n/messages/az.json | 5 +- src/i18n/messages/bg.json | 5 +- src/i18n/messages/bn.json | 5 +- src/i18n/messages/cs.json | 5 +- src/i18n/messages/da.json | 5 +- src/i18n/messages/de.json | 5 +- src/i18n/messages/en.json | 43 +- src/i18n/messages/es.json | 5 +- src/i18n/messages/fa.json | 5 +- src/i18n/messages/fi.json | 5 +- src/i18n/messages/fr.json | 5 +- src/i18n/messages/gu.json | 5 +- src/i18n/messages/he.json | 5 +- src/i18n/messages/hi.json | 5 +- src/i18n/messages/hu.json | 5 +- src/i18n/messages/id.json | 5 +- src/i18n/messages/in.json | 5 +- src/i18n/messages/it.json | 1509 +++- src/i18n/messages/ja.json | 1509 +++- src/i18n/messages/ko.json | 1509 +++- src/i18n/messages/mr.json | 1509 +++- src/i18n/messages/ms.json | 1509 +++- src/i18n/messages/nl.json | 1509 +++- src/i18n/messages/no.json | 1509 +++- src/i18n/messages/phi.json | 1509 +++- src/i18n/messages/pl.json | 1509 +++- src/i18n/messages/pt-BR.json | 8048 +++++++++-------- src/i18n/messages/pt.json | 1506 ++- src/i18n/messages/ro.json | 1509 +++- src/i18n/messages/ru.json | 445 +- src/i18n/messages/sk.json | 1509 +++- src/i18n/messages/sv.json | 1509 +++- src/i18n/messages/sw.json | 5 +- src/i18n/messages/ta.json | 5 +- src/i18n/messages/te.json | 5 +- src/i18n/messages/th.json | 5 +- src/i18n/messages/tr.json | 5 +- src/i18n/messages/uk-UA.json | 5 +- src/i18n/messages/ur.json | 5 +- src/i18n/messages/vi.json | 5 +- src/i18n/messages/zh-CN.json | 3 + src/instrumentation-node.ts | 12 + src/lib/api/comboErrorResponse.ts | 80 + src/lib/copilot/tools.ts | 17 +- src/lib/db/apiKeys.ts | 309 +- src/lib/db/apiKeys/modelPermissions.ts | 122 + src/lib/db/apiKeys/rowParsers.ts | 161 + src/lib/db/apiKeys/types.ts | 21 + src/lib/db/caseMapping.ts | 80 + src/lib/db/core.ts | 369 +- src/lib/db/databaseSettings.ts | 18 +- src/lib/db/freeProxies.ts | 36 +- .../db/migrations/046_database_settings.sql | 2 +- .../104_normalize_database_cache_size.sql | 10 + .../migrations/105_usage_history_endpoint.sql | 6 + .../106_quota_allocation_model_caps.sql | 32 + .../107_quota_combos_quota_share_strategy.sql | 25 + src/lib/db/optimizationSettings.ts | 284 + src/lib/db/proxies.ts | 26 +- src/lib/db/quotaModelCaps.ts | 128 + src/lib/db/quotaPools.ts | 16 +- src/lib/db/schemaColumns.ts | 217 + src/lib/db/settings.ts | 2 + src/lib/db/usageAnalytics.ts | 73 + src/lib/display/useProviderNodeMap.ts | 68 + src/lib/localDb.ts | 8 + src/lib/oauth/constants/oauth.ts | 30 + src/lib/oauth/providers/codebuddy-cn.ts | 123 + src/lib/oauth/providers/index.ts | 2 + src/lib/oauth/providers/kiro.ts | 13 +- src/lib/oauth/services/kiro.ts | 7 +- src/lib/providers/validation.ts | 4009 +------- .../providers/validation/anthropicFormat.ts | 319 + .../validation/audioMiscProviders.ts | 582 ++ .../providers/validation/cloudProviders.ts | 669 ++ .../providers/validation/directChatProbe.ts | 47 + .../validation/embeddingProviders.ts | 143 + src/lib/providers/validation/headers.ts | 120 + src/lib/providers/validation/metaAi.ts | 116 + src/lib/providers/validation/openaiFormat.ts | 473 + .../providers/validation/searchProviders.ts | 206 + src/lib/providers/validation/transport.ts | 109 + src/lib/providers/validation/urlHelpers.ts | 114 + src/lib/providers/validation/webProvidersA.ts | 681 ++ src/lib/providers/validation/webProvidersB.ts | 460 + src/lib/proxyRelay/cloudflareWorkerScript.ts | 107 + src/lib/quota/accountBuckets.ts | 225 + src/lib/quota/connectionRecovery.ts | 303 + src/lib/quota/enforce.ts | 111 +- src/lib/quota/fairShare.ts | 26 +- src/lib/quota/quotaCombos.ts | 98 +- src/lib/quota/saturationSignals.ts | 370 +- src/lib/quota/spendRecorder.ts | 2 + src/lib/quota/types.ts | 13 + src/lib/resilience/modelLockoutSettings.ts | 18 +- src/lib/resilience/settings.ts | 97 + src/lib/usage/usageHistory.ts | 5 +- src/middleware/promptInjectionGuard.ts | 15 +- src/server/authz/headers.ts | 16 + src/server/authz/peerStamp.ts | 65 + src/server/authz/pipeline.ts | 28 +- src/server/authz/policies/management.ts | 25 +- src/server/ws/liveServer.ts | 52 +- src/server/ws/liveServerAllowList.ts | 106 + src/shared/components/ModelSelectModal.tsx | 91 +- src/shared/components/OAuthModal.tsx | 3 +- src/shared/components/flow/FlowCanvas.tsx | 48 +- src/shared/components/lobeProviderIcons.ts | 1 + src/shared/constants/errorCodes.ts | 44 + src/shared/constants/pricing.ts | 1570 +--- .../constants/pricing/default-pricing.ts | 16 + src/shared/constants/pricing/frontier-labs.ts | 384 + .../constants/pricing/inference-hosts.ts | 207 + .../constants/pricing/oauth-subscriptions.ts | 646 ++ src/shared/constants/pricing/regional.ts | 152 + src/shared/constants/pricing/shared-tiers.ts | 152 + src/shared/constants/providers.ts | 2864 +----- .../providers/apikey/enterprise-cloud.ts | 214 + .../providers/apikey/frontier-labs.ts | 249 + .../constants/providers/apikey/gateways.ts | 511 ++ .../constants/providers/apikey/index.ts | 21 + .../providers/apikey/inference-hosts.ts | 294 + .../constants/providers/apikey/regional.ts | 343 + .../providers/apikey/specialty-media.ts | 251 + src/shared/constants/providers/audio.ts | 71 + src/shared/constants/providers/cloud-agent.ts | 36 + src/shared/constants/providers/local.ts | 149 + src/shared/constants/providers/noauth.ts | 103 + src/shared/constants/providers/oauth.ts | 215 + src/shared/constants/providers/search.ts | 125 + src/shared/constants/providers/system.ts | 16 + .../constants/providers/upstream-proxy.ts | 36 + src/shared/constants/providers/web-cookie.ts | 296 + src/shared/constants/routingStrategies.ts | 35 +- src/shared/network/remoteImageFetch.ts | 64 + src/shared/utils/modelCatalogSearch.ts | 8 +- src/shared/utils/rateLimiter.ts | 82 +- src/shared/validation/freeProxySchemas.ts | 58 + src/shared/validation/schemas/combo.ts | 3 + src/shared/validation/schemas/settings.ts | 24 + src/shared/validation/settingsSchemas.ts | 2 +- src/sse/handlers/chat.ts | 15 +- src/sse/handlers/resolveRoutingModel.ts | 17 + src/sse/services/auth.ts | 57 +- src/sse/services/model.ts | 108 +- src/types/databaseSettings.ts | 2 +- tests/integration/chat-pipeline.test.ts | 2 +- ...accountfallback-ratelimit-400-4976.test.ts | 44 + ...opic-compat-validation-v1-messages.test.ts | 80 + ...ropic-stream-thinking-close-marker.test.ts | 109 + .../antigravity-429-quota-cooldown.test.ts | 11 + tests/unit/antigravity-model-aliases.test.ts | 14 +- .../antigravity-safetysettings-5003.test.ts | 60 + tests/unit/api/combo-error-response.test.ts | 116 + .../apikeys-model-permissions-split.test.ts | 42 + tests/unit/apikeys-row-parsers-split.test.ts | 90 + .../base-executor-sanitize-effort.test.ts | 25 + .../unit/build/check-test-runner-api.test.ts | 35 + .../optional-transformers-dependency.test.ts | 45 + tests/unit/build/sync-changelog-i18n.test.ts | 47 + .../unit/chatcore-client-usage-buffer.test.ts | 78 + ...atcore-compression-analytics-write.test.ts | 129 + ...tcore-compression-combo-predicates.test.ts | 64 + .../chatcore-compression-settings.test.ts | 44 + tests/unit/chatcore-json-body-to-sse.test.ts | 73 + ...core-nonstreaming-response-headers.test.ts | 73 + .../chatcore-output-style-telemetry.test.ts | 102 + tests/unit/chatcore-plugin-onrequest.test.ts | 73 + ...hatcore-postcall-guardrail-context.test.ts | 72 + .../chatcore-semantic-cache-store.test.ts | 109 + .../chatcore-streaming-cache-store.test.ts | 96 + .../unit/chatcore-streaming-pipeline.test.ts | 102 + ...hatcore-streaming-response-headers.test.ts | 65 + tests/unit/chatgpt-web.test.ts | 46 + .../check-db-rules-classification.test.ts | 5 +- tests/unit/check-public-creds.test.ts | 69 +- ...claude-codex-identity-version-sync.test.ts | 47 + .../claude-haiku-no-adaptive-thinking.test.ts | 99 + tests/unit/claude-oauth-tool-cloak.test.ts | 43 + .../claude-oauth-usage-429-backoff.test.ts | 66 + tests/unit/cli-tools-settings-jsonc.test.ts | 127 + tests/unit/cli-tools.test.ts | 4 +- tests/unit/cli-tray-systray2.test.ts | 75 + tests/unit/codebuddy-cn-provider.test.ts | 262 + .../codex-drop-nonstandard-events.test.ts | 51 + tests/unit/combo-cooldown-retry.test.ts | 271 + tests/unit/combo-description-5005.test.ts | 91 + tests/unit/combo-fusion-strategy.test.ts | 209 + tests/unit/combo-headroom-ranking.test.ts | 132 + tests/unit/combo-headroom-strategy.test.ts | 147 + tests/unit/combo-pin-health-gate.test.ts | 77 + .../combo-quota-share-cooldown-wait.test.ts | 234 + .../unit/combo-routes-composite-tiers.test.ts | 38 +- tests/unit/combo-session-stickiness.test.ts | 322 + tests/unit/combo-strategies.test.ts | 6 + tests/unit/combo-task-aware.test.ts | 429 + .../combo/auto-status-penalty-4540.test.ts | 101 + .../combo/effective-max-concurrency.test.ts | 46 + .../combo/quota-share-concurrency.test.ts | 132 + .../compression-run-telemetry-auth.test.ts | 45 + .../preservation-redos-4795.test.ts | 59 + .../rtk-file-read-no-truncate-4559.test.ts | 108 + .../copilot-agent-antigravity-parity.test.ts | 96 + ...t-gemini-claude-route-no-responses.test.ts | 93 + tests/unit/copilot-runOmniRouteCli.test.ts | 29 + .../custom-endpoint-system-prompt.test.ts | 156 + .../unit/custom-provider-combo-models.test.ts | 109 + .../database-settings-maintenance.test.ts | 75 + tests/unit/db-case-mapping-split.test.ts | 70 + tests/unit/db-quota-pools.test.ts | 59 + tests/unit/db-schema-columns-split.test.ts | 65 + .../unit/deno-deploy-relay-path-ssrf.test.ts | 104 + .../direct-dispatcher-pipelining-4580.test.ts | 33 +- tests/unit/display-and-error-utils.test.ts | 27 + tests/unit/executor-codex.test.ts | 12 +- tests/unit/executor-github.test.ts | 27 + ...executors-strip-unsupported-params.test.ts | 121 + ...mini-antigravity-pattern-preserved.test.ts | 54 + tests/unit/google-flow-video-4569.test.ts | 214 + ...isionBridgeHelpers.callVisionModel.test.ts | 21 + tests/unit/headroom-responses-format.test.ts | 138 + ...ome-provider-topology-default-4596.test.ts | 27 + .../image-compat-node-alias-shadow.test.ts | 80 + tests/unit/image-generation-handler.test.ts | 23 + tests/unit/kiro-region-ssrf-guard.test.ts | 96 + .../unit/kiro-toolresult-compression.test.ts | 238 + .../llm-selector-custom-vision-models.test.ts | 74 + ...migration-107-quota-share-strategy.test.ts | 140 + tests/unit/nanobanana-image-handler.test.ts | 20 + tests/unit/next-config.test.ts | 19 + tests/unit/oauth-providers-config.test.ts | 31 +- .../openai-developer-role-normalize.test.ts | 38 + ...encode-deepseek-reasoning-injected.test.ts | 94 + .../opencode-go-catalog-alignment.test.ts | 45 + .../unit/opencode-proxy-rotation-4954.test.ts | 168 + tests/unit/ops-scripts.test.ts | 184 + .../perplexity-key-validation-models.test.ts | 50 + tests/unit/pollinations-jsonmode-3981.test.ts | 38 + tests/unit/pricing-constants-split.test.ts | 56 + ...-request-capture-toolname-map-4091.test.ts | 17 +- .../unit/provider-validation-branches.test.ts | 10 +- tests/unit/providers-constants-split.test.ts | 87 + .../proxy-bypass-scope-guard-3226.test.ts | 15 +- ...y-pool-cloudflare-workers-deployer.test.ts | 183 + .../unit/proxy-pool-deno-deploy-relay.test.ts | 100 + tests/unit/proxy-pool-sync-4878.test.ts | 140 + tests/unit/qoder-display-label.test.ts | 17 + tests/unit/qoder-executor.test.ts | 24 +- .../unit/qoder-jobtoken-exchange-4683.test.ts | 129 + tests/unit/quota-account-buckets.test.ts | 222 + tests/unit/quota-combo-balancing.test.ts | 41 +- tests/unit/quota-combos-sync.test.ts | 87 + tests/unit/quota-connection-recovery.test.ts | 212 + .../quota-epsilon-unconfigured-allow.test.ts | 98 + .../unit/quota-exclusive-catalog-4806.test.ts | 182 + tests/unit/quota-fair-share.test.ts | 46 + tests/unit/quota-multiprovider.test.ts | 42 +- .../unit/quota-per-key-model-hotpath.test.ts | 166 + tests/unit/quota-per-key-model.test.ts | 201 + .../quota-saturation-anthropic-oauth.test.ts | 188 + .../quota-saturation-token-headers.test.ts | 306 + tests/unit/quota-share-strategy.test.ts | 492 + .../rate-limit-queue-timeout-lockout.test.ts | 140 + tests/unit/rate-limiter-eviction-4041.test.ts | 116 + .../remote-image-fetch-dns-rebinding.test.ts | 121 + tests/unit/remote-image-fetch.test.ts | 7 + ...ience-settings-combo-cooldown-wait.test.ts | 102 + ...e-settings-quota-share-concurrency.test.ts | 66 + tests/unit/resolve-routing-model.test.ts | 32 + tests/unit/responses-parse-once-4041.test.ts | 205 + .../route-guard-loopback-via-proxy.test.ts | 112 + .../sqliteRuntime-node26-compat.test.ts | 41 + .../security/live-server-allowlist.test.ts | 177 + tests/unit/sse-error-passthrough-3324.test.ts | 4 +- .../sync-env-bundled-require-5006.test.ts | 102 + tests/unit/t20-t22-provider-headers.test.ts | 2 +- tests/unit/t28-model-catalog-updates.test.ts | 17 + ...-claude-to-openai-orphan-tool-4385.test.ts | 115 +- ...tor-gemini-schema-pattern-property.test.ts | 13 +- tests/unit/translator-helper-branches.test.ts | 20 + tests/unit/translator-json-util.test.ts | 45 + ...nslator-resp-empty-string-tool-arg.test.ts | 236 + ...nslator-strip-client-metadata-1157.test.ts | 26 + ...ator-thinking-provider-compat-2043.test.ts | 187 + .../translator-tools-anthropic-shape.test.ts | 119 + ...-xiaomi-mimo-reasoning-replay-1321.test.ts | 44 + tests/unit/tray-kill-orphan-pid.test.ts | 97 + ...odel-visibility-handlers-test-all.test.tsx | 142 + .../usage-antigravity-family-split.test.ts | 45 + tests/unit/usage-endpoint-dimension.test.ts | 125 + tests/unit/usage-glm-family-split.test.ts | 30 + tests/unit/usage-minimax-family-split.test.ts | 49 + tests/unit/usage-quota-core-split.test.ts | 52 + tests/unit/usage-scalars-split.test.ts | 69 + tests/unit/v1-models-aliases-included.test.ts | 79 + .../unit/validation-audio-misc-split.test.ts | 34 + .../validation-cloud-providers-split.test.ts | 38 + ...validation-format-validators-split.test.ts | 44 + tests/unit/validation-helpers-split.test.ts | 57 + .../validation-search-embedding-split.test.ts | 33 + .../validation-web-providers-split.test.ts | 60 + .../vercel-deploy-relay-path-ssrf.test.ts | 87 + 534 files changed, 68304 insertions(+), 16622 deletions(-) create mode 100644 bin/_ops-common.sh create mode 100755 bin/cold-start-bench.sh create mode 100755 bin/restore-data.sh create mode 100755 bin/restore-policies.sh create mode 100755 bin/rollback.sh create mode 100755 bin/snapshot-data.sh create mode 100644 docs/INCIDENT_RESPONSE.md create mode 100644 docs/PERF_BUDGETS.md create mode 100644 docs/ops/RELEASE_GREEN.md create mode 100644 open-sse/config/providers/registry/codebuddy-cn/index.ts create mode 100644 open-sse/executors/codebuddy-cn.ts create mode 100644 open-sse/handlers/chatCore/clientUsageBuffer.ts create mode 100644 open-sse/handlers/chatCore/compressionAnalyticsWrite.ts create mode 100644 open-sse/handlers/chatCore/compressionComboPredicates.ts create mode 100644 open-sse/handlers/chatCore/compressionSettings.ts create mode 100644 open-sse/handlers/chatCore/jsonBodyToSse.ts create mode 100644 open-sse/handlers/chatCore/nonStreamingResponseHeaders.ts create mode 100644 open-sse/handlers/chatCore/outputStyleTelemetry.ts create mode 100644 open-sse/handlers/chatCore/pluginOnRequest.ts create mode 100644 open-sse/handlers/chatCore/postCallGuardrailContext.ts create mode 100644 open-sse/handlers/chatCore/semanticCacheStore.ts create mode 100644 open-sse/handlers/chatCore/streamingPipeline.ts create mode 100644 open-sse/handlers/chatCore/streamingResponseHeaders.ts create mode 100644 open-sse/handlers/chatCore/streamingSemanticCacheStore.ts create mode 100644 open-sse/handlers/videoGeneration/googleFlow.ts create mode 100644 open-sse/handlers/videoGeneration/googleFlowHandler.ts create mode 100644 open-sse/services/claudeHaikuConstraints.ts create mode 100644 open-sse/services/claudeUsageCooldown.ts create mode 100644 open-sse/services/combo/comboCooldownRetry.ts create mode 100644 open-sse/services/combo/concurrencyCaps.ts create mode 100644 open-sse/services/combo/headroomRanking.ts create mode 100644 open-sse/services/combo/quotaShareConcurrency.ts create mode 100644 open-sse/services/combo/quotaShareInflight.ts create mode 100644 open-sse/services/combo/quotaShareStrategy.ts create mode 100644 open-sse/services/combo/sessionStickiness.ts create mode 100644 open-sse/services/fusion.ts create mode 100644 open-sse/services/taskAwareRouting.ts create mode 100644 open-sse/services/usage/antigravity.ts create mode 100644 open-sse/services/usage/codebuddy-cn.ts create mode 100644 open-sse/services/usage/glm.ts create mode 100644 open-sse/services/usage/minimax.ts create mode 100644 open-sse/services/usage/quota.ts create mode 100644 open-sse/services/usage/scalars.ts create mode 100644 open-sse/translator/helpers/jsonUtil.ts create mode 100644 open-sse/translator/paramSupport.ts create mode 100644 open-sse/utils/reasoningContentInjector.ts create mode 100644 scripts/check/check-test-runner-api.mjs create mode 100755 scripts/ops/prune-stale-worktrees.sh create mode 100644 scripts/release/sync-changelog-i18n.mjs create mode 100644 src/app/(dashboard)/dashboard/homeAppearance.ts create mode 100644 src/app/(dashboard)/dashboard/settings/components/proxy/CloudflareRelayModal.tsx create mode 100644 src/app/(dashboard)/dashboard/settings/components/proxy/DenoRelayModal.tsx create mode 100644 src/app/api/cli-tools/_lib/jsoncConfig.ts create mode 100644 src/app/api/settings/proxy/cloudflare-deploy/route.ts create mode 100644 src/app/api/settings/proxy/deno-deploy/route.ts create mode 100644 src/app/api/v1/models/aliasBackedModels.ts create mode 100644 src/lib/api/comboErrorResponse.ts create mode 100644 src/lib/db/apiKeys/modelPermissions.ts create mode 100644 src/lib/db/apiKeys/rowParsers.ts create mode 100644 src/lib/db/apiKeys/types.ts create mode 100644 src/lib/db/caseMapping.ts create mode 100644 src/lib/db/migrations/104_normalize_database_cache_size.sql create mode 100644 src/lib/db/migrations/105_usage_history_endpoint.sql create mode 100644 src/lib/db/migrations/106_quota_allocation_model_caps.sql create mode 100644 src/lib/db/migrations/107_quota_combos_quota_share_strategy.sql create mode 100644 src/lib/db/optimizationSettings.ts create mode 100644 src/lib/db/quotaModelCaps.ts create mode 100644 src/lib/db/schemaColumns.ts create mode 100644 src/lib/display/useProviderNodeMap.ts create mode 100644 src/lib/oauth/providers/codebuddy-cn.ts create mode 100644 src/lib/providers/validation/anthropicFormat.ts create mode 100644 src/lib/providers/validation/audioMiscProviders.ts create mode 100644 src/lib/providers/validation/cloudProviders.ts create mode 100644 src/lib/providers/validation/directChatProbe.ts create mode 100644 src/lib/providers/validation/embeddingProviders.ts create mode 100644 src/lib/providers/validation/headers.ts create mode 100644 src/lib/providers/validation/metaAi.ts create mode 100644 src/lib/providers/validation/openaiFormat.ts create mode 100644 src/lib/providers/validation/searchProviders.ts create mode 100644 src/lib/providers/validation/transport.ts create mode 100644 src/lib/providers/validation/urlHelpers.ts create mode 100644 src/lib/providers/validation/webProvidersA.ts create mode 100644 src/lib/providers/validation/webProvidersB.ts create mode 100644 src/lib/proxyRelay/cloudflareWorkerScript.ts create mode 100644 src/lib/quota/accountBuckets.ts create mode 100644 src/lib/quota/connectionRecovery.ts create mode 100644 src/server/ws/liveServerAllowList.ts create mode 100644 src/shared/constants/pricing/default-pricing.ts create mode 100644 src/shared/constants/pricing/frontier-labs.ts create mode 100644 src/shared/constants/pricing/inference-hosts.ts create mode 100644 src/shared/constants/pricing/oauth-subscriptions.ts create mode 100644 src/shared/constants/pricing/regional.ts create mode 100644 src/shared/constants/pricing/shared-tiers.ts create mode 100644 src/shared/constants/providers/apikey/enterprise-cloud.ts create mode 100644 src/shared/constants/providers/apikey/frontier-labs.ts create mode 100644 src/shared/constants/providers/apikey/gateways.ts create mode 100644 src/shared/constants/providers/apikey/index.ts create mode 100644 src/shared/constants/providers/apikey/inference-hosts.ts create mode 100644 src/shared/constants/providers/apikey/regional.ts create mode 100644 src/shared/constants/providers/apikey/specialty-media.ts create mode 100644 src/shared/constants/providers/audio.ts create mode 100644 src/shared/constants/providers/cloud-agent.ts create mode 100644 src/shared/constants/providers/local.ts create mode 100644 src/shared/constants/providers/noauth.ts create mode 100644 src/shared/constants/providers/oauth.ts create mode 100644 src/shared/constants/providers/search.ts create mode 100644 src/shared/constants/providers/system.ts create mode 100644 src/shared/constants/providers/upstream-proxy.ts create mode 100644 src/shared/constants/providers/web-cookie.ts create mode 100644 src/sse/handlers/resolveRoutingModel.ts create mode 100644 tests/unit/accountfallback-ratelimit-400-4976.test.ts create mode 100644 tests/unit/anthropic-compat-validation-v1-messages.test.ts create mode 100644 tests/unit/anthropic-stream-thinking-close-marker.test.ts create mode 100644 tests/unit/antigravity-safetysettings-5003.test.ts create mode 100644 tests/unit/api/combo-error-response.test.ts create mode 100644 tests/unit/apikeys-model-permissions-split.test.ts create mode 100644 tests/unit/apikeys-row-parsers-split.test.ts create mode 100644 tests/unit/build/check-test-runner-api.test.ts create mode 100644 tests/unit/build/optional-transformers-dependency.test.ts create mode 100644 tests/unit/build/sync-changelog-i18n.test.ts create mode 100644 tests/unit/chatcore-client-usage-buffer.test.ts create mode 100644 tests/unit/chatcore-compression-analytics-write.test.ts create mode 100644 tests/unit/chatcore-compression-combo-predicates.test.ts create mode 100644 tests/unit/chatcore-compression-settings.test.ts create mode 100644 tests/unit/chatcore-json-body-to-sse.test.ts create mode 100644 tests/unit/chatcore-nonstreaming-response-headers.test.ts create mode 100644 tests/unit/chatcore-output-style-telemetry.test.ts create mode 100644 tests/unit/chatcore-plugin-onrequest.test.ts create mode 100644 tests/unit/chatcore-postcall-guardrail-context.test.ts create mode 100644 tests/unit/chatcore-semantic-cache-store.test.ts create mode 100644 tests/unit/chatcore-streaming-cache-store.test.ts create mode 100644 tests/unit/chatcore-streaming-pipeline.test.ts create mode 100644 tests/unit/chatcore-streaming-response-headers.test.ts create mode 100644 tests/unit/claude-codex-identity-version-sync.test.ts create mode 100644 tests/unit/claude-haiku-no-adaptive-thinking.test.ts create mode 100644 tests/unit/claude-oauth-usage-429-backoff.test.ts create mode 100644 tests/unit/cli-tools-settings-jsonc.test.ts create mode 100644 tests/unit/cli-tray-systray2.test.ts create mode 100644 tests/unit/codebuddy-cn-provider.test.ts create mode 100644 tests/unit/codex-drop-nonstandard-events.test.ts create mode 100644 tests/unit/combo-cooldown-retry.test.ts create mode 100644 tests/unit/combo-description-5005.test.ts create mode 100644 tests/unit/combo-fusion-strategy.test.ts create mode 100644 tests/unit/combo-headroom-ranking.test.ts create mode 100644 tests/unit/combo-headroom-strategy.test.ts create mode 100644 tests/unit/combo-pin-health-gate.test.ts create mode 100644 tests/unit/combo-quota-share-cooldown-wait.test.ts create mode 100644 tests/unit/combo-session-stickiness.test.ts create mode 100644 tests/unit/combo-task-aware.test.ts create mode 100644 tests/unit/combo/auto-status-penalty-4540.test.ts create mode 100644 tests/unit/combo/effective-max-concurrency.test.ts create mode 100644 tests/unit/combo/quota-share-concurrency.test.ts create mode 100644 tests/unit/compression-run-telemetry-auth.test.ts create mode 100644 tests/unit/compression/preservation-redos-4795.test.ts create mode 100644 tests/unit/compression/rtk-file-read-no-truncate-4559.test.ts create mode 100644 tests/unit/copilot-agent-antigravity-parity.test.ts create mode 100644 tests/unit/copilot-gemini-claude-route-no-responses.test.ts create mode 100644 tests/unit/copilot-runOmniRouteCli.test.ts create mode 100644 tests/unit/custom-endpoint-system-prompt.test.ts create mode 100644 tests/unit/custom-provider-combo-models.test.ts create mode 100644 tests/unit/db-case-mapping-split.test.ts create mode 100644 tests/unit/db-schema-columns-split.test.ts create mode 100644 tests/unit/deno-deploy-relay-path-ssrf.test.ts create mode 100644 tests/unit/executors-strip-unsupported-params.test.ts create mode 100644 tests/unit/gemini-antigravity-pattern-preserved.test.ts create mode 100644 tests/unit/google-flow-video-4569.test.ts create mode 100644 tests/unit/headroom-responses-format.test.ts create mode 100644 tests/unit/home-provider-topology-default-4596.test.ts create mode 100644 tests/unit/image-compat-node-alias-shadow.test.ts create mode 100644 tests/unit/kiro-region-ssrf-guard.test.ts create mode 100644 tests/unit/kiro-toolresult-compression.test.ts create mode 100644 tests/unit/llm-selector-custom-vision-models.test.ts create mode 100644 tests/unit/migration-107-quota-share-strategy.test.ts create mode 100644 tests/unit/openai-developer-role-normalize.test.ts create mode 100644 tests/unit/opencode-deepseek-reasoning-injected.test.ts create mode 100644 tests/unit/opencode-go-catalog-alignment.test.ts create mode 100644 tests/unit/opencode-proxy-rotation-4954.test.ts create mode 100644 tests/unit/ops-scripts.test.ts create mode 100644 tests/unit/perplexity-key-validation-models.test.ts create mode 100644 tests/unit/pollinations-jsonmode-3981.test.ts create mode 100644 tests/unit/pricing-constants-split.test.ts create mode 100644 tests/unit/providers-constants-split.test.ts create mode 100644 tests/unit/proxy-pool-cloudflare-workers-deployer.test.ts create mode 100644 tests/unit/proxy-pool-deno-deploy-relay.test.ts create mode 100644 tests/unit/proxy-pool-sync-4878.test.ts create mode 100644 tests/unit/qoder-display-label.test.ts create mode 100644 tests/unit/qoder-jobtoken-exchange-4683.test.ts create mode 100644 tests/unit/quota-account-buckets.test.ts create mode 100644 tests/unit/quota-connection-recovery.test.ts create mode 100644 tests/unit/quota-epsilon-unconfigured-allow.test.ts create mode 100644 tests/unit/quota-exclusive-catalog-4806.test.ts create mode 100644 tests/unit/quota-per-key-model-hotpath.test.ts create mode 100644 tests/unit/quota-per-key-model.test.ts create mode 100644 tests/unit/quota-saturation-anthropic-oauth.test.ts create mode 100644 tests/unit/quota-saturation-token-headers.test.ts create mode 100644 tests/unit/quota-share-strategy.test.ts create mode 100644 tests/unit/rate-limit-queue-timeout-lockout.test.ts create mode 100644 tests/unit/rate-limiter-eviction-4041.test.ts create mode 100644 tests/unit/remote-image-fetch-dns-rebinding.test.ts create mode 100644 tests/unit/resilience-settings-combo-cooldown-wait.test.ts create mode 100644 tests/unit/resilience-settings-quota-share-concurrency.test.ts create mode 100644 tests/unit/resolve-routing-model.test.ts create mode 100644 tests/unit/responses-parse-once-4041.test.ts create mode 100644 tests/unit/route-guard-loopback-via-proxy.test.ts create mode 100644 tests/unit/runtime/sqliteRuntime-node26-compat.test.ts create mode 100644 tests/unit/security/live-server-allowlist.test.ts create mode 100644 tests/unit/sync-env-bundled-require-5006.test.ts create mode 100644 tests/unit/translator-json-util.test.ts create mode 100644 tests/unit/translator-resp-empty-string-tool-arg.test.ts create mode 100644 tests/unit/translator-strip-client-metadata-1157.test.ts create mode 100644 tests/unit/translator-thinking-provider-compat-2043.test.ts create mode 100644 tests/unit/translator-tools-anthropic-shape.test.ts create mode 100644 tests/unit/translator-xiaomi-mimo-reasoning-replay-1321.test.ts create mode 100644 tests/unit/tray-kill-orphan-pid.test.ts create mode 100644 tests/unit/ui/use-model-visibility-handlers-test-all.test.tsx create mode 100644 tests/unit/usage-antigravity-family-split.test.ts create mode 100644 tests/unit/usage-endpoint-dimension.test.ts create mode 100644 tests/unit/usage-glm-family-split.test.ts create mode 100644 tests/unit/usage-minimax-family-split.test.ts create mode 100644 tests/unit/usage-quota-core-split.test.ts create mode 100644 tests/unit/usage-scalars-split.test.ts create mode 100644 tests/unit/v1-models-aliases-included.test.ts create mode 100644 tests/unit/validation-audio-misc-split.test.ts create mode 100644 tests/unit/validation-cloud-providers-split.test.ts create mode 100644 tests/unit/validation-format-validators-split.test.ts create mode 100644 tests/unit/validation-helpers-split.test.ts create mode 100644 tests/unit/validation-search-embedding-split.test.ts create mode 100644 tests/unit/validation-web-providers-split.test.ts create mode 100644 tests/unit/vercel-deploy-relay-path-ssrf.test.ts diff --git a/.env.example b/.env.example index 3335784a6c..c2b069c2da 100644 --- a/.env.example +++ b/.env.example @@ -56,10 +56,12 @@ STORAGE_ENCRYPTION_KEY_VERSION=v1 DISABLE_SQLITE_AUTO_BACKUP=false # ── Redis (Rate Limiting) ── -# Redis connection URL for the rate limiter backend. +# Redis connection URL for the rate limiter backend. OPT-IN: leave this +# commented out to use the built-in in-memory rate limiter. Setting it to a +# non-running localhost (#4878) makes ioredis flood "[REDIS] Error:" logs. # Used by: src/shared/utils/rateLimiter.ts -# Default: redis://localhost:6379 (or redis://redis:6379 in Docker) -REDIS_URL=redis://localhost:6379 +# Example: redis://localhost:6379 (or redis://redis:6379 in Docker) +# REDIS_URL=redis://localhost:6379 # ═══════════════════════════════════════════════════════════════════════════════ # 3. NETWORK & PORTS @@ -551,6 +553,16 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500 # heuristic in instrumentation-node.ts. Default: unset (tests skip background). #OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS=1 +# Proactive connection-cooldown recovery (#8): re-validates connections whose +# transient `rate_limited_until` window has elapsed OUTSIDE the request hot path, +# so the first request after a cooldown does not pay the probe latency. Lazy +# recovery in getProviderCredentials still applies regardless. Used by: +# src/lib/quota/connectionRecovery.ts. +# Tick cadence (ms). Default 60000, floor 5000. +# OMNIROUTE_CONNECTION_RECOVERY_INTERVAL_MS=60000 +# Disable the proactive recovery scheduler entirely (default: false). +# OMNIROUTE_DISABLE_CONNECTION_RECOVERY=false + # Background job interval for budget reset checks (ms). Default: 600000 (10m). # Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000. #OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000 @@ -598,6 +610,11 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500 # Used by: scripts/postinstall.mjs. #OMNIROUTE_SKIP_POSTINSTALL=0 +# Operator-supplied JSON credentials for the offline compression-eval CLI +# (parsed with JSON.parse; leave unset for a dry run). Developer tooling only. +# Used by: scripts/compression-eval/index.ts. Default: {} (empty). +#OMNIROUTE_EVAL_CREDENTIALS={} + # Skip the DB healthcheck entirely on startup (useful for short-lived tasks / tests). # Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to disable. Default: 0. #OMNIROUTE_SKIP_DB_HEALTHCHECK=0 @@ -792,7 +809,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98 # Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup. # Update these when providers release new CLI versions to avoid blocks. -CLAUDE_USER_AGENT="claude-cli/2.1.158 (external, cli)" +CLAUDE_USER_AGENT="claude-cli/2.1.187 (external, cli)" # Disable the deterministic tool-name cloak applied on both Anthropic-bound paths # (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) — @@ -801,7 +818,7 @@ CLAUDE_USER_AGENT="claude-cli/2.1.158 (external, cli)" # stream with a misleading 400 out-of-extra-usage placeholder. Set to true to # forward the original names verbatim (debugging only). # CLAUDE_DISABLE_TOOL_NAME_CLOAK=false -CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)" +CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)" GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1" ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0" KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0" @@ -823,7 +840,13 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # Override Codex client version sent in headers independently of the # CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts. -# CODEX_CLIENT_VERSION=0.132.0 +# CODEX_CLIENT_VERSION=0.142.0 + +# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits) +# from the Codex Responses stream. These frames break the OpenAI SDK's +# responses.stream() with a 502 "Controller is already closed". Off by default; +# set to true/1/yes to enable. Used by: open-sse/executors/codex.ts. +# OMNIROUTE_CODEX_DROP_NONSTANDARD_EVENTS=true # ═══════════════════════════════════════════════════════════════════════════════ # 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection) @@ -950,6 +973,15 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD=2 # OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS=15000 +# ── Context-cache pin health gate ── +# Used by: open-sse/services/combo.ts. When a context-cache pin points at a +# provider that is durably unhealthy, the pin is dropped to allow failover. +# PIN_DROP_BACKOFF_LEVEL gates how deep a connection's backoff must be before the +# pin is considered durably unhealthy; PIN_DROP_GRACE_MS is the anti-flap window +# that tolerates brief transient cooldowns before dropping the pin. +# PIN_DROP_BACKOFF_LEVEL=2 +# PIN_DROP_GRACE_MS=20000 + # ── Stream idle detection ── # STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000) # # Extended-thinking models rarely pause >90s. @@ -1200,6 +1232,34 @@ APP_LOG_TO_FILE=true # Used by: open-sse/executors/cloudflare-ai.ts # CLOUDFLARE_ACCOUNT_ID= +# ── Deno Deploy proxy relay (#4643 / 9router#1437) ── +# Override the Deno Deploy REST API base used by the proxy-pool relay deployer. +# Default: https://api.deno.com/v2 (omit unless mocking). +# Used by: src/app/api/settings/proxy/deno-deploy/route.ts +# DENO_DEPLOY_API_BASE=https://api.deno.com/v2 + +# Default Deno Deploy app name suggested in the "Deploy Relay" modal. +# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/DenoRelayModal.tsx +# NEXT_PUBLIC_DENO_RELAY_DEFAULT_PROJECT=omniroute-deno-relay + +# Set to "false" to hide the Deno Deploy relay option from the Proxy Pool tab. +# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/ProxyPoolTab.tsx +# NEXT_PUBLIC_DENO_RELAY_ENABLED=true + +# ── Cloudflare Workers proxy relay (#4640 / 9router#1360) ── +# Override the Cloudflare REST API base used by the proxy-pool relay deployer. +# Default: https://api.cloudflare.com/client/v4 (omit unless mocking). +# Used by: src/app/api/settings/proxy/cloudflare-deploy/route.ts +# CLOUDFLARE_API_BASE=https://api.cloudflare.com/client/v4 + +# Default worker project name suggested in the "Deploy Relay" modal. +# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/CloudflareRelayModal.tsx +# NEXT_PUBLIC_CLOUDFLARE_RELAY_DEFAULT_PROJECT=omniroute-relay + +# Set to "false" to hide the Cloudflare Workers relay option from the Proxy Pool tab. +# Used by: src/app/(dashboard)/dashboard/settings/components/proxy/ProxyPoolTab.tsx +# NEXT_PUBLIC_CLOUDFLARE_RELAY_ENABLED=true + # ── Cloudflare Tunnel (cloudflared) ── # Custom path to cloudflared binary for tunnel management. # Used by: src/lib/cloudflaredTunnel.ts @@ -1637,6 +1697,7 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis) # QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo) # QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa +# STATUS_SOFT_DEPRIORITIZE_FACTOR=0.5 # 0..1; multiplicador do score p/ provider esgotado (credits_exhausted/rate_limited) quando preflight cutoff OFF (#4540) # QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos # QUOTA_PREFLIGHT_CUTOFF_ENABLED=false # opt-in (default OFF): hard quota cutoff drops low-quota candidates before auto-routing scoring diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da08470d4..5181ebc246 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -846,6 +846,7 @@ jobs: path: | coverage/coverage-summary.json coverage/coverage-report.md + coverage/lcov.info if-no-files-found: warn sonarqube: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b6cfcb2e8e..1c3fdc5e36 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -40,6 +40,8 @@ jobs: - run: npm run check:fetch-targets - run: npm run check:openapi-routes - run: npm run check:docs-symbols + - name: Docs accuracy (fabricated-docs + i18n mirrors, strict) + run: npm run check:docs-all - run: npm run check:deps - run: npm run check:file-size - run: npm run check:error-helper @@ -49,6 +51,7 @@ jobs: - run: npm run check:known-symbols - run: npm run check:route-guard-membership - run: npm run check:test-discovery + - run: npm run check:test-runner-api - run: npm run check:any-budget:t11 - name: Typecheck (core) run: npm run typecheck:core @@ -82,3 +85,48 @@ jobs: echo "Running impacted tests:"; echo "$SEL" mapfile -t FILES <<< "$SEL" node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}" + + fast-vitest: + name: Vitest (fast-path) + runs-on: ubuntu-latest + env: + JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-lint-api-key-secret-long + DISABLE_SQLITE_AUTO_BACKUP: "true" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.CI_NODE_VERSION }} + cache: npm + - run: npm ci + - run: npm run test:vitest + + fast-unit: + name: Unit Tests fast-path (${{ matrix.shard }}/2) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2] + env: + JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-lint-api-key-secret-long + DISABLE_SQLITE_AUTO_BACKUP: "true" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.CI_NODE_VERSION }} + cache: npm + - run: npm ci + - run: > + node --max-old-space-size=4096 --import tsx + --import ./tests/_setup/isolateDataDir.ts + --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 + tests/unit/*.test.ts + "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts index 6aae7a8238..058b1cb8c7 100644 --- a/@omniroute/opencode-plugin/src/index.ts +++ b/@omniroute/opencode-plugin/src/index.ts @@ -4406,8 +4406,24 @@ export function createOmniRouteConfigHook( authJson = undefined; } - const entry = authJson?.[resolved.providerId] as AuthJsonApiEntry | undefined; - const apiKey = entry && entry.type === "api" && typeof entry.key === "string" ? entry.key : ""; + // Try both prefixed (e.g. opencode-omniroute) and unprefixed (e.g. omniroute) + // keys so a user who ran `/connect omniroute` before the auto-prefix fix + // does not need to re-auth. Also handles dual-key for auth.json entries + // written by a newer OC dispatcher with the prefixed key. + const bareKey = resolved.providerId.startsWith("opencode-") + ? resolved.providerId.slice("opencode-".length) + : resolved.providerId; + const lookupKeys = [resolved.providerId]; + if (bareKey !== resolved.providerId) lookupKeys.push(bareKey); + let entry; + for (const k of lookupKeys) { + const e = authJson?.[k]; + if (e?.type === "api" && typeof e.key === "string" && e.key.length > 0) { + entry = e; + break; + } + } + const apiKey = entry?.type === "api" && typeof entry.key === "string" ? entry.key : ""; if (!apiKey) { // (c) no apiKey — silent no-op (with debug breadcrumb). The operator diff --git a/@omniroute/opencode-plugin/tests/config-shim.test.ts b/@omniroute/opencode-plugin/tests/config-shim.test.ts index 16cb3253cf..63d7c277ac 100644 --- a/@omniroute/opencode-plugin/tests/config-shim.test.ts +++ b/@omniroute/opencode-plugin/tests/config-shim.test.ts @@ -256,6 +256,65 @@ test("config: with valid auth.json + apiKey + baseURL → mutates input.provider assert.equal(combo.limit?.context, 200_000, "LCD: min(200_000, 1_000_000)"); }); +// ──────────────────────────────────────────────────────────────────────────── +// 1b. Dual-key fallback (#5027) — auth.json stored under the BARE providerId +// (pre-auto-prefix login) must still resolve when the active providerId is +// prefixed (`opencode-omniroute`). Without the fallback the lookup misses +// the stored key and the user is forced to re-auth. +// ──────────────────────────────────────────────────────────────────────────── + +test("config: auth.json under bare key (pre-prefix login) resolves via dual-key fallback", async () => { + // Stored under bare `omniroute` (the key OC wrote before the auto-prefix fix), + // but the resolved providerId is now `opencode-omniroute`. + const readAuthJson = stubReadAuthJson({ + omniroute: { type: "api", key: "sk-bare-1", baseURL: "https://or.example.com/v1" }, + }); + const fetcher = stubModelsFetcher([MODEL_CLAUDE]); + const combosFetcher = stubCombosFetcher([]); + const logger = captureWarn(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, // resolves to opencode-omniroute internally + { readAuthJson, fetcher, combosFetcher, logger } + ); + const input = makeInput(); + await hook(input); + + const provider = (input as { provider: Record }).provider; + const entry = provider["opencode-omniroute"]; + assert.ok(entry, "provider entry published from bare-key apiKey"); + assert.equal(entry.options.apiKey, "sk-bare-1", "apiKey resolved from the bare auth.json key"); + assert.equal(entry.options.baseURL, "https://or.example.com/v1"); +}); + +test("config: prefixed key wins over bare key when both present (dual-key precedence)", async () => { + const readAuthJson = stubReadAuthJson({ + "opencode-omniroute": { type: "api", key: "sk-prefixed", baseURL: "https://pref.example/v1" }, + omniroute: { type: "api", key: "sk-bare", baseURL: "https://bare.example/v1" }, + }); + const fetcher = stubModelsFetcher([MODEL_CLAUDE]); + const combosFetcher = stubCombosFetcher([]); + const logger = captureWarn(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { readAuthJson, fetcher, combosFetcher, logger } + ); + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry); + assert.equal( + entry.options.apiKey, + "sk-prefixed", + "prefixed key takes precedence (looked up first)" + ); + assert.equal(entry.options.baseURL, "https://pref.example/v1"); +}); + // ──────────────────────────────────────────────────────────────────────────── // 2. Missing auth.json → no-op, no throw, no mutation // ──────────────────────────────────────────────────────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4175f354..2a67ba8279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,181 @@ --- +## [3.8.36] — 2026-06-25 + +### ✨ New Features + +**Quota-Share system** + +- **feat(quota):** introduce a dedicated `quota-share` combo strategy (Fase 3 #9) — Deficit Round Robin scheduling with per-model in-flight gating (P2C), automatic DB migration that promotes existing `qtSd/*` combos, and per-policy gating so invalid allocations cannot bleed `allow` to unintended connections. ([#4939](https://github.com/diegosouzapw/OmniRoute/pull/4939), [#4901](https://github.com/diegosouzapw/OmniRoute/pull/4901)) +- **feat(quota):** multi-window usage buckets, per-(key,model) caps, and session stickiness — connections now track consumption across 5 h, 7 d, and per-model windows; `quota_allocation_model_caps` enforces per-key/model limits; session stickiness preserves prompt-cache integrity across turns. ([#4928](https://github.com/diegosouzapw/OmniRoute/pull/4928), [#4927](https://github.com/diegosouzapw/OmniRoute/pull/4927), [#4929](https://github.com/diegosouzapw/OmniRoute/pull/4929)) +- **feat(quota):** headroom strategy + proactive saturation — new `headroom` combo strategy selects connections by available quota margin; universal proactive saturation via upstream token-usage response headers; real Claude quota saturation sourced from `/api/oauth/usage`. ([#4908](https://github.com/diegosouzapw/OmniRoute/pull/4908), [#4907](https://github.com/diegosouzapw/OmniRoute/pull/4907), [#4885](https://github.com/diegosouzapw/OmniRoute/pull/4885)) +- **feat(quota):** concurrency control + cooldown-wait (Fase 2.1) — `max_concurrent` is enforced at dispatch time; quota-share combos queue concurrent requests with a short cooldown-wait and re-dispatch on slot availability (Variant A); a cron heal proactively restores connections after their window resets. ([#4965](https://github.com/diegosouzapw/OmniRoute/pull/4965), [#4970](https://github.com/diegosouzapw/OmniRoute/pull/4970), [#4967](https://github.com/diegosouzapw/OmniRoute/pull/4967), [#4900](https://github.com/diegosouzapw/OmniRoute/pull/4900)) + +**Combo routing** + +- **feat(combo):** task-aware routing strategy — routes requests to the best-fit connection based on task-type metadata, enabling per-task provider specialization within a combo. ([#4945](https://github.com/diegosouzapw/OmniRoute/pull/4945)) +- **feat(combo):** Fusion strategy (16th strategy) — fan out to a configurable panel of models in parallel, then synthesize results through a judge model. ([#4652](https://github.com/diegosouzapw/OmniRoute/pull/4652)) +- **feat(combos):** add an editable per-combo `description` field. The routing-combo form now has a Description input, persisted in the combo `data` blob via `/api/combos` (POST/PUT) and round-tripped through GET — no new DB column. ([#5005](https://github.com/diegosouzapw/OmniRoute/issues/5005)) +- **feat(routing):** honor `X-Route-Model` request header to override `body.model`, enabling per-request model switching without modifying the request body. ([#4863](https://github.com/diegosouzapw/OmniRoute/pull/4863) — thanks @costaeder) + +**Providers & models** + +- **feat(providers):** update volcengine-ark model list, adding DeepSeek-V4-Flash and DeepSeek-V4-Pro. ([#4905](https://github.com/diegosouzapw/OmniRoute/pull/4905) — thanks @kenlin8827) +- **feat(provider):** add CodeBuddy CN (`copilot.tencent.com`) — full OAuth + executor + model catalog stack. ([#4664](https://github.com/diegosouzapw/OmniRoute/pull/4664)) +- **feat(opencode-go):** advertise `glm-5.2` and `kimi-k2.7-code` to align with official Go endpoints. ([#4711](https://github.com/diegosouzapw/OmniRoute/pull/4711)) +- **feat(sse):** add Google Flow video-generation provider. ([#4769](https://github.com/diegosouzapw/OmniRoute/pull/4769)) +- **feat(api/v1):** include alias-backed models in the `/v1/models` listing. ([#4630](https://github.com/diegosouzapw/OmniRoute/pull/4630)) + +**Proxy pool** + +- **feat(proxy-pool):** Cloudflare Workers proxy deployer + pool integration — deploy Cloudflare Workers relays directly from the dashboard and register them in the proxy pool. ([#4640](https://github.com/diegosouzapw/OmniRoute/pull/4640)) +- **feat(proxy-pool):** Deno Deploy relays + group action buttons — deploy Deno Deploy relay workers and manage proxy groups with new bulk-action controls. ([#4643](https://github.com/diegosouzapw/OmniRoute/pull/4643)) + +**Compression & infrastructure** + +- **feat(compression):** Kiro/CodeWhisperer tool-result compression engine — dedicated compressor for Kiro/CodeWhisperer tool outputs integrated into the streaming pipeline. ([#4635](https://github.com/diegosouzapw/OmniRoute/pull/4635)) +- **feat(endpoint):** per-endpoint custom system prompt injection. A toggle + text field in the Endpoint settings card lets users inject a custom system prompt into every model request, applied via the existing system-prompt engine. Stored in settings DB. ([#5022](https://github.com/diegosouzapw/OmniRoute/pull/5022) — thanks @whale9820) +- **feat(live-ws):** allow non-loopback clients via `LIVE_WS_ALLOWED_HOSTS` env var, enabling multi-host setups to access the live WebSocket API. ([#4877](https://github.com/diegosouzapw/OmniRoute/pull/4877) — thanks @KooshaPari) +- **feat(db):** track API endpoint dimension on `usage_history` for per-endpoint cost and usage analytics. ([#4676](https://github.com/diegosouzapw/OmniRoute/pull/4676)) + +--- + +### 🔧 Bug Fixes + +**Translator** + +- **fix(translator):** regroup parallel tool results to be adjacent to their originating assistant turn, fixing tool-message ordering for providers that require strict interleaving. ([#4882](https://github.com/diegosouzapw/OmniRoute/pull/4882)) +- **fix(translator):** preserve literal empty-string tool arguments in OpenAI-to-Claude streaming — they were previously dropped, causing tool calls to arrive with missing parameters. ([#4959](https://github.com/diegosouzapw/OmniRoute/pull/4959)) +- **fix(translator):** normalize tools to Anthropic-native shape for non-Anthropic providers, ensuring tool definitions pass validation regardless of the format at the call site. ([#4650](https://github.com/diegosouzapw/OmniRoute/pull/4650)) +- **fix(translator):** provider thinking compatibility — correct thinking-block serialization for DeepSeek and Gemini providers. ([#4946](https://github.com/diegosouzapw/OmniRoute/pull/4946)) +- **fix(translator):** emit `` close marker for Anthropic thinking blocks, fixing truncated reasoning output in streamed responses. ([#4633](https://github.com/diegosouzapw/OmniRoute/pull/4633)) +- **fix(translator):** normalize `developer` role to `system` for OpenAI-format providers. ([#4625](https://github.com/diegosouzapw/OmniRoute/pull/4625)) +- **fix(translator):** strip top-level `client_metadata` on the OpenAI passthrough path (port from 9router#1157). ([#4624](https://github.com/diegosouzapw/OmniRoute/pull/4624)) +- **fix(translator):** replay `reasoning_content` on plain Xiaomi MiMo turns (port from 9router#1321). ([#4639](https://github.com/diegosouzapw/OmniRoute/pull/4639)) + +**Copilot / GitHub executor** + +- **fix(copilot):** never route Gemini/Claude model variants to the `/responses` endpoint — these models require the chat-completions path only. ([#4627](https://github.com/diegosouzapw/OmniRoute/pull/4627)) +- **fix(github):** route Copilot Codex models to `/responses` (port from 9router#102). ([#4626](https://github.com/diegosouzapw/OmniRoute/pull/4626)) +- **fix(copilot,antigravity):** cap `maxOutputTokens` at 16384 to stop "Invalid Argument" 400 errors on high-token requests. ([#4636](https://github.com/diegosouzapw/OmniRoute/pull/4636)) +- **fix(codex):** drop non-standard `codex.*` streaming events that break `responses.stream` consumers. ([#4715](https://github.com/diegosouzapw/OmniRoute/pull/4715) — thanks @jeffer1312) + +**Claude / Anthropic** + +- **fix(claude):** omit `adaptive_thinking` and `output_config.effort` for Haiku model variants, which reject those parameters. ([#4661](https://github.com/diegosouzapw/OmniRoute/pull/4661)) +- **fix(claude):** skip `mcp__` tool-name cloaking and guard against missing `connectionId` to prevent crashes on Claude-native MCP tool calls. ([#4861](https://github.com/diegosouzapw/OmniRoute/pull/4861) — thanks @costaeder) +- **fix(claude-oauth):** respect `429` backoff headers on the Claude OAuth usage endpoint to reduce polling spam during quota checks. ([#4655](https://github.com/diegosouzapw/OmniRoute/pull/4655)) + +**Routing & SSE** + +- **fix(sse):** fail over on `400` responses that carry rate-limit text in the body, not just on canonical `429` status codes. ([#4986](https://github.com/diegosouzapw/OmniRoute/pull/4986)) +- **fix(sse):** honor per-account proxy and fingerprint-rotation settings in the opencode executor. ([#4989](https://github.com/diegosouzapw/OmniRoute/pull/4989)) +- **fix(sse):** soft-penalize exhausted providers in auto-combo scoring instead of hard-excluding them, improving fallback resilience. ([#4990](https://github.com/diegosouzapw/OmniRoute/pull/4990)) +- **fix(sse):** drop the CCP pin when the pinned provider is durably unhealthy, with anti-flap logic to prevent oscillation. ([#4864](https://github.com/diegosouzapw/OmniRoute/pull/4864) — thanks @costaeder) +- **fix(combo):** fetch models dynamically from custom provider endpoints instead of relying on a static list. ([#4860](https://github.com/diegosouzapw/OmniRoute/pull/4860)) +- **fix(combo):** propagate the selected connection ID to fallback error responses so model lockout applies to the correct connection rather than the wrong fallback target. ([#4809](https://github.com/diegosouzapw/OmniRoute/pull/4809) — thanks @Chewji9875) +- **fix(sse):** skip third-party tool-name cloaking for Anthropic-native server tools to prevent naming conflicts. ([#4808](https://github.com/diegosouzapw/OmniRoute/pull/4808) — thanks @NomenAK) + +**Quota** + +- **fix(quota):** quota-exclusive `qtSd/*` connections now appear in `/v1/models` listings; EPSILON-threshold check no longer falsely blocks under-budget allocations. ([#4830](https://github.com/diegosouzapw/OmniRoute/pull/4830)) +- **fix(quota):** migration 107 correctly activates the `quota-share` strategy on existing `qtSd/*` combos. ([#4962](https://github.com/diegosouzapw/OmniRoute/pull/4962)) + +**API / responses** + +- **fix(api):** parse the `/v1/responses` request body once instead of 3–4 times on the hot path, reducing per-request overhead. ([#4958](https://github.com/diegosouzapw/OmniRoute/pull/4958)) +- **fix(api):** evict stale in-memory rate-limit windows to stop a slow heap leak on long-running instances. ([#4957](https://github.com/diegosouzapw/OmniRoute/pull/4957)) +- **fix(api):** require authentication on the compression `run-telemetry` endpoint; document `OMNIROUTE_EVAL_CREDENTIALS` env var. ([#4796](https://github.com/diegosouzapw/OmniRoute/pull/4796)) +- **fix(api):** stop `GET /api/system/env/repair` returning HTTP `500` on packaged installs (it broke the onboarding wizard). `createRequire(import.meta.url)` ran at module top-level; once webpack bundles the route into the standalone build, `import.meta.url` is frozen to the build-machine path and `createRequire` throws during evaluation, so the whole route failed to load. `createRequire` is now resolved lazily inside the guarded `better-sqlite3` block, root-dir resolution falls back to `process.cwd()`, and the route passes an explicit `rootDir`. ([#5028](https://github.com/diegosouzapw/OmniRoute/pull/5028)) + +**Dashboard** + +- **fix(dashboard):** show custom provider given-name instead of internal id across dashboard pages — cache, combo health, compression analytics, cost overview, health/autopilot, provider stats, route explainability, provider utilization, runtime. Adds shared `resolveProviderName` resolver and `useProviderNodeMap` hook. (#4603) +- **fix(dashboard):** on OAuth providers (e.g. GLM Coding), "Test all models" with auto-hide-failed now switches the model list to the "visible" filter after the run, so just-hidden failed models actually disappear on-screen — parity with the passthrough-provider path (#3610). Previously they were hidden in the DB but stayed visible under the "All" filter, so it looked like nothing was hidden. (#4887) +- **fix(dashboard):** restore the home-page provider topology card that was hidden by a default state change in #4596. ([#4963](https://github.com/diegosouzapw/OmniRoute/pull/4963)) +- **fix(dashboard):** proxy-pool success gating, sync-timestamp persistence, and opt-in Redis backend. ([#4988](https://github.com/diegosouzapw/OmniRoute/pull/4988)) +- **fix(dashboard):** show custom vision models in the LLM selector dropdown. ([#4653](https://github.com/diegosouzapw/OmniRoute/pull/4653)) + +**Providers** + +- **fix(pollinations):** stop forcing `jsonMode` on every request. Pollinations treats `jsonMode=true` as "the model MUST return JSON" and rejects (HTTP 400 "messages must contain the word 'json'") any normal chat request whose messages don't mention "json", so all non-JSON chat was broken. `jsonMode` is now only enabled when the caller actually requests JSON output (`response_format.type` of `json_object` or `json_schema`). (#3981) +- **fix(antigravity):** default `safetySettings` to all-OFF for parity with the native Gemini paths. The Antigravity (Google Cloud Code) request builder set `safetySettings: undefined`, which `JSON.stringify` drops — so no safety settings reached Google and its server-side defaults false-flagged benign technical prompts as `prohibited_content` (HTTP 200 + blocked body, which combo failover treats as terminal). Now honors a caller-supplied value and otherwise defaults to `DEFAULT_SAFETY_SETTINGS`, matching the claude-to-gemini / openai-to-gemini paths. (#5003) +- **fix(antigravity):** exclude the standard Gemini rate-limit message from quota-exhaustion keyword matching to prevent false-positive saturation flags. ([#4810](https://github.com/diegosouzapw/OmniRoute/pull/4810) — thanks @Chewji9875) +- **fix(chatgpt-web):** map the advertised `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4-pro` and `gpt-5.2-pro` catalog ids to their dash-form ChatGPT backend slugs. They were missing from `MODEL_MAP`, so the executor sent the dot-form id verbatim, which the ChatGPT backend silently ignored and served the default Plus model instead of the requested one. Adds a drift guard asserting no advertised dot-form id reaches the backend verbatim. (#4665) +- **fix(gemini):** preserve the `pattern` field in the Antigravity tool schema sanitizer to avoid stripping valid regex constraints from tool definitions. ([#4651](https://github.com/diegosouzapw/OmniRoute/pull/4651)) +- **fix(opencode):** preserve DeepSeek reasoning content in streamed responses. ([#4631](https://github.com/diegosouzapw/OmniRoute/pull/4631)) +- **fix(perplexity):** validate API keys via the `/v1/models` endpoint instead of issuing a full chat request. ([#4654](https://github.com/diegosouzapw/OmniRoute/pull/4654)) +- **fix(qoder):** exchange PAT for `jt-*` job token before initiating Cosy chat, fixing auth failures after the Qoder credential format change. ([#4884](https://github.com/diegosouzapw/OmniRoute/pull/4884)) +- **fix(executors):** strip parameters unsupported by the target provider/model to prevent `400 Invalid parameter` errors on strict endpoints. ([#4658](https://github.com/diegosouzapw/OmniRoute/pull/4658)) +- **fix(executors):** preserve literal `reasoning_effort: "max"` for Ollama Cloud instead of normalizing to `xhigh`. Ollama Cloud accepts `high|medium|low|max|none` and rejects `xhigh` (`invalid reasoning value: 'xhigh'`); OpenRouter DeepSeek `max→xhigh` normalization is unaffected. ([#4993](https://github.com/diegosouzapw/OmniRoute/pull/4993) — thanks @Thinkscape) +- **fix(headroom):** translate openai-responses input through OpenAI for external compression. `adaptBodyForCompression` now serialises `function_call_output` items whose `output` field is a JSON object (not a string) so compression engines can process the content — previously those items were excluded from compression because `hasTextContent()` returned false for object values. ([#5023](https://github.com/diegosouzapw/OmniRoute/pull/5023) — thanks @anki1kr) +- **fix(proxy):** fan out direct dispatcher streams to all registered proxy endpoints. ([#4803](https://github.com/diegosouzapw/OmniRoute/pull/4803) — thanks @makcimbx) + +**Compression** + +- **fix(compression):** eliminate ReDoS in the `math_inline` preservation regex — the previous pattern could catastrophically backtrack on untrusted input. ([#4838](https://github.com/diegosouzapw/OmniRoute/pull/4838)) +- **fix(compression):** stop RTK over-truncating file-read tool results — RTK now respects the full content length for file-read outputs. ([#4987](https://github.com/diegosouzapw/OmniRoute/pull/4987)) + +**Build / CLI / infrastructure** + +- **fix(build):** drop `@omniroute/open-sse` from `optimizePackageImports` to fix the Next.js build OOM crash. ([#4968](https://github.com/diegosouzapw/OmniRoute/pull/4968)) +- **fix(cli):** SIGKILL the systray child PID before closing the IPC channel to prevent macOS NSStatusItem orphan processes. ([#4732](https://github.com/diegosouzapw/OmniRoute/pull/4732)) +- **fix(cli):** bump `better-sqlite3` runtime pin to 12.10.1 for Node 26 compatibility. ([#4685](https://github.com/diegosouzapw/OmniRoute/pull/4685)) +- **fix(cli):** harden the systray2 tray runtime (port from 9router#1080). ([#4628](https://github.com/diegosouzapw/OmniRoute/pull/4628)) +- **fix(cli-tools):** tolerate JSONC (comments and trailing commas) in tool settings files. ([#4659](https://github.com/diegosouzapw/OmniRoute/pull/4659)) +- **fix(install):** make the `transformers` dependency optional so CUDA-host installs that lack Python bindings succeed. ([#4807](https://github.com/diegosouzapw/OmniRoute/pull/4807) — thanks @megamen32) +- **fix(db):** correct storage tuning settings to prevent WAL runaway on high-write workloads. ([#4834](https://github.com/diegosouzapw/OmniRoute/pull/4834) — thanks @rdself) +- **fix(image):** prevent compatible nodes from shadowing provider aliases in the image routing table. ([#4656](https://github.com/diegosouzapw/OmniRoute/pull/4656)) + +**Plugin** + +- **fix(plugin):** opencode `auth.json` dual-key fallback for the auto-prefix migration. The config hook now looks up both the prefixed (`opencode-omniroute`) and bare (`omniroute`) keys, so users who authenticated before the `opencode-` prefix landed no longer need to re-auth. ([#5027](https://github.com/diegosouzapw/OmniRoute/pull/5027) — thanks @herjarsa) + +--- + +### 🔒 Security + +- **fix(security):** block SSRF allowlist bypass via `x-relay-path` header manipulation on Deno/Vercel relays. ([#4899](https://github.com/diegosouzapw/OmniRoute/pull/4899)) +- **fix(security):** pin image-fetch DNS resolution to prevent SSRF DNS-rebinding attacks (GHSA-cmhj-wh2f-9cgx). ([#4634](https://github.com/diegosouzapw/OmniRoute/pull/4634)) +- **fix(security):** do not trust the loopback socket as local-only when the server is behind a reverse proxy, closing a potential auth bypass path. ([#4632](https://github.com/diegosouzapw/OmniRoute/pull/4632)) +- **fix(security):** validate the Kiro region parameter to prevent SSRF via crafted region strings (GHSA-6mwv-4mrm-5p3m). ([#4629](https://github.com/diegosouzapw/OmniRoute/pull/4629)) +- **fix(copilot):** replace `execSync` shell interpolation with `execFile` in the `runOmniRouteCli` tool to prevent command injection. The user-supplied command is now split into an argv array and passed to `execFile` (no shell), so shell metacharacters are treated as literal text; error output is routed through `sanitizeErrorMessage()`. ([#5024](https://github.com/diegosouzapw/OmniRoute/pull/5024) — thanks @hamsa0x7) + +--- + +### 📝 Maintenance + +**God-file decomposition (continued, #3501)** + +- **refactor(chatCore):** extracted 12 focused helpers from `chatCore.ts` covering the streaming pipeline (`assembleStreamingPipeline`), cache-store logic (`storeStreamingSemanticCacheResponse`, `storeSemanticCacheResponse`), response headers (`assembleStreamingResponseHeaders`, `buildNonStreamingResponseHeaders`), JSON→SSE bridge (`maybeConvertJsonBodyToSse`), guardrail context (`buildPostCallGuardrailContext`), usage buffer (`applyClientUsageBuffer`), plugin hook (`runPluginOnRequestHook`), analytics (`writeCompressionAnalytics`, `emitOutputStyleTelemetry`), and compression predicates/settings (`resolveCompressionSettings`, et al.). ([#4811](https://github.com/diegosouzapw/OmniRoute/pull/4811)–[#4837](https://github.com/diegosouzapw/OmniRoute/pull/4837)) +- **refactor(sse/db/api):** continued decomposition of `services/usage.ts` (extracted quota-core, scalar/format helpers, Antigravity/GLM/MiniMax usage families), `db/core.ts` (schema-column reconciliation, snake↔camel column-mapping), `db/apiKeys.ts` (row-parsers, model-permission matching), and `validation.ts` (URL/headers/transport leaf layer, web-cookie/Meta-AI validators, enterprise-cloud + probe, audio/speech/apikey, search/embedding/rerank, and OpenAI/Anthropic format validators). ([#4921](https://github.com/diegosouzapw/OmniRoute/pull/4921)–[#4956](https://github.com/diegosouzapw/OmniRoute/pull/4956)) +- **refactor(pricing/providers):** decomposed `pricing.ts` into shared tiers + partitioned `DEFAULT_PRICING` modules, and split the `providers.ts` catalog into semantic data modules organized by provider family. ([#4917](https://github.com/diegosouzapw/OmniRoute/pull/4917), [#4918](https://github.com/diegosouzapw/OmniRoute/pull/4918)) +- **refactor(open-sse):** extract `safeParseJSON` utility and dedup `tryParseJSON` call sites; extract and dedup the fallback `tool_call` ID generation helper. ([#4735](https://github.com/diegosouzapw/OmniRoute/pull/4735), [#4736](https://github.com/diegosouzapw/OmniRoute/pull/4736)) + +**Quality & CI** + +- **chore(quality):** release base-red reconciliation + ratchet rebaselines — file-size, env-doc, and catalog baseline updates across multiple gates. ([#4630](https://github.com/diegosouzapw/OmniRoute/pull/4630), [#4879](https://github.com/diegosouzapw/OmniRoute/pull/4879), [#4886](https://github.com/diegosouzapw/OmniRoute/pull/4886), [#4915](https://github.com/diegosouzapw/OmniRoute/pull/4915), [#4961](https://github.com/diegosouzapw/OmniRoute/pull/4961), [#4973](https://github.com/diegosouzapw/OmniRoute/pull/4973)) +- **ci(quality):** shift heavy validation gates to the PR→release merge fast-path to accelerate the release cycle. ([#4857](https://github.com/diegosouzapw/OmniRoute/pull/4857)) +- **fix(ci):** include `coverage/lcov.info` in the coverage-report artifact so SonarQube can consume it. ([#4670](https://github.com/diegosouzapw/OmniRoute/pull/4670)) +- **fix(test):** validate Anthropic-compatible connections via `POST /v1/messages` for accurate connectivity testing. ([#4657](https://github.com/diegosouzapw/OmniRoute/pull/4657)) + +**Docs** + +- **docs(resilience):** document Quota-Share Concurrency Control — `max_concurrent` enforcement, serialization behavior, and cooldown-wait semantics. ([#4980](https://github.com/diegosouzapw/OmniRoute/pull/4980)) +- **docs(perf):** add per-endpoint p50/p95/p99 latency and cost budgets reference. ([#4867](https://github.com/diegosouzapw/OmniRoute/pull/4867) — thanks @KooshaPari) +- **docs(ops):** add canonical incident response runbook. ([#4868](https://github.com/diegosouzapw/OmniRoute/pull/4868) — thanks @KooshaPari) +- **docs(ops):** document the release-green family — `green-prs`, `check:release-green`, `babysit`, and nightly gate workflows. ([#4679](https://github.com/diegosouzapw/OmniRoute/pull/4679)) +- **docs(agentbridge):** document Electron `NODE_EXTRA_CA_CERTS`, real model IDs, and identity caveat for agent bridge integrations. ([#4718](https://github.com/diegosouzapw/OmniRoute/pull/4718)) +- **docs:** clarify Kiro provides ~50 credits/month per account, not unlimited. ([#4690](https://github.com/diegosouzapw/OmniRoute/pull/4690)) + +**Misc** + +- **chore(claude,codex):** bump pinned CLI identities — Claude `2.1.158 → 2.1.187`, Codex `0.132.0 → 0.142.0`. ([#4883](https://github.com/diegosouzapw/OmniRoute/pull/4883)) +- **chore(dashboard):** rename Qoder display label from "Qoder AI" to "Qoder". ([#4733](https://github.com/diegosouzapw/OmniRoute/pull/4733)) + +--- + ## [3.8.35] — 2026-06-23 ### ✨ New Features @@ -19,6 +194,7 @@ - **Dashboard**: remove the dead, unconditional `useLiveRequests()` call from `HomePageClient.tsx` — it crashed the `/home` page in production builds with `ReferenceError: useLiveRequests is not defined` (#4759, #4745) and opened the live-dashboard WebSocket even when Provider Topology was hidden (#4596). The live feed remains owned by the settings-gated `HomeProviderTopologySection` ([#4761](https://github.com/diegosouzapw/OmniRoute/pull/4761) — thanks @diegosouzapw). - **Providers dashboard**: dedupe provider nodes by id when adding a compatible provider (`upsertProviderNodeById`) so the same provider can no longer appear twice and no-op adds don't invalidate the compatible-provider memo ([#4768](https://github.com/diegosouzapw/OmniRoute/pull/4768) — closes #4746, thanks @diegosouzapw). - **Storage VACUUM**: the scheduled VACUUM job now follows the Storage page settings (`scheduledVacuum` / `vacuumHour`) as the single source of truth; the legacy env-flag control path was removed ([#4726](https://github.com/diegosouzapw/OmniRoute/pull/4726) — thanks @rdself). +- **Storage SQLite tuning**: `Cache Size` is now a positive KiB setting (for example, `16384`) that applies to SQLite as `PRAGMA cache_size = -16384`; Page Size and Cache Size changes are applied to the live database instead of being persisted only in the settings table. - **Tiers**: no-auth providers are now counted as free, and the free-tier filter returns an empty set instead of falling through to every provider ([#4753](https://github.com/diegosouzapw/OmniRoute/pull/4753) — thanks @megamen32 / @diegosouzapw). - **Combos**: auto-promote `zeroLatencyOptimizationsEnabled` so legacy configs (pre-3.8.33 `fallbackCompressionMode="lite"`) round-trip cleanly on the first GUI edit ([#4774](https://github.com/diegosouzapw/OmniRoute/pull/4774) — thanks @KooshaPari / @diegosouzapw). diff --git a/README.md b/README.md index 150f630fd5..37bde4d92e 100644 --- a/README.md +++ b/README.md @@ -287,18 +287,19 @@ Result: 4 layers of fallback = zero downtime -> Recent highlights from **v3.8.20 → v3.8.35**. Full history in [`CHANGELOG.md`](CHANGELOG.md). +> Recent highlights from **v3.8.20 → v3.8.36**. Full history in [`CHANGELOG.md`](CHANGELOG.md). +- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key,model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) - **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode, Gemini CLI); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md) - **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`). → [Remote Mode](docs/guides/REMOTE-MODE.md) -- **🧭 Smarter auto-routing** — OpenRouter-style `auto/:` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, and `web_search`-aware routing to a configured model. → [Auto-Combo](docs/routing/AUTO-COMBO.md) +- **🧭 Smarter auto-routing** — OpenRouter-style `auto/:` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (16th — fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, and `web_search`-aware routing. → [Auto-Combo](docs/routing/AUTO-COMBO.md) - **🗜️ Pluggable compression** — an async pipeline of **9 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, and a unified panel with named profiles + an active-profile selector. → [Compression](docs/compression/COMPRESSION_ENGINES.md) - **🕵️ Transparent MITM decrypt (TPROXY)** — capture & translate traffic from CLIs that ignore proxy env vars, with a per-SNI certificate authority and a trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md) - **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md) - **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md) - **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md) -- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), a refreshed 231-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md) -- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel) and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout). → [Environment](docs/reference/ENVIRONMENT.md) +- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, a refreshed 231-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md) +- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout). → [Environment](docs/reference/ENVIRONMENT.md)
@@ -544,12 +545,12 @@ Code blocks, URLs, JSON and structured data are **always protected** by the pres The 9 engines above shrink what goes **in**. Three more layers shape **how**, **when**, and what comes **out**: -- **🪄 Output Styles** *(output-axis steering)* — inject deterministic, cache-safe response-shaping instructions; combinable, each at `lite` / `full` / `ultra` intensity. Adding a style is a one-line registry entry: +- **🪄 Output Styles** _(output-axis steering)_ — inject deterministic, cache-safe response-shaping instructions; combinable, each at `lite` / `full` / `ultra` intensity. Adding a style is a one-line registry entry: - **Terse prose** — drop filler / articles / hedging; keep technical substance exact. - **Less code** — "lazy senior dev" YAGNI: smallest working change, no unrequested scaffolding. - **Terse CJK (文言)** — classical-Chinese ultra-terse style (locale-gated to `zh`). -- **🎯 Adaptive context-budget** *(the dial)* — instead of one on/off token threshold, escalate the cheapest, most-lossless engines only as far as needed to **fit the model's context window**. Policy: `reserve-output` (default, model-aware) · `percentage` · `absolute`. Mode: `floor` (guarantee fit) · `replace-autotrigger` (your explicit choice wins) · `off` (legacy threshold). -- **🎛️ Where compression is decided** *(precedence, high → low)* — per-request `x-omniroute-compression` header › routing-combo override › active named profile › adaptive / auto-trigger › panel default › off. The applied plan echoes back in the `X-OmniRoute-Compression: ; source=` response header. +- **🎯 Adaptive context-budget** _(the dial)_ — instead of one on/off token threshold, escalate the cheapest, most-lossless engines only as far as needed to **fit the model's context window**. Policy: `reserve-output` (default, model-aware) · `percentage` · `absolute`. Mode: `floor` (guarantee fit) · `replace-autotrigger` (your explicit choice wins) · `off` (legacy threshold). +- **🎛️ Where compression is decided** _(precedence, high → low)_ — per-request `x-omniroute-compression` header › routing-combo override › active named profile › adaptive / auto-trigger › panel default › off. The applied plan echoes back in the `X-OmniRoute-Compression: ; source=` response header. Auto-trigger by token threshold, flip on the adaptive dial, pin a named profile, set a one-off per request, or assign a pipeline per routing combo — whichever fits the workload. An opt-in offline **eval harness** (`npm run eval:compression`) scores fidelity vs. savings on a pinned corpus before you promote a change. @@ -574,7 +575,7 @@ Dashboard at `http://localhost:20128` · API at `http://localhost:20128/v1`. **2) Connect a FREE provider (no signup)** -Dashboard → **Providers** → connect **Kiro AI** (free Claude unlimited) or **OpenCode Free** (no auth) → done. +Dashboard → **Providers** → connect **Kiro AI** (free Claude, ~50 credits/month per account) or **OpenCode Free** (no auth) → done. **3) Point your coding tool** @@ -743,7 +744,7 @@ podman compose --profile base up -d **$0 forever:** ``` -1. kr/claude-sonnet-4.5 (Kiro — unlimited) +1. kr/claude-sonnet-4.5 (Kiro — ~50 credits/mo per acct) 2. if/kimi-k2-thinking (Qoder — unlimited) 3. pol/gpt-5 (Pollinations — no key) 4. lc/longcat-flash-lite (50M tok/day backup) @@ -800,7 +801,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo | `DATA_DIR` | `~/.omniroute` | Database & config storage | **Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system. -**Are FREE providers really unlimited?** Yes — Kiro, Qoder, Pollinations, LongCat, Cloudflare. No catch. +**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0. **Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected. **Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 231 providers. @@ -934,7 +935,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo | Document | Description | | ------------------------------------------------- | --------------------------------------------------- | | [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples | -| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification | +| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification | | [MCP Server](open-sse/mcp-server/README.md) | 87 MCP tools, IDE configs, Python/TS/Go clients | | [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference | | [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt | @@ -1089,32 +1090,32 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router] ### 🗜️ Context & token compression — engines -| Project | ⭐ | How it inspired OmniRoute | -| ---------------------------------------------------------------------------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **[Caveman](https://github.com/JuliusBrussee/caveman)** · JuliusBrussee | 74.5k | The viral "why use many token when few token do trick" project — its caveman-speak philosophy powers our standard compression mode and 30+ filler/condensation rules. | -| **[RTK – Rust Token Killer](https://github.com/rtk-ai/rtk)** · rtk-ai | 63.6k | High-performance command-output compression — inspired our RTK engine, JSON filter DSL, raw-output recovery and the stacked RTK → Caveman pipeline. | -| **[headroom](https://github.com/chopratejas/headroom)** · chopratejas | 33.6k | Reversible context-compression (SmartCrusher) — inspired our `headroom` engine and the `ccr` retrieve-marker pattern. | -| **[LLMLingua](https://github.com/microsoft/LLMLingua)** · Microsoft | 6.3k | Prompt-compression research (LLMLingua / LLMLingua-2) — inspired our async, code-safe, fail-open `llmlingua` engine. | -| **[llmlingua-2-js](https://github.com/atjsh/llmlingua-2-js)** · atjsh | 27 | The JS/ONNX port (MobileBERT / XLM-RoBERTa) used as the worker-thread backend for our LLMLingua engine. | -| **[Troglodita](https://github.com/leninejunior/troglodita)** · Lenine Júnior | 15 | PT-BR token compression — powers our pt-BR language pack: pleonasm reduction and filler removal tuned for Brazilian-Portuguese grammar. | -| **[ponytail](https://github.com/DietrichGebert/ponytail)** · DietrichGebert | 51.4k | The viral "lazy senior dev" YAGNI-coder skill — inspired our **less-code** Output Style: smallest-working-change steering that cuts *generated* code (the output-axis sibling to Caveman's terse prose). | +| Project | ⭐ | How it inspired OmniRoute | +| ---------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **[Caveman](https://github.com/JuliusBrussee/caveman)** · JuliusBrussee | 74.5k | The viral "why use many token when few token do trick" project — its caveman-speak philosophy powers our standard compression mode and 30+ filler/condensation rules. | +| **[RTK – Rust Token Killer](https://github.com/rtk-ai/rtk)** · rtk-ai | 63.6k | High-performance command-output compression — inspired our RTK engine, JSON filter DSL, raw-output recovery and the stacked RTK → Caveman pipeline. | +| **[headroom](https://github.com/chopratejas/headroom)** · chopratejas | 33.6k | Reversible context-compression (SmartCrusher) — inspired our `headroom` engine and the `ccr` retrieve-marker pattern. | +| **[LLMLingua](https://github.com/microsoft/LLMLingua)** · Microsoft | 6.3k | Prompt-compression research (LLMLingua / LLMLingua-2) — inspired our async, code-safe, fail-open `llmlingua` engine. | +| **[llmlingua-2-js](https://github.com/atjsh/llmlingua-2-js)** · atjsh | 27 | The JS/ONNX port (MobileBERT / XLM-RoBERTa) used as the worker-thread backend for our LLMLingua engine. | +| **[Troglodita](https://github.com/leninejunior/troglodita)** · Lenine Júnior | 15 | PT-BR token compression — powers our pt-BR language pack: pleonasm reduction and filler removal tuned for Brazilian-Portuguese grammar. | +| **[ponytail](https://github.com/DietrichGebert/ponytail)** · DietrichGebert | 51.4k | The viral "lazy senior dev" YAGNI-coder skill — inspired our **less-code** Output Style: smallest-working-change steering that cuts _generated_ code (the output-axis sibling to Caveman's terse prose). | ### 🧩 Compact formats, token research & code-aware tooling -| Project | ⭐ | How it inspired OmniRoute | -| ---------------------------------------------------------------------------------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------ | -| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.6k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. | -| **[GCF – Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 11 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. | -| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 409 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. | -| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 993 | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. | -| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 103 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. | -| **[token-optimizer](https://github.com/alexgreensh/token-optimizer)** · alexgreensh | 1.4k | "Find the ghost tokens" — its offload + recoverable-handle pattern informed our CCR offload thinking. | -| **[TokenMizer](https://github.com/Shweta-Mishra-ai/tokenmizer)** · Shweta-Mishra-ai | 1 | A session-graph + cross-turn line-dedup blueprint that informed our session-dedup design. | -| **[OmniCompress](https://github.com/jessefreitas/OmniCompress)** · jessefreitas | 2 | Rust columnar-JSON + content-addressed retrieve + cross-message dedup — validated our `headroom`/`ccr`/`session-dedup` engine design and the cache-stable "compressed form is position-independent" invariant. | -| **[mcp-compressor](https://github.com/atlassian-labs/mcp-compressor)** · Atlassian Labs | 80 | MCP tool-schema/description compression — informed our MCP tool-manifest cardinality reduction. | -| **[RepoMapper](https://github.com/pdavis68/RepoMapper)** · pdavis68 | 182 | Aider-style repo-map ranking — informed our repo-map / retrieval-ranking exploration. | -| **[quiet-shell-mcp](https://github.com/mrsimpson/quiet-shell-mcp)** · mrsimpson | 4 | Declarative shell-output reduction over MCP — validated our declarative bash-output compaction. | -| **[ts-morph](https://github.com/dsherret/ts-morph)** · David Sherret | 6.1k | TypeScript Compiler API toolkit — inspired our parser-based comment removal that preserves string, template and regex literals. | +| Project | ⭐ | How it inspired OmniRoute | +| ---------------------------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.6k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. | +| **[GCF – Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 11 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. | +| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 409 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. | +| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 993 | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. | +| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 103 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. | +| **[token-optimizer](https://github.com/alexgreensh/token-optimizer)** · alexgreensh | 1.4k | "Find the ghost tokens" — its offload + recoverable-handle pattern informed our CCR offload thinking. | +| **[TokenMizer](https://github.com/Shweta-Mishra-ai/tokenmizer)** · Shweta-Mishra-ai | 1 | A session-graph + cross-turn line-dedup blueprint that informed our session-dedup design. | +| **[OmniCompress](https://github.com/jessefreitas/OmniCompress)** · jessefreitas | 2 | Rust columnar-JSON + content-addressed retrieve + cross-message dedup — validated our `headroom`/`ccr`/`session-dedup` engine design and the cache-stable "compressed form is position-independent" invariant. | +| **[mcp-compressor](https://github.com/atlassian-labs/mcp-compressor)** · Atlassian Labs | 80 | MCP tool-schema/description compression — informed our MCP tool-manifest cardinality reduction. | +| **[RepoMapper](https://github.com/pdavis68/RepoMapper)** · pdavis68 | 182 | Aider-style repo-map ranking — informed our repo-map / retrieval-ranking exploration. | +| **[quiet-shell-mcp](https://github.com/mrsimpson/quiet-shell-mcp)** · mrsimpson | 4 | Declarative shell-output reduction over MCP — validated our declarative bash-output compaction. | +| **[ts-morph](https://github.com/dsherret/ts-morph)** · David Sherret | 6.1k | TypeScript Compiler API toolkit — inspired our parser-based comment removal that preserves string, template and regex literals. | ### 🧠 Memory & RAG diff --git a/bin/_ops-common.sh b/bin/_ops-common.sh new file mode 100644 index 0000000000..820efccc30 --- /dev/null +++ b/bin/_ops-common.sh @@ -0,0 +1,70 @@ +# bin/_ops-common.sh — shared helpers for the OmniRoute ops runbook scripts. +# +# Sourced (not executed) by rollback.sh / snapshot-data.sh / restore-data.sh / +# restore-policies.sh / cold-start-bench.sh. The runbook context lives in +# docs/INCIDENT_RESPONSE.md and docs/PERF_BUDGETS.md. +# +# Path resolution mirrors the app (src/lib/db/core.ts): the SQLite store is +# $DATA_DIR/storage.sqlite and managed backups go to $DATA_DIR/db_backups +# (overridable via DB_BACKUPS_DIR), so snapshots created here are interchangeable +# with the ones the server writes on migrations. + +# Recompute the data-dir-derived paths. Called once on source, and again by +# scripts that accept a --data-dir override. +ops_set_data_dir() { + OMNIROUTE_DATA_DIR="$1" + OMNIROUTE_SQLITE="${OMNIROUTE_DATA_DIR}/storage.sqlite" + OMNIROUTE_BACKUPS_DIR="${DB_BACKUPS_DIR:-${OMNIROUTE_DATA_DIR}/db_backups}" +} +ops_set_data_dir "${DATA_DIR:-$HOME/.omniroute}" + +ops_log() { printf '[%s] %s\n' "${SCRIPT_NAME:-ops}" "$*" >&2; } +ops_die() { + printf '[%s] ERROR: %s\n' "${SCRIPT_NAME:-ops}" "$*" >&2 + exit 1 +} + +ops_require_cmd() { + command -v "$1" >/dev/null 2>&1 || ops_die "required command not found: $1" +} + +# ops_confirm "" — return 0 to proceed. Honors ASSUME_YES=1 (set by the +# --yes flag) and REFUSES a destructive action on a non-interactive stdin unless +# ASSUME_YES is set, so an unattended/CI invocation can never silently destroy data. +ops_confirm() { + local prompt="$1" reply + if [ "${ASSUME_YES:-0}" = "1" ]; then return 0; fi + if [ ! -t 0 ]; then + ops_die "refusing a destructive action without a TTY; pass --yes to proceed non-interactively" + fi + read -r -p "$prompt [y/N] " reply + case "$reply" in + [yY] | [yY][eE][sS]) return 0 ;; + *) return 1 ;; + esac +} + +# ops_find_snapshot — resolve a snapshot identifier (a snapshot dir name, +# a bare timestamp/sha, or an explicit path) to a directory containing +# storage.sqlite. Echoes the resolved dir or dies. +ops_find_snapshot() { + local id="$1" cand + [ -n "$id" ] || ops_die "snapshot id required (a timestamp/sha, dir name, or path)" + for cand in \ + "$id" \ + "$id/" \ + "$OMNIROUTE_BACKUPS_DIR/$id" \ + "$OMNIROUTE_BACKUPS_DIR/snapshot_$id"; do + if [ -f "${cand%/}/storage.sqlite" ]; then + printf '%s\n' "${cand%/}" + return 0 + fi + done + # Fall back to a prefix match against snapshot_* dirs (e.g. a short sha/date). + if [ -d "$OMNIROUTE_BACKUPS_DIR" ]; then + for cand in "$OMNIROUTE_BACKUPS_DIR"/snapshot_*"$id"*; do + [ -f "$cand/storage.sqlite" ] && { printf '%s\n' "$cand"; return 0; } + done + fi + ops_die "no snapshot matching '$id' under $OMNIROUTE_BACKUPS_DIR (run bin/snapshot-data.sh first)" +} diff --git a/bin/cli/runtime/nativeDeps.mjs b/bin/cli/runtime/nativeDeps.mjs index f74ab1eea9..852cff76b7 100644 --- a/bin/cli/runtime/nativeDeps.mjs +++ b/bin/cli/runtime/nativeDeps.mjs @@ -12,7 +12,7 @@ import { spawnSync } from "node:child_process"; import { platform } from "node:os"; import { resolveDataDir } from "../data-dir.mjs"; -const BETTER_SQLITE3_VERSION = "12.9.0"; +const BETTER_SQLITE3_VERSION = "12.10.1"; function runtimeDir() { return join(resolveDataDir(), "runtime"); diff --git a/bin/cli/runtime/sqliteRuntime.mjs b/bin/cli/runtime/sqliteRuntime.mjs index 19dd790764..506481a353 100644 --- a/bin/cli/runtime/sqliteRuntime.mjs +++ b/bin/cli/runtime/sqliteRuntime.mjs @@ -6,7 +6,7 @@ import { pathToFileURL } from "node:url"; import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs"; const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime"); -const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.6.2"; +const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.10.1"; let resolvedCached = null; diff --git a/bin/cli/runtime/trayRuntime.ts b/bin/cli/runtime/trayRuntime.ts index c1d76a96fd..cc734b6f80 100644 --- a/bin/cli/runtime/trayRuntime.ts +++ b/bin/cli/runtime/trayRuntime.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { execSync } from "node:child_process"; @@ -6,7 +6,43 @@ import { execSync } from "node:child_process"; const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime"); // systray2 is a maintained fork with prebuilt binaries — installed lazily at runtime, // not in dependencies, to avoid npm install overhead for users who don't use --tray. -const SYSTRAY_VERSION = "systray2@1.4.5"; +// +// Pin: stay on the 2.x line. The original `systray@1.0.5` package bundles a 2017 +// x86_64 Go binary whose Mach-O headers modern dyld (macOS 14+) rejects, so the +// tray silently fails to register on Apple Silicon. systray2 2.x ships newer +// getlantern/systray-portable binaries that work under Rosetta. Inherited from +// upstream decolua/9router#1080. +export const SYSTRAY_PACKAGE = "systray2"; +export const SYSTRAY_VERSION = "2.1.4"; +const SYSTRAY_SPEC = `${SYSTRAY_PACKAGE}@${SYSTRAY_VERSION}`; + +export function resolveSystrayBinName(platform: NodeJS.Platform): string | null { + if (platform === "win32") return null; + if (platform === "darwin") return "tray_darwin_release"; + return "tray_linux_release"; +} + +export interface ChmodResult { + changed: boolean; + reason?: "win32-skip" | "missing" | "chmod-failed"; +} + +// systray2's npm tarball sometimes ships the bundled Go binary without the +// executable bit set on macOS/Linux, causing spawn() to fail with EACCES. +// Set +x best-effort so the tray actually starts. Inherited from +// upstream decolua/9router#1080. +export function chmodSystrayBinAt(runtimeRoot: string, platform: NodeJS.Platform): ChmodResult { + const binName = resolveSystrayBinName(platform); + if (!binName) return { changed: false, reason: "win32-skip" }; + const binPath = join(runtimeRoot, "node_modules", SYSTRAY_PACKAGE, "traybin", binName); + if (!existsSync(binPath)) return { changed: false, reason: "missing" }; + try { + chmodSync(binPath, 0o755); + return { changed: true }; + } catch { + return { changed: false, reason: "chmod-failed" }; + } +} export async function loadSystray(): Promise<(new (...args: unknown[]) => unknown) | null> { if (process.platform === "win32") return null; // Windows uses tray.ps1 instead @@ -15,15 +51,21 @@ export async function loadSystray(): Promise<(new (...args: unknown[]) => unknow try { installSystray(); } catch (err) { + // Surface failures to stderr instead of staying silent — anyone hitting + // a tray problem otherwise has zero diagnostic. (PR #1080) console.warn(`[omniroute] tray runtime install failed: ${(err as Error).message}`); return null; } } + // Best-effort: ensure the bundled Go binary is executable. Some npm tarballs + // drop the +x bit on extraction (observed on macOS). + chmodSystrayBinAt(RUNTIME_DIR, process.platform); try { - const modPath = join(RUNTIME_DIR, "node_modules", "systray2"); + const modPath = join(RUNTIME_DIR, "node_modules", SYSTRAY_PACKAGE); const mod = await import(modPath); return (mod.default ?? mod.SysTray ?? mod) as (new (...args: unknown[]) => unknown) | null; - } catch { + } catch (err) { + console.warn(`[omniroute] tray runtime import failed: ${(err as Error).message}`); return null; } } @@ -37,12 +79,12 @@ function ensureRuntimeDir(): void { } function isInstalled(): boolean { - return existsSync(join(RUNTIME_DIR, "node_modules", "systray2", "package.json")); + return existsSync(join(RUNTIME_DIR, "node_modules", SYSTRAY_PACKAGE, "package.json")); } function installSystray(): void { execSync( - `npm install --prefix "${RUNTIME_DIR}" ${SYSTRAY_VERSION} --no-audit --no-fund --silent`, + `npm install --prefix "${RUNTIME_DIR}" ${SYSTRAY_SPEC} --no-audit --no-fund --silent`, { stdio: ["ignore", "ignore", "pipe"], timeout: 120_000 } ); } diff --git a/bin/cli/tray/tray.ts b/bin/cli/tray/tray.ts index f9e3c642e1..502467bc92 100644 --- a/bin/cli/tray/tray.ts +++ b/bin/cli/tray/tray.ts @@ -124,6 +124,10 @@ async function initUnixTray(options: TrayOptions): Promise const systray = new SysTray({ menu: { icon: getIconBase64(), + // isTemplateIcon: false on darwin — the bundled icon.png is a full-color + // RGBA logo; template mode would render it as a solid white square + // because macOS template icons only use the alpha channel. (PR #1080) + isTemplateIcon: false, title: "OmniRoute", tooltip: `OmniRoute :${options.port}`, items: menuItems.map((it) => ({ @@ -175,6 +179,8 @@ async function initUnixTray(options: TrayOptions): Promise setTooltip: () => { /* systray2 does not support runtime tooltip change */ }, - destroy: () => systray.kill(), + // Pass false so systray2's kill does NOT call process.exit(0) before the + // rest of cleanup (server SIGKILL, MITM/tunnel cleanup) runs. (PR #1080) + destroy: () => systray.kill(false), }; } diff --git a/bin/cli/tray/traySystray.mjs b/bin/cli/tray/traySystray.mjs index bf0b7caee0..4c5bcb3c5e 100644 --- a/bin/cli/tray/traySystray.mjs +++ b/bin/cli/tray/traySystray.mjs @@ -100,8 +100,32 @@ export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) { return tray; } +/** + * Resolve the Go systray2 child subprocess PID from a tray instance. + * systray2 exposes the spawned binary either as the `_process` field or via a + * `process()` accessor depending on version. Returns the numeric PID or null. + */ +export function getSystrayChildPid(tray) { + if (!tray) return null; + try { + const proc = tray._process || (typeof tray.process === "function" ? tray.process() : null); + if (proc && typeof proc.pid === "number") return proc.pid; + } catch {} + return null; +} + export function killSystrayUnix(tray) { try { + // systray2.kill(false) closes the IPC channel but leaves the Go tray binary + // subprocess running, which keeps an orphan NSStatusItem on macOS and blocks + // a freshly spawned tray (e.g. on respawn / hide-to-tray) from registering. + // SIGKILL the child PID directly first, then close IPC. + const pid = getSystrayChildPid(tray); + if (pid) { + try { + process.kill(pid, "SIGKILL"); + } catch {} + } tray.kill(false); } catch {} } diff --git a/bin/cold-start-bench.sh b/bin/cold-start-bench.sh new file mode 100755 index 0000000000..f879610712 --- /dev/null +++ b/bin/cold-start-bench.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# bin/cold-start-bench.sh — measure OmniRoute cold-start against the budgets in +# docs/PERF_BUDGETS.md §5 (container start → HTTP listening ≤ 800 ms; first warm +# TTFB ≤ 200 ms). Boots the server on a throwaway port, times until +# /api/health/ping answers 200, measures a warm request, and reports PASS/FAIL. +set -euo pipefail +SCRIPT_NAME="cold-start-bench" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_ops-common.sh" + +usage() { + cat <<'EOF' +Usage: bin/cold-start-bench.sh [--port ] [--start-cmd ""] [--url ] + [--listen-budget-ms ] [--ttfb-budget-ms ] [-h|--help] + +Boots OmniRoute, times cold-start to the first /api/health/ping 200, measures +warm TTFB, and compares against the PERF_BUDGETS.md §5 budgets +(listen ≤ 800 ms, TTFB ≤ 200 ms). Exits non-zero if a budget is exceeded. + +--url benches an already-running server instead of booting one (skips the boot +timing; only TTFB is measured). +EOF +} + +PORT="${PORT:-21987}" +START_CMD="" +BASE_URL="" +LISTEN_BUDGET_MS=800 +TTFB_BUDGET_MS=200 + +while [ $# -gt 0 ]; do + case "$1" in + --port) PORT="${2:?--port needs a value}"; shift 2 ;; + --start-cmd) START_CMD="${2:?--start-cmd needs a value}"; shift 2 ;; + --url) BASE_URL="${2:?--url needs a value}"; shift 2 ;; + --listen-budget-ms) LISTEN_BUDGET_MS="${2:?}"; shift 2 ;; + --ttfb-budget-ms) TTFB_BUDGET_MS="${2:?}"; shift 2 ;; + -h | --help) usage; exit 0 ;; + *) ops_die "unknown argument: $1 (see --help)" ;; + esac +done + +ops_require_cmd curl + +now_ms() { date +%s%3N; } # ms since epoch (GNU date / Linux) +ping_ok() { curl -fsS -o /dev/null --max-time 2 "$1/api/health/ping" 2>/dev/null; } + +SERVER_PID="" +cleanup() { [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true; } +trap cleanup EXIT + +if [ -n "$BASE_URL" ]; then + ops_log "benching already-running server at $BASE_URL (boot timing skipped)" + listen_ms="" +else + BASE_URL="http://127.0.0.1:$PORT" + [ -n "$START_CMD" ] || START_CMD="npm start -- --port $PORT" + ops_log "booting: $START_CMD" + start_ms="$(now_ms)" + # shellcheck disable=SC2086 + PORT="$PORT" $START_CMD >/tmp/omniroute-coldstart.log 2>&1 & + SERVER_PID="$!" + deadline=$(($(now_ms) + 30000)) + until ping_ok "$BASE_URL"; do + kill -0 "$SERVER_PID" 2>/dev/null || ops_die "server process exited during boot (see /tmp/omniroute-coldstart.log)" + [ "$(now_ms)" -gt "$deadline" ] && ops_die "server did not answer /api/health/ping within 30s" + sleep 0.05 + done + listen_ms=$(($(now_ms) - start_ms)) +fi + +ping_ok "$BASE_URL" || ops_die "server at $BASE_URL is not answering /api/health/ping" +ttfb_ms="$(curl -fsS -o /dev/null -w '%{time_starttransfer}' "$BASE_URL/api/health/ping" | awk '{printf "%d", $1 * 1000}')" + +fail=0 +if [ -n "$listen_ms" ]; then + echo "cold-start (start → listening): ${listen_ms} ms (budget ${LISTEN_BUDGET_MS} ms)" + [ "$listen_ms" -le "$LISTEN_BUDGET_MS" ] || { echo "FAIL: listen budget exceeded"; fail=1; } +fi +echo "warm TTFB: ${ttfb_ms} ms (budget ${TTFB_BUDGET_MS} ms)" +[ "$ttfb_ms" -le "$TTFB_BUDGET_MS" ] || { echo "FAIL: TTFB budget exceeded"; fail=1; } +[ "$fail" -eq 0 ] && echo "PASS: within cold-start budgets" +exit "$fail" diff --git a/bin/restore-data.sh b/bin/restore-data.sh new file mode 100755 index 0000000000..df907fb732 --- /dev/null +++ b/bin/restore-data.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# bin/restore-data.sh — restore the OmniRoute SQLite data volume from a snapshot +# created by bin/snapshot-data.sh. Used by the data-layer incident runbook +# (docs/INCIDENT_RESPONSE.md §4.4) after stopping writers. +# +# Safety: takes a pre-restore snapshot of the CURRENT data, refuses to run +# unattended without --yes, and verifies the snapshot before overwriting. +set -euo pipefail +SCRIPT_NAME="restore-data" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_ops-common.sh" + +usage() { + cat <<'EOF' +Usage: bin/restore-data.sh [--data-dir ] [--yes|-y] [-h|--help] + +Restores storage.sqlite (and any sibling *.sqlite) from a snapshot. The current +data is first copied to $DB_BACKUPS_DIR/pre-restore_ as a safety net. + is a timestamp/sha, a snapshot dir name, or a path (see snapshot-data.sh). +Stop OmniRoute before running, and restart it afterwards. +EOF +} + +ID="" +while [ $# -gt 0 ]; do + case "$1" in + --yes | -y) ASSUME_YES=1; shift ;; + --data-dir) ops_set_data_dir "${2:?--data-dir needs a value}"; shift 2 ;; + -h | --help) usage; exit 0 ;; + -*) ops_die "unknown argument: $1 (see --help)" ;; + *) ID="$1"; shift ;; + esac +done + +[ -n "$ID" ] || ops_die "snapshot id required (see --help)" +snap="$(ops_find_snapshot "$ID")" +ops_log "restore source: $snap → $OMNIROUTE_DATA_DIR" +ops_confirm "Overwrite storage.sqlite at $OMNIROUTE_DATA_DIR from $snap?" || ops_die "aborted" + +# Pre-restore safety copy of the live data. +if [ -f "$OMNIROUTE_SQLITE" ]; then + safety="$OMNIROUTE_BACKUPS_DIR/pre-restore_$(date -u +%Y%m%dT%H%M%SZ)" + mkdir -p "$safety" + if command -v sqlite3 >/dev/null 2>&1; then + sqlite3 "$OMNIROUTE_SQLITE" "VACUUM INTO '$safety/storage.sqlite'" \ + || cp -a "$OMNIROUTE_SQLITE" "$safety/storage.sqlite" + else + cp -a "$OMNIROUTE_SQLITE" "$safety/storage.sqlite" + fi + ops_log "current data saved to $safety" +fi + +mkdir -p "$OMNIROUTE_DATA_DIR" +# Drop stale WAL/SHM so the restored DB is authoritative, then copy in. +rm -f "$OMNIROUTE_SQLITE" "${OMNIROUTE_SQLITE}-wal" "${OMNIROUTE_SQLITE}-shm" +cp -a "$snap/storage.sqlite" "$OMNIROUTE_SQLITE" + +# Restore sibling DBs captured in the snapshot. +for f in "$snap"/*.sqlite; do + [ -e "$f" ] || continue + [ "$(basename "$f")" = "storage.sqlite" ] && continue + cp -a "$f" "$OMNIROUTE_DATA_DIR/" +done + +ops_log "restore complete — restart OmniRoute to pick up the restored data" diff --git a/bin/restore-policies.sh b/bin/restore-policies.sh new file mode 100755 index 0000000000..9cf4da03ef --- /dev/null +++ b/bin/restore-policies.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# bin/restore-policies.sh — restore ONLY the API-key policy tables from a +# snapshot, leaving request/audit/runtime state intact. Used by the auth-layer +# incident runbook (docs/INCIDENT_RESPONSE.md §4.3) when policies_active is empty +# but the rest of the database is healthy (so a full restore-data is overkill). +# +# "Policy" tables = api_key* definition tables (the key + its limits/allowed +# quotas/context sources), EXCLUDING usage counters and reset logs so a restore +# never rewinds live usage accounting. +set -euo pipefail +SCRIPT_NAME="restore-policies" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_ops-common.sh" + +usage() { + cat <<'EOF' +Usage: bin/restore-policies.sh [--data-dir ] [--yes|-y] [-h|--help] + +Replaces the API-key policy tables (api_key*, excluding *counter* / *_log*) in +the live DB with the copies from a snapshot, inside one transaction. The live DB +is snapshotted to $DB_BACKUPS_DIR/pre-policy-restore_ first. Requires sqlite3. +EOF +} + +ID="" +while [ $# -gt 0 ]; do + case "$1" in + --yes | -y) ASSUME_YES=1; shift ;; + --data-dir) ops_set_data_dir "${2:?--data-dir needs a value}"; shift 2 ;; + -h | --help) usage; exit 0 ;; + -*) ops_die "unknown argument: $1 (see --help)" ;; + *) ID="$1"; shift ;; + esac +done + +[ -n "$ID" ] || ops_die "snapshot id required (see --help)" +ops_require_cmd sqlite3 +snap="$(ops_find_snapshot "$ID")" +[ -f "$OMNIROUTE_SQLITE" ] || ops_die "no live DB at $OMNIROUTE_SQLITE (use restore-data.sh for a full restore)" + +# Policy definition tables present in BOTH the snapshot and the live DB. GLOB +# keeps `_` literal; we drop usage counters / logs so accounting isn't rewound. +readarray -t tables < <( + sqlite3 "$snap/storage.sqlite" \ + "SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'api_key*' \ + AND name NOT GLOB '*counter*' AND name NOT GLOB '*_log*' ORDER BY name;" +) +[ "${#tables[@]}" -gt 0 ] || ops_die "snapshot has no api_key* policy tables" + +ops_log "policy tables to restore: ${tables[*]}" +ops_confirm "Replace ${#tables[@]} policy table(s) in $OMNIROUTE_SQLITE from $snap?" || ops_die "aborted" + +# Safety snapshot of the live DB before mutating it. +safety="$OMNIROUTE_BACKUPS_DIR/pre-policy-restore_$(date -u +%Y%m%dT%H%M%SZ)" +mkdir -p "$safety" +sqlite3 "$OMNIROUTE_SQLITE" "VACUUM INTO '$safety/storage.sqlite'" +ops_log "live DB saved to $safety" + +# Replace each policy table inside a single transaction, attaching the snapshot +# (only tables that also exist in the live DB are touched). +sql="ATTACH DATABASE '$snap/storage.sqlite' AS snap; +PRAGMA foreign_keys=OFF; +BEGIN;" +for t in "${tables[@]}"; do + if [ -n "$(sqlite3 "$OMNIROUTE_SQLITE" "SELECT 1 FROM sqlite_master WHERE type='table' AND name='$t' LIMIT 1;")" ]; then + sql="$sql +DELETE FROM main.\"$t\"; +INSERT INTO main.\"$t\" SELECT * FROM snap.\"$t\";" + else + ops_log "skipping '$t' — not present in live DB" + fi +done +sql="$sql +COMMIT; +DETACH DATABASE snap;" + +printf '%s\n' "$sql" | sqlite3 "$OMNIROUTE_SQLITE" +ops_log "policies restored from $snap — restart OmniRoute to apply" diff --git a/bin/rollback.sh b/bin/rollback.sh new file mode 100755 index 0000000000..41d7bfcd43 --- /dev/null +++ b/bin/rollback.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# bin/rollback.sh — roll OmniRoute back to a previous release to mitigate a bad +# deploy. Used by the incident runbook (docs/INCIDENT_RESPONSE.md §3 / §4). +# +# Methods (auto-detected; override with --method): +# • npm — `npm install -g omniroute@` and, if PM2 manages it, +# `pm2 restart omniroute`. This is how the VPS deploy runs. +# • docker — re-tag the local image omniroute: to omniroute:prod and +# recreate the prod service from docker-compose.prod.yml. (That +# compose builds the `prod` tag locally rather than pulling a +# registry tag, so the versioned image must already exist locally.) +# With no , targets the highest published release strictly below the +# current package.json version. +set -euo pipefail +SCRIPT_NAME="rollback" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_ops-common.sh" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: bin/rollback.sh [] [--method npm|docker] [--yes|-y] [-h|--help] + +Rolls OmniRoute back to (e.g. 3.8.35 or v3.8.35). With no version, +picks the highest published release below the current package.json version. +Auto-detects npm vs docker deployment; override with --method. +EOF +} + +VERSION="" +METHOD="" +while [ $# -gt 0 ]; do + case "$1" in + --yes | -y) ASSUME_YES=1; shift ;; + --method) METHOD="${2:?--method needs npm|docker}"; shift 2 ;; + -h | --help) usage; exit 0 ;; + -*) ops_die "unknown argument: $1 (see --help)" ;; + *) VERSION="${1#v}"; shift ;; + esac +done + +if [ -z "$METHOD" ]; then + if command -v docker >/dev/null 2>&1 && [ -f "$REPO_ROOT/docker-compose.prod.yml" ] \ + && docker compose -f "$REPO_ROOT/docker-compose.prod.yml" ps -q 2>/dev/null | grep -q .; then + METHOD="docker" + elif command -v npm >/dev/null 2>&1; then + METHOD="npm" + else + ops_die "no deploy method detected (no running prod compose, no npm) — pass --method npm|docker" + fi +fi + +# Resolve the previous published version when none was given. +if [ -z "$VERSION" ]; then + ops_require_cmd npm + ops_require_cmd node + current="$(node -p "require('$REPO_ROOT/package.json').version" 2>/dev/null || true)" + [ -n "$current" ] || ops_die "cannot read current version from package.json — pass " + VERSION="$(npm view omniroute versions --json 2>/dev/null | node -e ' + let s = ""; + process.stdin.on("data", (d) => (s += d)).on("end", () => { + let vs; + try { vs = JSON.parse(s); } catch { vs = []; } + if (!Array.isArray(vs)) vs = [vs]; + const ok = (v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v); + const cmp = (a, b) => { + const x = a.split(".").map(Number), y = b.split(".").map(Number); + return x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; + }; + const cur = process.argv[1]; + const prev = vs.filter(ok).filter((v) => cmp(v, cur) < 0).sort(cmp).pop() || ""; + process.stdout.write(prev); + }); + ' "$current")" + [ -n "$VERSION" ] || ops_die "could not resolve the previous published version — pass explicitly" +fi + +ops_log "target: omniroute@$VERSION via $METHOD" +ops_confirm "Roll OmniRoute back to $VERSION via $METHOD?" || ops_die "aborted" + +case "$METHOD" in + npm) + ops_require_cmd npm + npm install -g "omniroute@$VERSION" + if command -v pm2 >/dev/null 2>&1 && pm2 jlist 2>/dev/null | grep -q '"name":"omniroute"'; then + pm2 restart omniroute --update-env + ops_log "pm2 restarted omniroute" + else + ops_log "installed omniroute@$VERSION — restart the service to apply (no PM2 'omniroute' process found)" + fi + ;; + docker) + ops_require_cmd docker + if ! docker image inspect "omniroute:$VERSION" >/dev/null 2>&1; then + ops_die "local image omniroute:$VERSION not found — build it from the $VERSION checkout first (this compose builds the 'prod' tag, it does not pull a registry tag)" + fi + docker tag "omniroute:$VERSION" omniroute:prod + docker compose -f "$REPO_ROOT/docker-compose.prod.yml" up -d --no-build + ops_log "recreated prod service from omniroute:$VERSION" + ;; + *) ops_die "unknown method: $METHOD (use npm or docker)" ;; +esac +ops_log "rollback to $VERSION complete" diff --git a/bin/snapshot-data.sh b/bin/snapshot-data.sh new file mode 100755 index 0000000000..f312a46b9a --- /dev/null +++ b/bin/snapshot-data.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# bin/snapshot-data.sh — consistent point-in-time snapshot of the OmniRoute data +# volume (the SQLite store under $DATA_DIR). Used by the data-layer incident +# runbook (docs/INCIDENT_RESPONSE.md §4.4) before any restore. +# +# Output: a directory $DB_BACKUPS_DIR/snapshot_[_