mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
Compare commits
2 Commits
docs/audit
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a53a53277 | ||
|
|
4b914ad297 |
17
AGENTS.md
17
AGENTS.md
@@ -118,18 +118,11 @@ upstream/service level, so one unhealthy provider does not slow down every reque
|
||||
- `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
|
||||
breaker, failure opens it again.
|
||||
|
||||
**Defaults** (`open-sse/config/constants.ts` → `PROVIDER_PROFILES`). Two thresholds live side by
|
||||
side — do not confuse them:
|
||||
**Defaults** (`open-sse/config/constants.ts`):
|
||||
|
||||
| Profile | `providerFailureThreshold` (whole provider) | `providerCooldownMs` | `circuitBreakerThreshold` (one connection) | `circuitBreakerReset` |
|
||||
| ------- | ------------------------------------------: | -------------------: | -----------------------------------------: | --------------------: |
|
||||
| OAuth | `10` | `5min` | `8` | `60s` |
|
||||
| API key | `15` | `10min` | `12` | `30s` |
|
||||
| Local | `2` | `1min` | `2` | `15s` |
|
||||
|
||||
The provider-level thresholds were scaled up for deployments with 500+ connections (OAuth was
|
||||
`3`, API key was `5`); every default is overridable through the `OMNIROUTE_PROVIDER_BREAKER_*`
|
||||
and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars.
|
||||
- OAuth providers: threshold `3`, reset timeout `60s`.
|
||||
- API-key providers: threshold `5`, reset timeout `30s`.
|
||||
- Local providers: threshold `2`, reset timeout `15s`.
|
||||
|
||||
Only provider-level failure statuses should trip the provider breaker:
|
||||
|
||||
@@ -686,7 +679,7 @@ the stale-enforcement added in Fase 6A.3.
|
||||
causa-raiz de DOIS wipes (2026-08-08 e 2026-08-10: `git reset --hard` materializou o
|
||||
symlink rastreado por cima do diretório real e o git apagou todo o conteúdo ignorado sem
|
||||
aviso); (c) após qualquer escrita relevante, `git -C _tasks add -A && git -C _tasks commit
|
||||
&& git -C _tasks push` — o push frequente é o backup real; (d) repetir esta proibição
|
||||
&& git -C _tasks push` — o push frequente é o backup real; (d) repetir esta proibição
|
||||
VERBATIM no prompt de todo subagente que toque git; (e) se `_tasks` aparecer como symlink
|
||||
quebrado, NÃO commitar nada — restaurar do remote e avisar o operador. O gate
|
||||
`check:tracked-artifacts` (pre-commit + CI) bloqueia `_tasks` rastreado em qualquer forma.
|
||||
|
||||
772
CHANGELOG.md
772
CHANGELOG.md
@@ -9,7 +9,6 @@
|
||||
_Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(core):** add Layer A capability filter at router (#5696)
|
||||
- **feat(providers):** add DeepAI as paid API-key image provider ([#6671](https://github.com/diegosouzapw/OmniRoute/issues/6671))
|
||||
- **feat(providers):** add Naga.ac and ChatAnywhere aggregator gateway providers (#6674 — thanks @chirag127)
|
||||
@@ -32,7 +31,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **Providers**: expands the Novita AI catalog from a single Llama 3.1 8B entry to 19 curated serving models (DeepSeek V4, Kimi K3, GLM 5.2, MiniMax M3, Qwen3.7 Max, Qwen3 Coder 480B, MiMo V2.5 Pro, gpt-oss-120b, Gemma 4 31B and more), each carrying its real context window, output cap and reasoning flag from the live `/openai/v1/models` listing, and each vision flag confirmed by an actual image request rather than the listing's self-reported modalities ([#8913](https://github.com/diegosouzapw/OmniRoute/pull/8913)) — thanks @jax-novita
|
||||
- **feat(providers):** native xAI Agent Tools passthrough on `/v1/responses` for `xai` / `xai-oauth` (`xao`) — forward `web_search` + `x_search` to `api.x.ai` instead of rewriting or rejecting them ([#8964](https://github.com/diegosouzapw/OmniRoute/issues/8964))
|
||||
- **feat(providers): add UnoRouter provider** — UnoRouter is an OpenAI-compatible routing gateway supporting hundreds of models. It is now registered as an API-key provider. ([#8978](https://github.com/diegosouzapw/OmniRoute/issues/8978))
|
||||
- **feat(sse):** deprecated the legacy `gemini-cli` **upstream provider**. It was not routable (no registry entry, no executor), yet the scheduler kept refreshing its token against Google — maintaining a credential that could never serve a request. A stored connection now becomes terminal with a legible reason and a working migration path: re-add the account under `gemini`, which uses the same Google OAuth client. The `gemini-cli` **client identity** (requests arriving _from_ the Gemini CLI, issue #7034) is untouched ([#8980](https://github.com/diegosouzapw/OmniRoute/pull/8980))
|
||||
- **feat(sse):** deprecated the legacy `gemini-cli` **upstream provider**. It was not routable (no registry entry, no executor), yet the scheduler kept refreshing its token against Google — maintaining a credential that could never serve a request. A stored connection now becomes terminal with a legible reason and a working migration path: re-add the account under `gemini`, which uses the same Google OAuth client. The `gemini-cli` **client identity** (requests arriving *from* the Gemini CLI, issue #7034) is untouched ([#8980](https://github.com/diegosouzapw/OmniRoute/pull/8980))
|
||||
- Add a default-off connection setting for Codex, OpenAI, and OpenAI-compatible Responses API providers that preserves client-supplied `reasoning.encrypted_content` items for replay, including per-target combo routing.
|
||||
- Omit opaque encrypted reasoning values from persisted call logs while retaining compact diagnostic markers. (#9000)
|
||||
- **feat(providers):** add Regolo AI OpenAI-compatible provider ([#9031](https://github.com/diegosouzapw/OmniRoute/issues/9031))
|
||||
@@ -45,7 +44,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- feat(images): execute full combo strategy + fallback in /v1/images/generations (#9239)
|
||||
|
||||
Adds open-sse/services/imageCombo.ts that expands combo targets, filters to images-capable, executes the priority strategy with handleImageGeneration per target, and returns the first success or last failure. Route patches detect combo names before model resolution and divert to the new execution path.
|
||||
|
||||
- feat: make forwarded upstream response-header budget configurable via env var (#9243)
|
||||
- **feat(providers):** filter provider detail connections server-side while preserving full-page search and pagination. (thanks @RobertsXML) ([#9247](https://github.com/diegosouzapw/OmniRoute/pull/9247))
|
||||
- **feat(providers):** make video_url passthrough configurable per provider/model via compat override ([#9248](https://github.com/diegosouzapw/OmniRoute/issues/9248)) — thanks @HellFiveOsborn
|
||||
@@ -63,7 +61,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- feat(opencode-plugin): warm catalog startup from disk snapshot + parallel refresh (#9490)
|
||||
|
||||
The config-shim hook now reads the last disk snapshot before fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via Promise.allSettled instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The features.diskCache: false opt-out disables the warm read entirely.
|
||||
|
||||
- **feat(models):** Test All's "Auto-hide failed models" no longer hides quota errors — daily-quota-exhausted and credits-exhausted responses are now classified via the routing path's existing quota detectors, so an evening Test All on a free-tier provider no longer silently wipes the catalog. Quota results stay visible with a distinct amber badge ([#9511](https://github.com/diegosouzapw/OmniRoute/issues/9511))
|
||||
- Add an advisory forgotten-sibling-tests report to pull-request quality checks. The report traces changed modules through their static consumers to candidate sibling tests, while keeping barrel and dynamic-import cases non-blocking and requiring reviewed, referenced exceptions. (#9530)
|
||||
- feat(providers): add Muse Code CLI provider preset (#9544)
|
||||
@@ -79,7 +76,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
`model`, `provider`, `errorCode`.
|
||||
|
||||
Non-breaking — existing `onResponse` hooks with `{ streamed: true }` remain unchanged.
|
||||
|
||||
- **feat(audio):** Soniox STT + TTS provider (`sx`) — async speech-to-text (`stt-async-v5`, `stt-async-v4`) and real-time text-to-speech (`tts-rt-v1`) ([#9579](https://github.com/diegosouzapw/OmniRoute/pull/9579))
|
||||
- Show cache-read and cache-write token counts in request log rows and details when providers
|
||||
report them. (#9620)
|
||||
@@ -152,22 +148,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **feat(admission) — direct pushes:** adaptive overload/pressure controls with shared admission wired across the LLM routes, plus mutation-test registration for the capability-filter suite
|
||||
- **feat(agentrouter) — direct pushes:** support Claude and Codex protocols — infer the protocol from the client endpoint and honor the alternate protocol through the chat pipeline
|
||||
- **feat(providers) — direct pushes:** ChatGPT Web session credential guide with a Cookie Editor fast-path (canonical chromewebstore install link) and web-session fast-path test coverage
|
||||
- **feat(sse):** honor provider-rule lock scope for agentrouter (connection vs model) ([#10419](https://github.com/diegosouzapw/OmniRoute/pull/10419))
|
||||
- **feat(ocr):** Vertex AI DeepSeek-OCR provider ([#10398](https://github.com/diegosouzapw/OmniRoute/pull/10398))
|
||||
- **feat(providers):** derive imageToText from the OCR registry + chutes dots.ocr seed ([#10400](https://github.com/diegosouzapw/OmniRoute/pull/10400))
|
||||
- **feat(ocr):** multi-provider /v1/ocr with transformation layer (Azure Document Intelligence) ([#10283](https://github.com/diegosouzapw/OmniRoute/pull/10283))
|
||||
- **feat(providers):** declare imageToText serviceKind on major vision providers ([#10275](https://github.com/diegosouzapw/OmniRoute/pull/10275))
|
||||
- **feat(bridge):** native-vision skip guard + configurable describe output cap ([#10289](https://github.com/diegosouzapw/OmniRoute/pull/10289))
|
||||
- **feat(bridge):** normalize images to 2048px long edge before vision describe self-call ([#10287](https://github.com/diegosouzapw/OmniRoute/pull/10287))
|
||||
- **feat(sse):** restate agentrouter quota 403/400 as retryable 429 with provider-scoped error rules ([#10335](https://github.com/diegosouzapw/OmniRoute/pull/10335))
|
||||
- **feat(sse):** add i-have-adhd output style to compression catalog ([#10271](https://github.com/diegosouzapw/OmniRoute/pull/10271))
|
||||
- **feat(codex):** add OAuth fingerprint convergence modes ([#10243](https://github.com/diegosouzapw/OmniRoute/pull/10243)) — thanks @xz-dev
|
||||
- **feat(i18n):** complete Portuguese (PT-PT) translation ([#10250](https://github.com/diegosouzapw/OmniRoute/pull/10250)) — thanks @DarkEsteves
|
||||
- **feat(providers):** publish Poolside's probed Laguna Preview catalog ([#10216](https://github.com/diegosouzapw/OmniRoute/pull/10216)) — thanks @pacocartones
|
||||
- **feat(crof):** advertise reasoning effort tiers incl. max from live discovery and registry ([#10062](https://github.com/diegosouzapw/OmniRoute/pull/10062)) — thanks @excessivechaos
|
||||
- **feat(open-sse):** expose provider-level circuit breaker thresholds via env vars (#10040) ([#10046](https://github.com/diegosouzapw/OmniRoute/pull/10046)) — thanks @tiangao88
|
||||
- **feat(dashboard):** Kimi 15% first-top-up campaign — dedicated tracked link + discount-first banner copy ([#10240](https://github.com/diegosouzapw/OmniRoute/pull/10240))
|
||||
- **feat(providers):** integrate audited free-tier gateways ([#9210](https://github.com/diegosouzapw/OmniRoute/pull/9210))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -573,63 +553,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **fix(i18n) — direct pushes:** restore/unescape HTML entities in UI strings, translate capability-filter messages, complete web-session guide translations and Vietnamese parity
|
||||
- **fix(providers) — direct pushes:** repair the DeepAI registry import + executor
|
||||
- **fix(deps) — direct pushes:** CVE-driven bumps (nanoid, dompurify, mermaid, js-yaml + transitive deps for 26 Dependabot alerts) and retained isolated-build runtime dependencies in the pack
|
||||
- **fix(ci):** pin Build (advisory) to a hosted runner with memory provisioning ([#10408](https://github.com/diegosouzapw/OmniRoute/pull/10408))
|
||||
- **fix(providers):** refresh the translate-path golden for the bailian Token Plan endpoint ([#10410](https://github.com/diegosouzapw/OmniRoute/pull/10410))
|
||||
- **fix(sse):** surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight ([#10290](https://github.com/diegosouzapw/OmniRoute/pull/10290))
|
||||
- **fix(deps):** pin next to an exact version so a fresh upstream release cannot break installs ([#10340](https://github.com/diegosouzapw/OmniRoute/pull/10340))
|
||||
- **fix(types):** restore custom model output limit contract ([#10339](https://github.com/diegosouzapw/OmniRoute/pull/10339)) — thanks @backryun
|
||||
- **fix(sse):** stop the executor-contract guard from hot-looping the router ([#10373](https://github.com/diegosouzapw/OmniRoute/pull/10373))
|
||||
- **fix(types):** validate nonstreaming JSON contracts ([#10258](https://github.com/diegosouzapw/OmniRoute/pull/10258)) — thanks @backryun
|
||||
- **fix(types):** narrow refresh token rotation inputs ([#10257](https://github.com/diegosouzapw/OmniRoute/pull/10257)) — thanks @backryun
|
||||
- **fix(types):** normalize executor result contracts ([#10256](https://github.com/diegosouzapw/OmniRoute/pull/10256)) — thanks @backryun
|
||||
- **fix(types):** align Responses stream options ([#10255](https://github.com/diegosouzapw/OmniRoute/pull/10255)) — thanks @backryun
|
||||
- **fix(types):** narrow combo credential preflight ([#10254](https://github.com/diegosouzapw/OmniRoute/pull/10254)) — thanks @backryun
|
||||
- **fix(compression):** cap countTextTokens at 50k chars and strip base64 data URIs ([#10118](https://github.com/diegosouzapw/OmniRoute/pull/10118)) — thanks @adevwithpurpose
|
||||
- **fix(ci):** clear base-reds on release/v3.8.50 (round 4) ([#10260](https://github.com/diegosouzapw/OmniRoute/pull/10260))
|
||||
- **fix(sse):** extract perplexity-web answers from workflow_block ([#10259](https://github.com/diegosouzapw/OmniRoute/pull/10259)) — thanks @jeyhunfaslanov
|
||||
- **fix(mcp):** persist and re-attach Gemini thoughtSignature on the direct Claude<->Gemini path ([#9448](https://github.com/diegosouzapw/OmniRoute/pull/9448)) — thanks @Sam280903
|
||||
- **fix(opencode-plugin):** respect log level for lifecycle output (#8982) ([#9316](https://github.com/diegosouzapw/OmniRoute/pull/9316)) — thanks @xiaoyaner0201
|
||||
- **fix(providers):** raise default provider probe timeout from 5s to 8s ([#9283](https://github.com/diegosouzapw/OmniRoute/pull/9283)) — thanks @Sam280903
|
||||
- **fix(opencode-plugin):** stop warning when an auto combo replaces its expected /v1/models twin (#8983) ([#9042](https://github.com/diegosouzapw/OmniRoute/pull/9042)) — thanks @xiaoyaner0201
|
||||
- **fix(opencode):** force CLI User-Agent when CLI identity synthesis is enabled ([#10222](https://github.com/diegosouzapw/OmniRoute/pull/10222)) — thanks @adevwithpurpose
|
||||
- **fix(deepseek-web):** classify business auth rejection as 401 ([#10218](https://github.com/diegosouzapw/OmniRoute/pull/10218)) — thanks @Zartharas
|
||||
- **fix(combo):** make failoverBeforeRetry actually skip the same-model retry ([#10217](https://github.com/diegosouzapw/OmniRoute/pull/10217)) — thanks @hartmark
|
||||
- **fix(responses):** preserve case-insensitive combo names before Codex rewrite ([#10177](https://github.com/diegosouzapw/OmniRoute/pull/10177)) — thanks @ddarkr
|
||||
- **fix(discovery):** parse reasoning tiers nested under metadata.reasoning.supported_efforts ([#10138](https://github.com/diegosouzapw/OmniRoute/pull/10138)) — thanks @excessivechaos
|
||||
- **fix(combo):** isolate session stickiness by combo ([#10137](https://github.com/diegosouzapw/OmniRoute/pull/10137)) — thanks @hydraxman
|
||||
- **fix(combo):** default chaos SSE to comment-only for OpenAI-compatible clients ([#10128](https://github.com/diegosouzapw/OmniRoute/pull/10128)) — thanks @herjarsa
|
||||
- **fix(kimi):** normalize MFJS tool schemas ([#10079](https://github.com/diegosouzapw/OmniRoute/pull/10079)) — thanks @xz-dev
|
||||
- **fix(mcp):** move pack validation out of unit suite ([#10065](https://github.com/diegosouzapw/OmniRoute/pull/10065)) — thanks @yansigit
|
||||
- **fix(zed-hosted):** send the provider wire values cloud.zed.dev accepts ([#10051](https://github.com/diegosouzapw/OmniRoute/pull/10051)) — thanks @ARC345
|
||||
- **fix(ci):** repair and wire the two live-server E2E suites ([#10050](https://github.com/diegosouzapw/OmniRoute/pull/10050)) — thanks @ARC345
|
||||
- **fix(reasoning):** preserve and replay assistant turns ([#10045](https://github.com/diegosouzapw/OmniRoute/pull/10045)) — thanks @jackjinke
|
||||
- **fix(types):** tighten chatCore helper contracts ([#10175](https://github.com/diegosouzapw/OmniRoute/pull/10175)) — thanks @backryun
|
||||
- **fix(cli):** read the full provider catalog instead of the 6-entry fallback ([#10097](https://github.com/diegosouzapw/OmniRoute/pull/10097)) — thanks @amartinawi
|
||||
- **fix(cli):** stop swallowing non-2xx responses into benign-looking results ([#10092](https://github.com/diegosouzapw/OmniRoute/pull/10092)) — thanks @amartinawi
|
||||
- **fix(cli):** openapi endpoints/paths/validate accept the served catalog shape ([#10091](https://github.com/diegosouzapw/OmniRoute/pull/10091)) — thanks @amartinawi
|
||||
- **fix(cli):** doctor detects prebuilt better-sqlite3 binaries ([#10090](https://github.com/diegosouzapw/OmniRoute/pull/10090)) — thanks @amartinawi
|
||||
- **fix(providers):** kilo-gateway authType should be optional, not apikey ([#10086](https://github.com/diegosouzapw/OmniRoute/pull/10086)) — thanks @TengSivtean
|
||||
- **fix(cli):** strip inline comments when parsing .env values ([#10101](https://github.com/diegosouzapw/OmniRoute/pull/10101)) — thanks @amartinawi
|
||||
- **fix(logging):** document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies ([#10038](https://github.com/diegosouzapw/OmniRoute/pull/10038)) — thanks @hartmark
|
||||
- **fix(dashboard):** expose OpenAI Responses store toggle for non-Codex connections ([#10121](https://github.com/diegosouzapw/OmniRoute/pull/10121)) — thanks @hartmark
|
||||
- **fix(combo):** clear LKGP pin when its target fails, not only set it on success ([#10034](https://github.com/diegosouzapw/OmniRoute/pull/10034)) — thanks @hartmark
|
||||
- **fix(sse):** provider-response summary format bugs (dashboard Provider Response panel) ([#10037](https://github.com/diegosouzapw/OmniRoute/pull/10037)) — thanks @hartmark
|
||||
- **fix(responses-api):** tool call after reasoning collided on the same output_index ([#10025](https://github.com/diegosouzapw/OmniRoute/pull/10025)) — thanks @hartmark
|
||||
- **fix(responses-api):** explicit function-tool declaration must win over apply_patch-is-custom fallback ([#10041](https://github.com/diegosouzapw/OmniRoute/pull/10041)) — thanks @hartmark
|
||||
- **fix(kimi):** recupera limite temporario sem bloquear conta ([#10058](https://github.com/diegosouzapw/OmniRoute/pull/10058)) — thanks @bortolidiego
|
||||
- **fix(translator):** preserve Responses custom tools for OpenAI-compatible providers ([#10114](https://github.com/diegosouzapw/OmniRoute/pull/10114)) — thanks @mtb-ninja
|
||||
- **fix(providers):** xai-oauth chat→responses body + missing breaker import (#10165) ([#10170](https://github.com/diegosouzapw/OmniRoute/pull/10170)) — thanks @nordz0r
|
||||
- **fix(ollama-cloud):** map xhigh reasoning effort to max ([#10160](https://github.com/diegosouzapw/OmniRoute/pull/10160)) — thanks @Chewji9875
|
||||
- **fix(translator):** strip Codex encrypted tool-schema key for Gemini/Antigravity ([#10053](https://github.com/diegosouzapw/OmniRoute/pull/10053)) — thanks @XDayonline
|
||||
- **fix(combo):** preserve OpenCode Free oc/ prefix for connections ([#10180](https://github.com/diegosouzapw/OmniRoute/pull/10180)) — thanks @AStupidBear
|
||||
- **fix(sse):** apply free-tier filter to auto/best-free on chat path ([#10199](https://github.com/diegosouzapw/OmniRoute/pull/10199)) — thanks @ggdayup
|
||||
- **fix(providers):** default missing cache_control.ttl to 1h on the native Claude OAuth path ([#10221](https://github.com/diegosouzapw/OmniRoute/pull/10221)) — thanks @jeff-alves
|
||||
- **fix(ci):** clear base-reds on release/v3.8.50 (round 3) ([#10213](https://github.com/diegosouzapw/OmniRoute/pull/10213))
|
||||
- **fix(security):** correct XML double-unescape and non-CSPRNG nonce from CodeQL sweep ([#10154](https://github.com/diegosouzapw/OmniRoute/pull/10154))
|
||||
- **fix(docker):** eliminate npm-bundled CVEs from the published image ([#10182](https://github.com/diegosouzapw/OmniRoute/pull/10182))
|
||||
- **fix(security):** resolve open CodeQL alerts ([#10188](https://github.com/diegosouzapw/OmniRoute/pull/10188))
|
||||
- **fix(dashboard):** retarget Kimi promo CTA to the API platform aff link ([#10200](https://github.com/diegosouzapw/OmniRoute/pull/10200))
|
||||
- **fix(build):** repair broken production build, red lint gate and SWR crash ([#10198](https://github.com/diegosouzapw/OmniRoute/pull/10198))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -775,178 +698,160 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **deps (rollup):** dependency bumps and lockfile maintenance across the cycle — Dependabot groups and manual CVE-driven bumps ([#9081](https://github.com/diegosouzapw/OmniRoute/pull/9081), [#9082](https://github.com/diegosouzapw/OmniRoute/pull/9082), [#9427](https://github.com/diegosouzapw/OmniRoute/pull/9427), [#9458](https://github.com/diegosouzapw/OmniRoute/pull/9458), [#9459](https://github.com/diegosouzapw/OmniRoute/pull/9459), [#9461](https://github.com/diegosouzapw/OmniRoute/pull/9461), [#9462](https://github.com/diegosouzapw/OmniRoute/pull/9462), [#9472](https://github.com/diegosouzapw/OmniRoute/pull/9472))
|
||||
- **docs/chore (rollup):** documentation, refactoring and repository-hygiene upkeep across the cycle ([#8954](https://github.com/diegosouzapw/OmniRoute/pull/8954), [#8991](https://github.com/diegosouzapw/OmniRoute/pull/8991), [#9059](https://github.com/diegosouzapw/OmniRoute/pull/9059), [#9194](https://github.com/diegosouzapw/OmniRoute/pull/9194), [#9258](https://github.com/diegosouzapw/OmniRoute/pull/9258), [#9508](https://github.com/diegosouzapw/OmniRoute/pull/9508))
|
||||
- **main-branch plumbing (rollup):** work that landed on `main` between cycles and was carried into this one — the Mergify merge-queue migration and tuning (#7168, #7179, #7216, #7220, #7225), npm-publish unblock via dynamic runner + CI build reuse (#8941), CodeQL-driven e2e mock hardening (#7559), hermetic self-ref guard (#6634, #7341), coverage-baseline tightening (#7347), Dependabot alert resolutions via npm overrides (#8067, #8070), README flag/doc-link polish (#8317), and the v3.8.49 release plumbing itself (#7076)
|
||||
- **deps:** bump the development group across 1 directory with 22 updates ([#10043](https://github.com/diegosouzapw/OmniRoute/pull/10043)) — thanks @app/dependabot
|
||||
- **deps:** bump electron from 43.2.0 to 43.3.0 in /electron ([#10042](https://github.com/diegosouzapw/OmniRoute/pull/10042)) — thanks @app/dependabot
|
||||
- **maint(release):** 45 direct pushes to the release branch with no PR ref — base-red and quality-gate repairs, i18n string completion and stream/type fixes (quality ×6, i18n ×5, deps ×3, agentrouter ×3, providers ×2, release ×2, security ×2, logging ×2)
|
||||
- **maint(repo):** 29 chore/ci/test/docs commits rolled up — quality baselines, mutation registration, CI re-triggers, doc restructure and repo hygiene (#10187, #10189, #10190, #10193, #10196, #10203, #10204, #10205, #10207, #10210, #10236, #10318)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.50:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [@AbdullahFageeh](https://github.com/AbdullahFageeh) | #9087 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #9790, #10118, #10222 |
|
||||
| [@adrianojiu](https://github.com/adrianojiu) | #8438 |
|
||||
| [@agisota](https://github.com/agisota) | #9837 |
|
||||
| [@AgnesRiber](https://github.com/AgnesRiber) | #9718, #9976 |
|
||||
| [@ahmet-cetinkaya](https://github.com/ahmet-cetinkaya) | #8878 |
|
||||
| [@AIB1TAL0S](https://github.com/AIB1TAL0S) | #9284 |
|
||||
| [@AlanSyue](https://github.com/AlanSyue) | direct commit / report |
|
||||
| [@alex-jordan547](https://github.com/alex-jordan547) | #9235, #9245, #9813 |
|
||||
| [@amartinawi](https://github.com/amartinawi) | #10090, #10091, #10092, #10097, #10101 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #8888, #8889, #8890, #8891, #8892, #8893, #8894, #8895 |
|
||||
| [@AnhLead](https://github.com/AnhLead) | #9722 |
|
||||
| [@aniketshukla1](https://github.com/aniketshukla1) | #9148 |
|
||||
| [@Anjielon](https://github.com/Anjielon) | #8776 |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | #8916 |
|
||||
| [@ARC345](https://github.com/ARC345) | #9628, #10050, #10051 |
|
||||
| [@artickc](https://github.com/artickc) | #8571, #8578, #8791, #8843, #8870, #8927, #8974, #9097, #9549 |
|
||||
| [@Arul-](https://github.com/Arul-) | #9761 |
|
||||
| [@AStupidBear](https://github.com/AStupidBear) | #10180 |
|
||||
| [@b1nhm1nh](https://github.com/b1nhm1nh) | direct commit / report |
|
||||
| [@backryun](https://github.com/backryun) | #8228, #8451, #8627, #8809, #8818, #9084, #9086, #9090, #9091, #9092, #9093, #9114, #9119, #9120, #9122, #9135, #9136, #9137, #9138, #9139, #9141, #9561, #9562, #9563, #9564, #9565, #9566, #9742, #9747, #9748, #9751, #9753, #9755, #9791, #9792, #9793, #9795, #9796, #9797, #9798, #9920, #9972, #9973, #9974, #9975, #9977, #9978, #9979, #9984, #9986, #9987, #9988, #9989, #9990, #9998, #10087, #10088, #10134, #10178, #10175, #10254, #10255, #10256, #10257, #10258, #10339 |
|
||||
| [@Benson-mk](https://github.com/Benson-mk) | #8369 |
|
||||
| [@benzntech](https://github.com/benzntech) | #9810, #9812, #9939 |
|
||||
| [@Bl0ck154](https://github.com/Bl0ck154) | #9231 |
|
||||
| [@bortolidiego](https://github.com/bortolidiego) | #10058 |
|
||||
| [@branben](https://github.com/branben) | #9940 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #9257, #9420, #9821, #9994, #10160 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6674 |
|
||||
| [@chloeassistant](https://github.com/chloeassistant) | #9675, #9746 |
|
||||
| [@configurowebmax](https://github.com/configurowebmax) | #8877 |
|
||||
| [@corefusiion](https://github.com/corefusiion) | #8285 |
|
||||
| [@costaeder](https://github.com/costaeder) | #8626, #8629, #8630 |
|
||||
| [@csoftware-arigpt](https://github.com/csoftware-arigpt) | #3440 |
|
||||
| [@DaDecky](https://github.com/DaDecky) | direct commit / report |
|
||||
| [@DarkEsteves](https://github.com/DarkEsteves) | #10250 |
|
||||
| [@ddarkr](https://github.com/ddarkr) | #9035, #9036, #10177 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
| [@Dingding-leo](https://github.com/Dingding-leo) | #7987, #8640, #8678, #8704, #8774, #8790, #8808, #8817 |
|
||||
| [@DinonowDev](https://github.com/DinonowDev) | #8804 |
|
||||
| [@Dragost](https://github.com/Dragost) | #8339 |
|
||||
| [@dsitmilis](https://github.com/dsitmilis) | direct commit / report |
|
||||
| [@Egorich-print](https://github.com/Egorich-print) | #9001, #9020, #9058 |
|
||||
| [@engmarcosjr](https://github.com/engmarcosjr) | #9993 |
|
||||
| [@epsilonode](https://github.com/epsilonode) | #8871 |
|
||||
| [@ervareza](https://github.com/ervareza) | direct commit / report |
|
||||
| [@excessivechaos](https://github.com/excessivechaos) | #10062, #10138 |
|
||||
| [@fajarhide](https://github.com/fajarhide) | #9191, #9198 |
|
||||
| [@fenix007](https://github.com/fenix007) | #9618 |
|
||||
| [@Gecky2102](https://github.com/Gecky2102) | #9280 |
|
||||
| [@ggdayup](https://github.com/ggdayup) | #10199 |
|
||||
| [@Gioxaa](https://github.com/Gioxaa) | #9162, #9171 |
|
||||
| [@HaoNgo232](https://github.com/HaoNgo232) | direct commit / report |
|
||||
| [@Hariprajwal](https://github.com/Hariprajwal) | #9922 |
|
||||
| [@hartmark](https://github.com/hartmark) | #9635, #9704, #9711, #9712, #9727, #9734, #9735, #9738, #9741, #9744, #9745, #9822, #10025, #10034, #10037, #10038, #10041, #10121, #10217 |
|
||||
| [@Hdiaktoros](https://github.com/Hdiaktoros) | #8930 |
|
||||
| [@HectorBernstorff](https://github.com/HectorBernstorff) | direct commit / report |
|
||||
| [@HellFiveOsborn](https://github.com/HellFiveOsborn) | #9248 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #9714, #9816, #9937, #9946, #10128 |
|
||||
| [@horacecar](https://github.com/horacecar) | #7679 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #8886, #8904, #8905, #8976, #8984, #9079, #9106, #9207, #9242, #9328, #9340, #9342, #9351, #9365, #9380, #9381, #9392, #9449, #9482, #9483, #9509, #9510, #9572, #9631, #9634, #9695, #9929 |
|
||||
| [@hppsc1215](https://github.com/hppsc1215) | #8970 |
|
||||
| [@hydraxman](https://github.com/hydraxman) | #10137 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #9353, #9397 |
|
||||
| [@ikelvingo](https://github.com/ikelvingo) | #8591, #8872, #9053 |
|
||||
| [@infinit-X](https://github.com/infinit-X) | #9095 |
|
||||
| [@isaaclb98](https://github.com/isaaclb98) | #9730 |
|
||||
| [@jackjinke](https://github.com/jackjinke) | #9556, #9601, #10005, #10045 |
|
||||
| [@jax-novita](https://github.com/jax-novita) | #8913 |
|
||||
| [@jeff-alves](https://github.com/jeff-alves) | #10221 |
|
||||
| [@jeyhunfaslanov](https://github.com/jeyhunfaslanov) | #10259 |
|
||||
| [@jhordanjw123](https://github.com/jhordanjw123) | #8736 |
|
||||
| [@jktan0504](https://github.com/jktan0504) | #9025 |
|
||||
| [@joachimBrindeau](https://github.com/joachimBrindeau) | #9200 |
|
||||
| [@JoshimOfficial](https://github.com/JoshimOfficial) | #9011 |
|
||||
| [@jowimila](https://github.com/jowimila) | #9325 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | #8933, #8940, #8944, #8949 |
|
||||
| [@Kaedo17](https://github.com/Kaedo17) | #8922 |
|
||||
| [@khoazero123](https://github.com/khoazero123) | #9272 |
|
||||
| [@KittisakT](https://github.com/KittisakT) | #9423 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #7329 |
|
||||
| [@larin-vas](https://github.com/larin-vas) | #9828 |
|
||||
| [@lazysaltyfish](https://github.com/lazysaltyfish) | direct commit / report |
|
||||
| [@LeonG606](https://github.com/LeonG606) | #9457 |
|
||||
| [@Llliao1113](https://github.com/Llliao1113) | #8921 |
|
||||
| [@lucasalx](https://github.com/lucasalx) | #9919 |
|
||||
| [@lucasmellos](https://github.com/lucasmellos) | #8925 |
|
||||
| [@lukiod](https://github.com/lukiod) | #8828 |
|
||||
| [@luoyide](https://github.com/luoyide) | direct commit / report |
|
||||
| [@mad-gooze](https://github.com/mad-gooze) | #9052 |
|
||||
| [@maisdesign](https://github.com/maisdesign) | #8858 |
|
||||
| [@marchlhw](https://github.com/marchlhw) | #9050 |
|
||||
| [@matiasbaglieri](https://github.com/matiasbaglieri) | #9707 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #9150, #9246, #9291, #9414 |
|
||||
| [@McLuck](https://github.com/McLuck) | #8914 |
|
||||
| [@Michael-Rocco-Goldmann](https://github.com/Michael-Rocco-Goldmann) | #9770, #9773, #9777, #9787 |
|
||||
| [@MichaelYcJo](https://github.com/MichaelYcJo) | #8244 |
|
||||
| [@minhnhat166](https://github.com/minhnhat166) | direct commit / report |
|
||||
| [@MohitRawat017](https://github.com/MohitRawat017) | #8718, #8772, #9605 |
|
||||
| [@Momen4444](https://github.com/Momen4444) | #9612 |
|
||||
| [@MrShitFox](https://github.com/MrShitFox) | #9826 |
|
||||
| [@mtb-ninja](https://github.com/mtb-ninja) | #10114 |
|
||||
| [@MumuTW](https://github.com/MumuTW) | #8839 |
|
||||
| [@mvanhorn](https://github.com/mvanhorn) | #9542 |
|
||||
| [@Mynacol](https://github.com/Mynacol) | #9733 |
|
||||
| [@nguyenha935](https://github.com/nguyenha935) | #9044, #9215 |
|
||||
| [@nordz0r](https://github.com/nordz0r) | #10170 |
|
||||
| [@nosolosoft](https://github.com/nosolosoft) | #8900 |
|
||||
| [@oyi77](https://github.com/oyi77) | #8299, #8752, #9158, #9818 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #10216 |
|
||||
| [@PixmaNts](https://github.com/PixmaNts) | #9432 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #9077 |
|
||||
| [@Poid-ZA](https://github.com/Poid-ZA) | #9467 |
|
||||
| [@Prudhvivuda](https://github.com/Prudhvivuda) | #8807, #9014, #9015, #9016 |
|
||||
| [@qianze0628](https://github.com/qianze0628) | #9038 |
|
||||
| [@raflyazf](https://github.com/raflyazf) | direct commit / report |
|
||||
| [@Rahulsharma0810](https://github.com/Rahulsharma0810) | #8961 |
|
||||
| [@rinseaid](https://github.com/rinseaid) | #8945, #9037, #9932, #9933, #9969, #9982 |
|
||||
| [@rixzkiye](https://github.com/rixzkiye) | direct commit / report |
|
||||
| [@RobertsXML](https://github.com/RobertsXML) | direct commit / report |
|
||||
| [@royanrosyad85](https://github.com/royanrosyad85) | direct commit / report |
|
||||
| [@rushsinging](https://github.com/rushsinging) | #8947 |
|
||||
| [@ryan-brosas](https://github.com/ryan-brosas) | #9693 |
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@sadSanta-07](https://github.com/sadSanta-07) | #9938 |
|
||||
| [@SalyyS1](https://github.com/SalyyS1) | direct commit / report |
|
||||
| [@Sam280903](https://github.com/Sam280903) | #9274, #9278, #9281, #9283, #9448 |
|
||||
| [@seakleangnhak](https://github.com/seakleangnhak) | direct commit / report |
|
||||
| [@seanford](https://github.com/seanford) | #8523 |
|
||||
| [@SemonCat](https://github.com/SemonCat) | direct commit / report |
|
||||
| [@shixi-li](https://github.com/shixi-li) | #9022, #9513, #10001 |
|
||||
| [@soulhakr](https://github.com/soulhakr) | #8799 |
|
||||
| [@stanleytejakusuma](https://github.com/stanleytejakusuma) | #9610 |
|
||||
| [@Stazyu](https://github.com/Stazyu) | #9007, #9226, #9438 |
|
||||
| [@SupremeNexas](https://github.com/SupremeNexas) | #9913 |
|
||||
| [@swingtempo](https://github.com/swingtempo) | #9307 |
|
||||
| [@szzhoujiarui](https://github.com/szzhoujiarui) | #9218 |
|
||||
| [@tald26](https://github.com/tald26) | #9959 |
|
||||
| [@taltas](https://github.com/taltas) | direct commit / report |
|
||||
| [@TechNickAI](https://github.com/TechNickAI) | #9251 |
|
||||
| [@TengSivtean](https://github.com/TengSivtean) | #10000, #10002, #10086 |
|
||||
| [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) | #9326 |
|
||||
| [@tiangao88](https://github.com/tiangao88) | #10046 |
|
||||
| [@tuxmonteiro](https://github.com/tuxmonteiro) | #9065 |
|
||||
| [@vinogradovnet](https://github.com/vinogradovnet) | #9581 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #9111, #9783 |
|
||||
| [@wgordon17](https://github.com/wgordon17) | #8909, #9233, #9441, #9619 |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #8713 |
|
||||
| [@witt3rd](https://github.com/witt3rd) | #9962, #9963 |
|
||||
| [@XDayonline](https://github.com/XDayonline) | #10053 |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8757, #8869, #8876, #8883, #8906, #8931, #9021, #9027, #9452, #9042, #9316 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #8908, #9199, #9205, #9262, #9290, #9313, #9555, #9569, #9629, #9788, #9983, #10079, #10243 |
|
||||
| [@yansigit](https://github.com/yansigit) | #9834, #9911, #9917, #9921, #10065 |
|
||||
| [@yidecode](https://github.com/yidecode) | direct commit / report |
|
||||
| [@yulinlina](https://github.com/yulinlina) | #10013 |
|
||||
| [@yutuknown](https://github.com/yutuknown) | #8999 |
|
||||
| [@zabrodschiipavel-sketch](https://github.com/zabrodschiipavel-sketch) | #9312 |
|
||||
| [@Zartharas](https://github.com/Zartharas) | #9161, #9164, #9181, #9182, #9184, #9185, #9186, #9189, #9294, #9825, #9833, #9936, #9965, #9992, #10218 |
|
||||
| [@Zenlyte](https://github.com/Zenlyte) | #9005 |
|
||||
| [@zhiru](https://github.com/zhiru) | #9099, #9101 |
|
||||
| [@ziuus](https://github.com/ziuus) | #8912 |
|
||||
| [@zuckdorsey](https://github.com/zuckdorsey) | #9723 |
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@AbdullahFageeh](https://github.com/AbdullahFageeh) | #9087 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #9790 |
|
||||
| [@adrianojiu](https://github.com/adrianojiu) | #8438 |
|
||||
| [@agisota](https://github.com/agisota) | #9837 |
|
||||
| [@AgnesRiber](https://github.com/AgnesRiber) | #9718, #9976 |
|
||||
| [@ahmet-cetinkaya](https://github.com/ahmet-cetinkaya) | #8878 |
|
||||
| [@AIB1TAL0S](https://github.com/AIB1TAL0S) | #9284 |
|
||||
| [@AlanSyue](https://github.com/AlanSyue) | direct commit / report |
|
||||
| [@alex-jordan547](https://github.com/alex-jordan547) | #9235, #9245, #9813 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #8888, #8889, #8890, #8891, #8892, #8893, #8894, #8895 |
|
||||
| [@AnhLead](https://github.com/AnhLead) | #9722 |
|
||||
| [@aniketshukla1](https://github.com/aniketshukla1) | #9148 |
|
||||
| [@Anjielon](https://github.com/Anjielon) | #8776 |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | #8916 |
|
||||
| [@ARC345](https://github.com/ARC345) | #9628 |
|
||||
| [@artickc](https://github.com/artickc) | #8571, #8578, #8791, #8843, #8870, #8927, #8974, #9097, #9549 |
|
||||
| [@Arul-](https://github.com/Arul-) | #9761 |
|
||||
| [@b1nhm1nh](https://github.com/b1nhm1nh) | direct commit / report |
|
||||
| [@backryun](https://github.com/backryun) | #8228, #8451, #8627, #8809, #8818, #9084, #9086, #9090, #9091, #9092, #9093, #9114, #9119, #9120, #9122, #9135, #9136, #9137, #9138, #9139, #9141, #9561, #9562, #9563, #9564, #9565, #9566, #9742, #9747, #9748, #9751, #9753, #9755, #9791, #9792, #9793, #9795, #9796, #9797, #9798, #9920, #9972, #9973, #9974, #9975, #9977, #9978, #9979, #9984, #9986, #9987, #9988, #9989, #9990, #9998, #10087, #10088, #10134, #10178 |
|
||||
| [@Benson-mk](https://github.com/Benson-mk) | #8369 |
|
||||
| [@benzntech](https://github.com/benzntech) | #9810, #9812, #9939 |
|
||||
| [@Bl0ck154](https://github.com/Bl0ck154) | #9231 |
|
||||
| [@branben](https://github.com/branben) | #9940 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #9257, #9420, #9821, #9994 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6674 |
|
||||
| [@chloeassistant](https://github.com/chloeassistant) | #9675, #9746 |
|
||||
| [@configurowebmax](https://github.com/configurowebmax) | #8877 |
|
||||
| [@corefusiion](https://github.com/corefusiion) | #8285 |
|
||||
| [@costaeder](https://github.com/costaeder) | #8626, #8629, #8630 |
|
||||
| [@csoftware-arigpt](https://github.com/csoftware-arigpt) | #3440 |
|
||||
| [@DaDecky](https://github.com/DaDecky) | direct commit / report |
|
||||
| [@ddarkr](https://github.com/ddarkr) | #9035, #9036 |
|
||||
| [@Dingding-leo](https://github.com/Dingding-leo) | #7987, #8640, #8678, #8704, #8774, #8790, #8808, #8817 |
|
||||
| [@DinonowDev](https://github.com/DinonowDev) | #8804 |
|
||||
| [@Dragost](https://github.com/Dragost) | #8339 |
|
||||
| [@dsitmilis](https://github.com/dsitmilis) | direct commit / report |
|
||||
| [@Egorich-print](https://github.com/Egorich-print) | #9001, #9020, #9058 |
|
||||
| [@engmarcosjr](https://github.com/engmarcosjr) | #9993 |
|
||||
| [@epsilonode](https://github.com/epsilonode) | #8871 |
|
||||
| [@ervareza](https://github.com/ervareza) | direct commit / report |
|
||||
| [@fajarhide](https://github.com/fajarhide) | #9191, #9198 |
|
||||
| [@fenix007](https://github.com/fenix007) | #9618 |
|
||||
| [@Gecky2102](https://github.com/Gecky2102) | #9280 |
|
||||
| [@Gioxaa](https://github.com/Gioxaa) | #9162, #9171 |
|
||||
| [@HaoNgo232](https://github.com/HaoNgo232) | direct commit / report |
|
||||
| [@Hariprajwal](https://github.com/Hariprajwal) | #9922 |
|
||||
| [@hartmark](https://github.com/hartmark) | #9635, #9704, #9711, #9712, #9727, #9734, #9735, #9738, #9741, #9744, #9745, #9822 |
|
||||
| [@Hdiaktoros](https://github.com/Hdiaktoros) | #8930 |
|
||||
| [@HectorBernstorff](https://github.com/HectorBernstorff) | direct commit / report |
|
||||
| [@HellFiveOsborn](https://github.com/HellFiveOsborn) | #9248 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #9714, #9816, #9937, #9946 |
|
||||
| [@horacecar](https://github.com/horacecar) | #7679 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #8886, #8904, #8905, #8976, #8984, #9079, #9106, #9207, #9242, #9328, #9340, #9342, #9351, #9365, #9380, #9381, #9392, #9449, #9482, #9483, #9509, #9510, #9572, #9631, #9634, #9695, #9929 |
|
||||
| [@hppsc1215](https://github.com/hppsc1215) | #8970 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #9353, #9397 |
|
||||
| [@ikelvingo](https://github.com/ikelvingo) | #8591, #8872, #9053 |
|
||||
| [@infinit-X](https://github.com/infinit-X) | #9095 |
|
||||
| [@isaaclb98](https://github.com/isaaclb98) | #9730 |
|
||||
| [@jackjinke](https://github.com/jackjinke) | #9556, #9601, #10005 |
|
||||
| [@jax-novita](https://github.com/jax-novita) | #8913 |
|
||||
| [@jhordanjw123](https://github.com/jhordanjw123) | #8736 |
|
||||
| [@jktan0504](https://github.com/jktan0504) | #9025 |
|
||||
| [@joachimBrindeau](https://github.com/joachimBrindeau) | #9200 |
|
||||
| [@JoshimOfficial](https://github.com/JoshimOfficial) | #9011 |
|
||||
| [@jowimila](https://github.com/jowimila) | #9325 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | #8933, #8940, #8944, #8949 |
|
||||
| [@Kaedo17](https://github.com/Kaedo17) | #8922 |
|
||||
| [@khoazero123](https://github.com/khoazero123) | #9272 |
|
||||
| [@KittisakT](https://github.com/KittisakT) | #9423 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #7329 |
|
||||
| [@larin-vas](https://github.com/larin-vas) | #9828 |
|
||||
| [@lazysaltyfish](https://github.com/lazysaltyfish) | direct commit / report |
|
||||
| [@LeonG606](https://github.com/LeonG606) | #9457 |
|
||||
| [@Llliao1113](https://github.com/Llliao1113) | #8921 |
|
||||
| [@lucasalx](https://github.com/lucasalx) | #9919 |
|
||||
| [@lucasmellos](https://github.com/lucasmellos) | #8925 |
|
||||
| [@lukiod](https://github.com/lukiod) | #8828 |
|
||||
| [@luoyide](https://github.com/luoyide) | direct commit / report |
|
||||
| [@mad-gooze](https://github.com/mad-gooze) | #9052 |
|
||||
| [@maisdesign](https://github.com/maisdesign) | #8858 |
|
||||
| [@marchlhw](https://github.com/marchlhw) | #9050 |
|
||||
| [@matiasbaglieri](https://github.com/matiasbaglieri) | #9707 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #9150, #9246, #9291, #9414 |
|
||||
| [@McLuck](https://github.com/McLuck) | #8914 |
|
||||
| [@Michael-Rocco-Goldmann](https://github.com/Michael-Rocco-Goldmann) | #9770, #9773, #9777, #9787 |
|
||||
| [@MichaelYcJo](https://github.com/MichaelYcJo) | #8244 |
|
||||
| [@minhnhat166](https://github.com/minhnhat166) | direct commit / report |
|
||||
| [@MohitRawat017](https://github.com/MohitRawat017) | #8718, #8772, #9605 |
|
||||
| [@Momen4444](https://github.com/Momen4444) | #9612 |
|
||||
| [@MrShitFox](https://github.com/MrShitFox) | #9826 |
|
||||
| [@MumuTW](https://github.com/MumuTW) | #8839 |
|
||||
| [@mvanhorn](https://github.com/mvanhorn) | #9542 |
|
||||
| [@Mynacol](https://github.com/Mynacol) | #9733 |
|
||||
| [@nguyenha935](https://github.com/nguyenha935) | #9044, #9215 |
|
||||
| [@nosolosoft](https://github.com/nosolosoft) | #8900 |
|
||||
| [@oyi77](https://github.com/oyi77) | #8299, #8752, #9158, #9818 |
|
||||
| [@PixmaNts](https://github.com/PixmaNts) | #9432 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #9077 |
|
||||
| [@Poid-ZA](https://github.com/Poid-ZA) | #9467 |
|
||||
| [@Prudhvivuda](https://github.com/Prudhvivuda) | #8807, #9014, #9015, #9016 |
|
||||
| [@qianze0628](https://github.com/qianze0628) | #9038 |
|
||||
| [@raflyazf](https://github.com/raflyazf) | direct commit / report |
|
||||
| [@Rahulsharma0810](https://github.com/Rahulsharma0810) | #8961 |
|
||||
| [@rinseaid](https://github.com/rinseaid) | #8945, #9037, #9932, #9933, #9969, #9982 |
|
||||
| [@rixzkiye](https://github.com/rixzkiye) | direct commit / report |
|
||||
| [@RobertsXML](https://github.com/RobertsXML) | direct commit / report |
|
||||
| [@royanrosyad85](https://github.com/royanrosyad85) | direct commit / report |
|
||||
| [@rushsinging](https://github.com/rushsinging) | #8947 |
|
||||
| [@ryan-brosas](https://github.com/ryan-brosas) | #9693 |
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@sadSanta-07](https://github.com/sadSanta-07) | #9938 |
|
||||
| [@SalyyS1](https://github.com/SalyyS1) | direct commit / report |
|
||||
| [@Sam280903](https://github.com/Sam280903) | #9274, #9278, #9281 |
|
||||
| [@seakleangnhak](https://github.com/seakleangnhak) | direct commit / report |
|
||||
| [@seanford](https://github.com/seanford) | #8523 |
|
||||
| [@SemonCat](https://github.com/SemonCat) | direct commit / report |
|
||||
| [@shixi-li](https://github.com/shixi-li) | #9022, #9513, #10001 |
|
||||
| [@soulhakr](https://github.com/soulhakr) | #8799 |
|
||||
| [@stanleytejakusuma](https://github.com/stanleytejakusuma) | #9610 |
|
||||
| [@Stazyu](https://github.com/Stazyu) | #9007, #9226, #9438 |
|
||||
| [@SupremeNexas](https://github.com/SupremeNexas) | #9913 |
|
||||
| [@swingtempo](https://github.com/swingtempo) | #9307 |
|
||||
| [@szzhoujiarui](https://github.com/szzhoujiarui) | #9218 |
|
||||
| [@tald26](https://github.com/tald26) | #9959 |
|
||||
| [@taltas](https://github.com/taltas) | direct commit / report |
|
||||
| [@TechNickAI](https://github.com/TechNickAI) | #9251 |
|
||||
| [@TengSivtean](https://github.com/TengSivtean) | #10000, #10002 |
|
||||
| [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) | #9326 |
|
||||
| [@tuxmonteiro](https://github.com/tuxmonteiro) | #9065 |
|
||||
| [@vinogradovnet](https://github.com/vinogradovnet) | #9581 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #9111, #9783 |
|
||||
| [@wgordon17](https://github.com/wgordon17) | #8909, #9233, #9441, #9619 |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #8713 |
|
||||
| [@witt3rd](https://github.com/witt3rd) | #9962, #9963 |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8757, #8869, #8876, #8883, #8906, #8931, #9021, #9027, #9452 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #8908, #9199, #9205, #9262, #9290, #9313, #9555, #9569, #9629, #9788, #9983 |
|
||||
| [@yansigit](https://github.com/yansigit) | #9834, #9911, #9917, #9921 |
|
||||
| [@yidecode](https://github.com/yidecode) | direct commit / report |
|
||||
| [@yulinlina](https://github.com/yulinlina) | #10013 |
|
||||
| [@yutuknown](https://github.com/yutuknown) | #8999 |
|
||||
| [@zabrodschiipavel-sketch](https://github.com/zabrodschiipavel-sketch) | #9312 |
|
||||
| [@Zartharas](https://github.com/Zartharas) | #9161, #9164, #9181, #9182, #9184, #9185, #9186, #9189, #9294, #9825, #9833, #9936, #9965, #9992 |
|
||||
| [@Zenlyte](https://github.com/Zenlyte) | #9005 |
|
||||
| [@zhiru](https://github.com/zhiru) | #9099, #9101 |
|
||||
| [@ziuus](https://github.com/ziuus) | #8912 |
|
||||
| [@zuckdorsey](https://github.com/zuckdorsey) | #9723 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
@@ -1243,7 +1148,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **feat(quality):** temporary relax of complexity/file-size ratchets for v3.8.50-3.8.54 PREPARE phase ([#8767](https://github.com/diegosouzapw/OmniRoute/pull/8767))
|
||||
- **feat(db):** let the migration runner scan extra namespaced directories ([#8770](https://github.com/diegosouzapw/OmniRoute/pull/8770))
|
||||
- **feat(api):** prompt-cache health summary endpoint and analytics tab ([#8827](https://github.com/diegosouzapw/OmniRoute/pull/8827))
|
||||
|
||||
### ⚡ Performance
|
||||
|
||||
- **perf(db):** project columns + composite index in getProviderConnections ([#6918](https://github.com/diegosouzapw/OmniRoute/pull/6918)) — thanks @oyi77
|
||||
@@ -1259,7 +1163,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **perf:** lazy provider init, P2C quota cache, structuredClone elimination, getSettings→getCachedSettings (batch 2) ([#7893](https://github.com/diegosouzapw/OmniRoute/pull/7893)) — thanks @oyi77
|
||||
- **perf(api):** singleflight version lookups ([#8301](https://github.com/diegosouzapw/OmniRoute/pull/8301)) — thanks @RaviTharuma
|
||||
- **perf(api):** skip the full catalog build for quota-exclusive keys ([#8771](https://github.com/diegosouzapw/OmniRoute/pull/8771))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix:** add re-entrancy guard to token health check sweep ([#6917](https://github.com/diegosouzapw/OmniRoute/pull/6917)) — thanks @oyi77
|
||||
@@ -1659,7 +1562,7 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`. ([#6732](https://github.com/diegosouzapw/OmniRoute/pull/6732))
|
||||
- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path). ([#6735](https://github.com/diegosouzapw/OmniRoute/pull/6735))
|
||||
- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`. ([#6741](https://github.com/diegosouzapw/OmniRoute/pull/6741))
|
||||
- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). ([#6742](https://github.com/diegosouzapw/OmniRoute/pull/6742))
|
||||
- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a *local cache* already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account). ([#6742](https://github.com/diegosouzapw/OmniRoute/pull/6742))
|
||||
- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters). ([#6743](https://github.com/diegosouzapw/OmniRoute/pull/6743))
|
||||
- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756).
|
||||
- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos).
|
||||
@@ -1779,7 +1682,7 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- fix(sse): stop stream readiness from treating a choices-less mid-stream error frame as a successful stream, so combo can fail over instead of returning zero `choices` (#7503)
|
||||
- Fixed the Codex connection **Test** button always reporting success for ChatGPT-account tokens: the probe used `gpt-5.3-codex`, a codex-only model ChatGPT accounts reject with a 400 — the same status the probe treats as "auth OK", so a bad token was indistinguishable from a good one. It now probes with `gpt-5.5`, a model ChatGPT-account sessions actually support (#7521).
|
||||
- The Codex account import (`POST /api/oauth/codex/import`) now validates each record's `refresh_token` against OpenAI's OAuth endpoint before persisting the connection: an already-invalidated session (`refresh_token_invalidated` / a dead `auth.json`) is rejected with a clear "run `codex login` again and re-import" message instead of importing as `active` and failing confusingly on first use. Valid tokens import as before, with any rotated tokens applied (#7522).
|
||||
- The PKCE OAuth start (`/api/oauth/[provider]/start-callback-server`, used by Codex/Windsurf/Devin) now detects when OmniRoute is being driven from a remote host and returns a reverse-tunnel hint (`remoteHost`, `tunnelCommand`, `message`) instead of hanging silently: the callback server binds the _server's_ localhost:PORT, so a browser on a different machine would redirect to its own localhost and never complete. Loopback access is unchanged (#7523).
|
||||
- The PKCE OAuth start (`/api/oauth/[provider]/start-callback-server`, used by Codex/Windsurf/Devin) now detects when OmniRoute is being driven from a remote host and returns a reverse-tunnel hint (`remoteHost`, `tunnelCommand`, `message`) instead of hanging silently: the callback server binds the *server's* localhost:PORT, so a browser on a different machine would redirect to its own localhost and never complete. Loopback access is unchanged (#7523).
|
||||
- fix(providers): search providers now expose a static model catalog derived from `searchTypes`, fixing "does not support models listing" 400 for serper-search, brave-search, perplexity-search, exa-search, tavily-search, google-pse-search, youcom-search, searxng-search, zai-search (#7529)
|
||||
- fix(sse): map `tool_search` to a Chat function tool instead of dropping it during Responses->Chat translation (#7532)
|
||||
- fix(sse): gate `verbosity`/`prompt_cache_key` on OpenAI destination during Responses->Chat translation, stopping the leak to non-OpenAI upstreams like NVIDIA (#7533)
|
||||
@@ -2089,7 +1992,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **fix(dashboard):** the /home quick-start cards no longer prefetch — #8292 opted the sidebar out of automatic route prefetch but left the landing page's own five links untouched, so first paint still fired 12 speculative RSC requests. The e2e guard shipped in that same PR could not catch it: it hung in the auth helper and never reached its assertion until this release's CI.
|
||||
- **fix(dashboard):** Request Logs detail no longer crashes on a structured error object — #7920 introduced `formatErrorForDisplay` for exactly this, but the combo-503 / cooldown checks added by #8213 read the raw field and called `.toLowerCase()` on it. Both paths now share the helper.
|
||||
- **fix(dashboard):** the logs detail modal stops reopening on first close again — #6830 fixed it by reading the deep-link id once, and the #8354 page rewrite regressed it by reading the live `searchParams` on every render, flipping the prop mid-session.
|
||||
|
||||
### 📚 Docs
|
||||
|
||||
- **docs(quality):** codify retry policy per runner + release-level drift rule (WS5.4/WS5.5) ([#7107](https://github.com/diegosouzapw/OmniRoute/pull/7107))
|
||||
@@ -2132,7 +2034,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **docs(codex):** document session affinity and stream idle for long tasks ([#8709](https://github.com/diegosouzapw/OmniRoute/pull/8709)) — thanks @DinonowDev
|
||||
- **docs:** replace outdated Polish docs with translation from latest English ([#8823](https://github.com/diegosouzapw/OmniRoute/pull/8823)) — thanks @leszek3737
|
||||
- **docs:** restore the Polish API_REFERENCE removed by #8823 ([#8831](https://github.com/diegosouzapw/OmniRoute/pull/8831))
|
||||
|
||||
### 🧪 Tests & Quality
|
||||
|
||||
- **test(build):** derive pack-artifact closures for all npm-shipped entrypoints (#7065 class) ([#7081](https://github.com/diegosouzapw/OmniRoute/pull/7081))
|
||||
@@ -2150,7 +2051,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **test(e2e):** contract test for the full provider journey ([#8444](https://github.com/diegosouzapw/OmniRoute/pull/8444)) — thanks @HoneyTyagii
|
||||
- **test(sse):** repair two base-red gates on release/v3.8.49 ([#8490](https://github.com/diegosouzapw/OmniRoute/pull/8490)) — thanks @backryun
|
||||
- **test(context):** isolate context-manager suite from local DATA_DIR ([#8596](https://github.com/diegosouzapw/OmniRoute/pull/8596)) — thanks @DinonowDev
|
||||
|
||||
### 🔧 Chores / CI
|
||||
|
||||
- **chore(release):** gate the sync-back push on release-green --quick (WS0.3) ([#7083](https://github.com/diegosouzapw/OmniRoute/pull/7083))
|
||||
@@ -2241,7 +2141,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **refactor(sse):** use the shared ApiKeyMetadata in reasoningRouting instead of a local duck-type ([#8643](https://github.com/diegosouzapw/OmniRoute/pull/8643)) — thanks @backryun
|
||||
- **refactor(sse):** narrow three media-generation result unions ([#8645](https://github.com/diegosouzapw/OmniRoute/pull/8645)) — thanks @backryun
|
||||
- **refactor(sse):** declare the semantic-cache read path's parameters ([#8646](https://github.com/diegosouzapw/OmniRoute/pull/8646)) — thanks @backryun
|
||||
|
||||
### 🔀 Other
|
||||
|
||||
- [needs-vps] fix(electron): materialize Turbopack hashed-module symlinks during packaging (#6724, #6594) ([#6794](https://github.com/diegosouzapw/OmniRoute/pull/6794)) — thanks @huohua-dev
|
||||
@@ -2271,7 +2170,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **refactor(antigravity):** align official clients and callable catalog ([#8013](https://github.com/diegosouzapw/OmniRoute/pull/8013)) — thanks @backryun
|
||||
- **refactor(compression):** extract resolveHeadroomDetail to keep dispatchCompression under the complexity gate ([#8058](https://github.com/diegosouzapw/OmniRoute/pull/8058))
|
||||
- **deps:** bump next from 16.2.10 to 16.2.11 ([#8235](https://github.com/diegosouzapw/OmniRoute/pull/8235)) — thanks @dependabot[bot]
|
||||
|
||||
### 🩹 Direct release-branch fixes (no PR — authorized base-red sweep, 2026-07-18)
|
||||
|
||||
- **fix(base-red):** full-suite realignment after the 102-PR merge campaign: two real production fixes (legacy `refresh_token` column healed before its index is created; `shouldSkipCloudSyncInitialization` no longer swaps its `(env, argv)` arguments) plus 13 test files, goldens, provider counts, and env docs realigned to the live-validated behavior of the merged PRs.
|
||||
@@ -2279,7 +2177,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **test(ui):** the `vitest:ui` suite was red across the whole cycle and nobody saw it — the job kept being cancelled by successive pushes, so a blocking gate never ran to completion. Root cause: #7935 instrumented ~180 shared/dashboard components with `next-intl` without updating the tests that mount them. Fixed at the shared setup (`tests/_setup/vitestUiPolyfills.ts`) with a translator backed by the real `en.json`, memoized per namespace so components whose `useCallback`/`useEffect` depend on `t` no longer loop; 15 test files realigned to the real strings. 194→198 files green.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`. ([#6784](https://github.com/diegosouzapw/OmniRoute/pull/6784))
|
||||
- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `<PR>-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878))
|
||||
- **chore(ci):** stop dependabot from proposing `typescript` majors — `typescript-eslint` pins a hard peer upper bound (`>=4.8.4 <6.1.0`), so a TS 7 bump violates the peer and takes the whole toolchain red at once. #7068 grouped it with 6 harmless dev bumps and blocked all of them. TS majors now migrate intentionally, in their own PR.
|
||||
@@ -2377,190 +2274,195 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- **CI**: promote `test:vitest:ui` to a blocking gate — the suite is 870/870 green again after the WS6.1 triage (#7127), so `continue-on-error` is removed from the vitest job
|
||||
- chore(tests): fix all 70 failing `test:vitest:ui` tests across 30 files (was advisory/parked) — root causes were 15 node:test-authored `.tsx` files never collected by vitest, a missing `window.matchMedia` jsdom polyfill, stale assertions against a redesigned BuildTab wizard / CompressionHub Phase-2 UI, and one obsolete test for a retired Plans screen; suite is now 158/158 files, 870/870 tests green (promotion to blocking is a follow-up)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.49:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [@0x2f0](https://github.com/0x2f0) | #8679 |
|
||||
| [@0xheycat](https://github.com/0xheycat) | #8751 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #8123 |
|
||||
| [@adrianaryaputra](https://github.com/adrianaryaputra) | #7928 |
|
||||
| [@advane204f](https://github.com/advane204f) | direct commit / report |
|
||||
| [@Ajeesh25353646](https://github.com/Ajeesh25353646) | #7528 |
|
||||
| [@allanvb](https://github.com/allanvb) | #8471, #8759, #8814, #8862 |
|
||||
| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821, #7041, #7042, #7164, #7277, #7490, #7492, #7644 |
|
||||
| [@alvaretto](https://github.com/alvaretto) | #8077, #8161, #8170 |
|
||||
| [@amitgolan60-coder](https://github.com/amitgolan60-coder) | #8727 |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | #7678 |
|
||||
| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829, #7794, #7804, #7810, #7813, #7816, #7891, #8050 |
|
||||
| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report |
|
||||
| [@anndev-69](https://github.com/anndev-69) | direct commit / report |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | #8127 |
|
||||
| [@arpit-jaiswal-dev](https://github.com/arpit-jaiswal-dev) | #7881 |
|
||||
| [@artickc](https://github.com/artickc) | #6763, #6955, #7204, #7696, #7768, #7896, #7900, #7911, #7930, #7994, #8006, #8159, #8470, #8496, #8511, #8863 |
|
||||
| [@Arul-](https://github.com/Arul-) | #7878 |
|
||||
| [@asynx6](https://github.com/asynx6) | direct commit / report |
|
||||
| [@attid](https://github.com/attid) | #6984 |
|
||||
| [@backryun](https://github.com/backryun) | #6280, #6675, #6862, #7296, #7314, #7358, #7531, #7687, #7772, #7812, #7866, #7874, #7882, #7914, #8013, #8225, #8226, #8227, #8230, #8266, #8275, #8298, #8464, #8473, #8483, #8485, #8489, #8490, #8498, #8499, #8520, #8525, #8528, #8531, #8533, #8557, #8638, #8639, #8641, #8643, #8644, #8645, #8646, #8647, #8661, #8663, #8665, #8732, #8810, #8811, #8812, #8815, #8816, #8819, #8820, #8824 |
|
||||
| [@beingshafin](https://github.com/beingshafin) | direct commit / report |
|
||||
| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 |
|
||||
| [@brunnolouzada](https://github.com/brunnolouzada) | #8581 |
|
||||
| [@c4usal](https://github.com/c4usal) | #7627, #8129 |
|
||||
| [@CahyokPutraDev99](https://github.com/CahyokPutraDev99) | direct commit / report |
|
||||
| [@Capslockb](https://github.com/Capslockb) | #7892 |
|
||||
| [@CarmeloCampos](https://github.com/CarmeloCampos) | direct commit / report |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #7545 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6022, #6593, #6643, #6644, #6646, #6650, #6769, #6771, #6804, #7079, #7520, #8143 |
|
||||
| [@chitholian](https://github.com/chitholian) | direct commit / report |
|
||||
| [@chy1211](https://github.com/chy1211) | direct commit / report |
|
||||
| [@CitrusIce](https://github.com/CitrusIce) | #6937, #6938 |
|
||||
| [@costaeder](https://github.com/costaeder) | #8628 |
|
||||
| [@Dan-ex-hub](https://github.com/Dan-ex-hub) | #7743 |
|
||||
| [@danscMax](https://github.com/danscMax) | #7359, #7511, #7517, #7648, #7656, #7672, #7689 |
|
||||
| [@dependabot](https://github.com/dependabot) | #7897, #7898, #8235 |
|
||||
| [@Dingding-leo](https://github.com/Dingding-leo) | #7988, #7989, #8162, #8165, #8167, #8283, #8286, #8287, #8633, #8636, #8642, #8651, #8652, #8667, #8691, #8715, #8730, #8731, #8762, #8763, #8764, #8795 |
|
||||
| [@DinonowDev](https://github.com/DinonowDev) | #8552, #8559, #8583, #8596, #8615, #8709, #8710, #8716, #8798, #8800, #8802 |
|
||||
| [@dionisius95](https://github.com/dionisius95) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #7500 |
|
||||
| [@dongwook-chan](https://github.com/dongwook-chan) | #7574, #7582, #7704 |
|
||||
| [@Dragost](https://github.com/Dragost) | #8289 |
|
||||
| [@Duongkhanhtool](https://github.com/Duongkhanhtool) | direct commit / report |
|
||||
| [@dvirarad](https://github.com/dvirarad) | #8410 |
|
||||
| [@ekinnee](https://github.com/ekinnee) | #7601, #7613, #7614, #7662, #7779, #7927, #7932, #7980, #8735 |
|
||||
| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647, #7863 |
|
||||
| [@epsilonode](https://github.com/epsilonode) | #8724 |
|
||||
| [@evinjohnn](https://github.com/evinjohnn) | direct commit / report |
|
||||
| [@fajarbossit](https://github.com/fajarbossit) | direct commit / report |
|
||||
| [@felipeleite](https://github.com/felipeleite) | direct commit / report |
|
||||
| [@fenix007](https://github.com/fenix007) | #7171, #7399, #8306, #8561 |
|
||||
| [@FenjuFu](https://github.com/FenjuFu) | #7942 |
|
||||
| [@floze-the-genius](https://github.com/floze-the-genius) | #7707 |
|
||||
| [@fontvu](https://github.com/fontvu) | direct commit / report |
|
||||
| [@fuko2935](https://github.com/fuko2935) | #8726 |
|
||||
| [@fzrilsh](https://github.com/fzrilsh) | direct commit / report |
|
||||
| [@gitcommit90](https://github.com/gitcommit90) | #6986 |
|
||||
| [@glazec](https://github.com/glazec) | #8632 |
|
||||
| [@growab](https://github.com/growab) | #7062, #7300 |
|
||||
| [@guanbear](https://github.com/guanbear) | #7028 |
|
||||
| [@guhcostan](https://github.com/guhcostan) | #8433 |
|
||||
| [@hartmark](https://github.com/hartmark) | #8208, #8209, #8210, #8211, #8212, #8213, #8337, #8341, #8354, #8462 |
|
||||
| [@HassiyYT](https://github.com/HassiyYT) | #7864 |
|
||||
| [@heishen6](https://github.com/heishen6) | direct commit / report |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #7612, #7625, #7633, #7869, #7871, #8476 |
|
||||
| [@HoneyTyagii](https://github.com/HoneyTyagii) | #8444 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #7035, #7129, #7290, #7398, #7408, #7973, #8290, #8312, #8842, #8845, #8860, #8865 |
|
||||
| [@hppsc1215](https://github.com/hppsc1215) | #7546, #8835 |
|
||||
| [@huohua-dev](https://github.com/huohua-dev) | #6794 |
|
||||
| [@hydraxman](https://github.com/hydraxman) | #7909 |
|
||||
| [@iamraydoan](https://github.com/iamraydoan) | #6798 |
|
||||
| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 |
|
||||
| [@ikelvingo](https://github.com/ikelvingo) | #8355 |
|
||||
| [@insoln](https://github.com/insoln) | #7906, #7908, #8041, #8054, #8062 |
|
||||
| [@irvandikky](https://github.com/irvandikky) | #7695 |
|
||||
| [@isiahw1](https://github.com/isiahw1) | #7555 |
|
||||
| [@itiwant](https://github.com/itiwant) | direct commit / report |
|
||||
| [@janeza2](https://github.com/janeza2) | #6308 |
|
||||
| [@Jordannst](https://github.com/Jordannst) | direct commit / report |
|
||||
| [@JoshimOfficial](https://github.com/JoshimOfficial) | #8526 |
|
||||
| [@justdoGIT](https://github.com/justdoGIT) | #8695 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | #6776, #6993, #7154, #7177, #7269, #7273, #7280, #7281, #7282, #7323, #7360, #7377, #7378, #7379, #7380, #7381, #7419, #7607, #7894, #7905, #7912, #8008, #8009, #8010 |
|
||||
| [@kamenkadmitry](https://github.com/kamenkadmitry) | #7350, #7425 |
|
||||
| [@kaon0388v1](https://github.com/kaon0388v1) | #7049 |
|
||||
| [@KaynXu](https://github.com/KaynXu) | #8284 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856, #7008, #7087, #7093, #7128, #7130, #7136, #7315, #7318, #7334, #7336 |
|
||||
| [@KunN-21](https://github.com/KunN-21) | direct commit / report |
|
||||
| [@leninejunior](https://github.com/leninejunior) | #8049 |
|
||||
| [@leszek3737](https://github.com/leszek3737) | #7782, #7807, #8343, #8543, #8823 |
|
||||
| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report |
|
||||
| [@linhdmn](https://github.com/linhdmn) | #8439 |
|
||||
| [@Long-Feeds](https://github.com/Long-Feeds) | #8011 |
|
||||
| [@loulanyue](https://github.com/loulanyue) | #7540 |
|
||||
| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report |
|
||||
| [@lunkerchen](https://github.com/lunkerchen) | #8024 |
|
||||
| [@makcimbx](https://github.com/makcimbx) | #7692, #8171, #8432 |
|
||||
| [@Mananz90](https://github.com/Mananz90) | #8723 |
|
||||
| [@marceli1404](https://github.com/marceli1404) | #8570 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #8660 |
|
||||
| [@megamen32](https://github.com/megamen32) | #7313 |
|
||||
| [@MichaelYcJo](https://github.com/MichaelYcJo) | #8224 |
|
||||
| [@mikeiagents](https://github.com/mikeiagents) | #8598 |
|
||||
| [@MikeTuev](https://github.com/MikeTuev) | #6586 |
|
||||
| [@mikolaj92](https://github.com/mikolaj92) | #6973 |
|
||||
| [@MisileLab](https://github.com/MisileLab) | #8566 |
|
||||
| [@MonteNegroX](https://github.com/MonteNegroX) | #8217 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #7781, #8264 |
|
||||
| [@MrFadiAi](https://github.com/MrFadiAi) | #7073 |
|
||||
| [@mrprohack](https://github.com/mrprohack) | direct commit / report |
|
||||
| [@MumuTW](https://github.com/MumuTW) | #8423, #8425, #8426, #8427, #8428, #8524, #8534, #8544, #8545, #8546, #8547, #8548, #8554, #8582, #8585, #8589, #8592, #8604, #8605, #8612, #8619, #8657, #8690, #8741, #8749 |
|
||||
| [@mustafa-phd](https://github.com/mustafa-phd) | #7686 |
|
||||
| [@NBN-N3](https://github.com/NBN-N3) | #8794 |
|
||||
| [@nguyenha935](https://github.com/nguyenha935) | #7493, #7547, #7552, #7553, #7629, #7935, #8031, #8098, #8233, #8565 |
|
||||
| [@nguyenphi37](https://github.com/nguyenphi37) | direct commit / report |
|
||||
| [@not-knope](https://github.com/not-knope) | #8206 |
|
||||
| [@nramabad](https://github.com/nramabad) | #7926 |
|
||||
| [@oyi77](https://github.com/oyi77) | #6917, #6918, #6919, #6920, #6921, #6923, #7032, #7045, #7046, #7066, #7070, #7178, #7719, #7744, #7787, #7893, #8219 |
|
||||
| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791 |
|
||||
| [@professional-ALFIE](https://github.com/professional-ALFIE) | #6877 |
|
||||
| [@Prudhvivuda](https://github.com/Prudhvivuda) | #8032, #8220, #8250, #8309, #8441, #8467, #8488, #8493, #8494, #8495, #8586, #8587, #8606, #8607, #8608, #8611, #8805, #8806 |
|
||||
| [@QRcode1337](https://github.com/QRcode1337) | #7034 |
|
||||
| [@quanturbo](https://github.com/quanturbo) | #6780 |
|
||||
| [@rafaeldrincon](https://github.com/rafaeldrincon) | #8866, #8867 |
|
||||
| [@rafaumeu](https://github.com/rafaumeu) | #6813, #6979, #6982, #6983, #6987, #6988, #7001, #7808, #7815, #8071, #8113, #8179, #8184, #8185, #8190, #8195, #8196, #8203 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #7852, #7853, #7855, #7862, #7885, #7972, #7978, #8021, #8022, #8023, #8025, #8027, #8030, #8101, #8102, #8124, #8252, #8292, #8296, #8301, #8302, #8303, #8304, #8308 |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | #8151, #8378 |
|
||||
| [@ricatix](https://github.com/ricatix) | direct commit / report |
|
||||
| [@ridho9](https://github.com/ridho9) | #8310 |
|
||||
| [@rinseaid](https://github.com/rinseaid) | #8721, #8729, #8821 |
|
||||
| [@rixzkiye](https://github.com/rixzkiye) | direct commit / report |
|
||||
| [@rqzbeh](https://github.com/rqzbeh) | #8514, #8515 |
|
||||
| [@rushsinging](https://github.com/rushsinging) | #7256 |
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@samir-abis](https://github.com/samir-abis) | direct commit / report |
|
||||
| [@seanford](https://github.com/seanford) | #8194, #8218, #8232 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | #7063, #7264, #7294 |
|
||||
| [@Securiteru](https://github.com/Securiteru) | #7683 |
|
||||
| [@SemonCat](https://github.com/SemonCat) | direct commit / report |
|
||||
| [@shixi-li](https://github.com/shixi-li) | #8569 |
|
||||
| [@SingCJ](https://github.com/SingCJ) | direct commit / report |
|
||||
| [@skutanjir](https://github.com/skutanjir) | #7865, #7939 |
|
||||
| [@spacesky-cell](https://github.com/spacesky-cell) | direct commit / report |
|
||||
| [@SteeleHu](https://github.com/SteeleHu) | #8747 |
|
||||
| [@sumanxg](https://github.com/sumanxg) | #8837, #8856 |
|
||||
| [@swingtempo](https://github.com/swingtempo) | #7790, #8349, #8766 |
|
||||
| [@Tasogarre](https://github.com/Tasogarre) | #7861, #8207 |
|
||||
| [@techsolutionmta](https://github.com/techsolutionmta) | direct commit / report |
|
||||
| [@tenshiak](https://github.com/tenshiak) | #7274, #7643 |
|
||||
| [@terrafirmbot-source](https://github.com/terrafirmbot-source) | #8685 |
|
||||
| [@thepigdestroyer](https://github.com/thepigdestroyer) | #7497 |
|
||||
| [@tianrking](https://github.com/tianrking) | #7353 |
|
||||
| [@tientien17](https://github.com/tientien17) | #7841, #7844, #7925 |
|
||||
| [@TitoTFP](https://github.com/TitoTFP) | #8838 |
|
||||
| [@tjengbudi](https://github.com/tjengbudi) | #4009 |
|
||||
| [@tmone](https://github.com/tmone) | #7806, #7933 |
|
||||
| [@TrackCrewGalore](https://github.com/TrackCrewGalore) | #6271, #8128 |
|
||||
| [@trfi](https://github.com/trfi) | direct commit / report |
|
||||
| [@TuyulSpam](https://github.com/TuyulSpam) | direct commit / report |
|
||||
| [@ViFigueiredo](https://github.com/ViFigueiredo) | #7301 |
|
||||
| [@vzts](https://github.com/vzts) | #7390 |
|
||||
| [@warelik](https://github.com/warelik) | direct commit / report |
|
||||
| [@way-art](https://github.com/way-art) | direct commit / report |
|
||||
| [@webmasterarbez](https://github.com/webmasterarbez) | #7504 |
|
||||
| [@wgordon17](https://github.com/wgordon17) | #8852 |
|
||||
| [@whale9820](https://github.com/whale9820) | direct commit / report |
|
||||
| [@Wibias](https://github.com/Wibias) | #7125 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | direct commit / report |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790, #7901, #7902 |
|
||||
| [@XCrag](https://github.com/XCrag) | direct commit / report |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8122, #8261, #8262 |
|
||||
| [@xier2012](https://github.com/xier2012) | #7036, #7050, #7052, #7053, #7056, #7059, #7060, #7061, #7166, #7299 |
|
||||
| [@xxue-z](https://github.com/xxue-z) | #7098 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6714, #6727, #7004, #7012, #7027, #7673, #7700, #7747, #7776, #7843 |
|
||||
| [@yidecode](https://github.com/yidecode) | direct commit / report |
|
||||
| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@0x2f0](https://github.com/0x2f0) | #8679 |
|
||||
| [@0xheycat](https://github.com/0xheycat) | #8751 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #8123 |
|
||||
| [@adrianaryaputra](https://github.com/adrianaryaputra) | #7928 |
|
||||
| [@advane204f](https://github.com/advane204f) | direct commit / report |
|
||||
| [@Ajeesh25353646](https://github.com/Ajeesh25353646) | #7528 |
|
||||
| [@allanvb](https://github.com/allanvb) | #8471, #8759, #8814, #8862 |
|
||||
| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821, #7041, #7042, #7164, #7277, #7490, #7492, #7644 |
|
||||
| [@alvaretto](https://github.com/alvaretto) | #8077, #8161, #8170 |
|
||||
| [@amitgolan60-coder](https://github.com/amitgolan60-coder) | #8727 |
|
||||
| [@andrea-kingautomation](https://github.com/andrea-kingautomation) | #7678 |
|
||||
| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829, #7794, #7804, #7810, #7813, #7816, #7891, #8050 |
|
||||
| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report |
|
||||
| [@anndev-69](https://github.com/anndev-69) | direct commit / report |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | #8127 |
|
||||
| [@arpit-jaiswal-dev](https://github.com/arpit-jaiswal-dev) | #7881 |
|
||||
| [@artickc](https://github.com/artickc) | #6763, #6955, #7204, #7696, #7768, #7896, #7900, #7911, #7930, #7994, #8006, #8159, #8470, #8496, #8511, #8863 |
|
||||
| [@Arul-](https://github.com/Arul-) | #7878 |
|
||||
| [@asynx6](https://github.com/asynx6) | direct commit / report |
|
||||
| [@attid](https://github.com/attid) | #6984 |
|
||||
| [@backryun](https://github.com/backryun) | #6280, #6675, #6862, #7296, #7314, #7358, #7531, #7687, #7772, #7812, #7866, #7874, #7882, #7914, #8013, #8225, #8226, #8227, #8230, #8266, #8275, #8298, #8464, #8473, #8483, #8485, #8489, #8490, #8498, #8499, #8520, #8525, #8528, #8531, #8533, #8557, #8638, #8639, #8641, #8643, #8644, #8645, #8646, #8647, #8661, #8663, #8665, #8732, #8810, #8811, #8812, #8815, #8816, #8819, #8820, #8824 |
|
||||
| [@beingshafin](https://github.com/beingshafin) | direct commit / report |
|
||||
| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 |
|
||||
| [@brunnolouzada](https://github.com/brunnolouzada) | #8581 |
|
||||
| [@c4usal](https://github.com/c4usal) | #7627, #8129 |
|
||||
| [@CahyokPutraDev99](https://github.com/CahyokPutraDev99) | direct commit / report |
|
||||
| [@Capslockb](https://github.com/Capslockb) | #7892 |
|
||||
| [@CarmeloCampos](https://github.com/CarmeloCampos) | direct commit / report |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #7545 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6022, #6593, #6643, #6644, #6646, #6650, #6769, #6771, #6804, #7079, #7520, #8143 |
|
||||
| [@chitholian](https://github.com/chitholian) | direct commit / report |
|
||||
| [@chy1211](https://github.com/chy1211) | direct commit / report |
|
||||
| [@CitrusIce](https://github.com/CitrusIce) | #6937, #6938 |
|
||||
| [@costaeder](https://github.com/costaeder) | #8628 |
|
||||
| [@Dan-ex-hub](https://github.com/Dan-ex-hub) | #7743 |
|
||||
| [@danscMax](https://github.com/danscMax) | #7359, #7511, #7517, #7648, #7656, #7672, #7689 |
|
||||
| [@dependabot](https://github.com/dependabot) | #7897, #7898, #8235 |
|
||||
| [@Dingding-leo](https://github.com/Dingding-leo) | #7988, #7989, #8162, #8165, #8167, #8283, #8286, #8287, #8633, #8636, #8642, #8651, #8652, #8667, #8691, #8715, #8730, #8731, #8762, #8763, #8764, #8795 |
|
||||
| [@DinonowDev](https://github.com/DinonowDev) | #8552, #8559, #8583, #8596, #8615, #8709, #8710, #8716, #8798, #8800, #8802 |
|
||||
| [@dionisius95](https://github.com/dionisius95) | direct commit / report |
|
||||
| [@DKotsyuba](https://github.com/DKotsyuba) | #7500 |
|
||||
| [@dongwook-chan](https://github.com/dongwook-chan) | #7574, #7582, #7704 |
|
||||
| [@Dragost](https://github.com/Dragost) | #8289 |
|
||||
| [@Duongkhanhtool](https://github.com/Duongkhanhtool) | direct commit / report |
|
||||
| [@dvirarad](https://github.com/dvirarad) | #8410 |
|
||||
| [@ekinnee](https://github.com/ekinnee) | #7601, #7613, #7614, #7662, #7779, #7927, #7932, #7980, #8735 |
|
||||
| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647, #7863 |
|
||||
| [@epsilonode](https://github.com/epsilonode) | #8724 |
|
||||
| [@evinjohnn](https://github.com/evinjohnn) | direct commit / report |
|
||||
| [@fajarbossit](https://github.com/fajarbossit) | direct commit / report |
|
||||
| [@felipeleite](https://github.com/felipeleite) | direct commit / report |
|
||||
| [@fenix007](https://github.com/fenix007) | #7171, #7399, #8306, #8561 |
|
||||
| [@FenjuFu](https://github.com/FenjuFu) | #7942 |
|
||||
| [@floze-the-genius](https://github.com/floze-the-genius) | #7707 |
|
||||
| [@fontvu](https://github.com/fontvu) | direct commit / report |
|
||||
| [@fuko2935](https://github.com/fuko2935) | #8726 |
|
||||
| [@fzrilsh](https://github.com/fzrilsh) | direct commit / report |
|
||||
| [@gitcommit90](https://github.com/gitcommit90) | #6986 |
|
||||
| [@glazec](https://github.com/glazec) | #8632 |
|
||||
| [@growab](https://github.com/growab) | #7062, #7300 |
|
||||
| [@guanbear](https://github.com/guanbear) | #7028 |
|
||||
| [@guhcostan](https://github.com/guhcostan) | #8433 |
|
||||
| [@hartmark](https://github.com/hartmark) | #8208, #8209, #8210, #8211, #8212, #8213, #8337, #8341, #8354, #8462 |
|
||||
| [@HassiyYT](https://github.com/HassiyYT) | #7864 |
|
||||
| [@heishen6](https://github.com/heishen6) | direct commit / report |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #7612, #7625, #7633, #7869, #7871, #8476 |
|
||||
| [@HoneyTyagii](https://github.com/HoneyTyagii) | #8444 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #7035, #7129, #7290, #7398, #7408, #7973, #8290, #8312, #8842, #8845, #8860, #8865 |
|
||||
| [@hppsc1215](https://github.com/hppsc1215) | #7546, #8835 |
|
||||
| [@huohua-dev](https://github.com/huohua-dev) | #6794 |
|
||||
| [@hydraxman](https://github.com/hydraxman) | #7909 |
|
||||
| [@iamraydoan](https://github.com/iamraydoan) | #6798 |
|
||||
| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 |
|
||||
| [@ikelvingo](https://github.com/ikelvingo) | #8355 |
|
||||
| [@insoln](https://github.com/insoln) | #7906, #7908, #8041, #8054, #8062 |
|
||||
| [@irvandikky](https://github.com/irvandikky) | #7695 |
|
||||
| [@isiahw1](https://github.com/isiahw1) | #7555 |
|
||||
| [@itiwant](https://github.com/itiwant) | direct commit / report |
|
||||
| [@janeza2](https://github.com/janeza2) | #6308 |
|
||||
| [@Jordannst](https://github.com/Jordannst) | direct commit / report |
|
||||
| [@JoshimOfficial](https://github.com/JoshimOfficial) | #8526 |
|
||||
| [@justdoGIT](https://github.com/justdoGIT) | #8695 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | #6776, #6993, #7154, #7177, #7269, #7273, #7280, #7281, #7282, #7323, #7360, #7377, #7378, #7379, #7380, #7381, #7419, #7607, #7894, #7905, #7912, #8008, #8009, #8010 |
|
||||
| [@kamenkadmitry](https://github.com/kamenkadmitry) | #7350, #7425 |
|
||||
| [@kaon0388v1](https://github.com/kaon0388v1) | #7049 |
|
||||
| [@KaynXu](https://github.com/KaynXu) | #8284 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856, #7008, #7087, #7093, #7128, #7130, #7136, #7315, #7318, #7334, #7336 |
|
||||
| [@KunN-21](https://github.com/KunN-21) | direct commit / report |
|
||||
| [@leninejunior](https://github.com/leninejunior) | #8049 |
|
||||
| [@leszek3737](https://github.com/leszek3737) | #7782, #7807, #8343, #8543, #8823 |
|
||||
| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report |
|
||||
| [@linhdmn](https://github.com/linhdmn) | #8439 |
|
||||
| [@Long-Feeds](https://github.com/Long-Feeds) | #8011 |
|
||||
| [@loulanyue](https://github.com/loulanyue) | #7540 |
|
||||
| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report |
|
||||
| [@lunkerchen](https://github.com/lunkerchen) | #8024 |
|
||||
| [@makcimbx](https://github.com/makcimbx) | #7692, #8171, #8432 |
|
||||
| [@Mananz90](https://github.com/Mananz90) | #8723 |
|
||||
| [@marceli1404](https://github.com/marceli1404) | #8570 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #8660 |
|
||||
| [@megamen32](https://github.com/megamen32) | #7313 |
|
||||
| [@MichaelYcJo](https://github.com/MichaelYcJo) | #8224 |
|
||||
| [@mikeiagents](https://github.com/mikeiagents) | #8598 |
|
||||
| [@MikeTuev](https://github.com/MikeTuev) | #6586 |
|
||||
| [@mikolaj92](https://github.com/mikolaj92) | #6973 |
|
||||
| [@MisileLab](https://github.com/MisileLab) | #8566 |
|
||||
| [@MonteNegroX](https://github.com/MonteNegroX) | #8217 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #7781, #8264 |
|
||||
| [@MrFadiAi](https://github.com/MrFadiAi) | #7073 |
|
||||
| [@mrprohack](https://github.com/mrprohack) | direct commit / report |
|
||||
| [@MumuTW](https://github.com/MumuTW) | #8423, #8425, #8426, #8427, #8428, #8524, #8534, #8544, #8545, #8546, #8547, #8548, #8554, #8582, #8585, #8589, #8592, #8604, #8605, #8612, #8619, #8657, #8690, #8741, #8749 |
|
||||
| [@mustafa-phd](https://github.com/mustafa-phd) | #7686 |
|
||||
| [@NBN-N3](https://github.com/NBN-N3) | #8794 |
|
||||
| [@nguyenha935](https://github.com/nguyenha935) | #7493, #7547, #7552, #7553, #7629, #7935, #8031, #8098, #8233, #8565 |
|
||||
| [@nguyenphi37](https://github.com/nguyenphi37) | direct commit / report |
|
||||
| [@not-knope](https://github.com/not-knope) | #8206 |
|
||||
| [@nramabad](https://github.com/nramabad) | #7926 |
|
||||
| [@oyi77](https://github.com/oyi77) | #6917, #6918, #6919, #6920, #6921, #6923, #7032, #7045, #7046, #7066, #7070, #7178, #7719, #7744, #7787, #7893, #8219 |
|
||||
| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791 |
|
||||
| [@professional-ALFIE](https://github.com/professional-ALFIE) | #6877 |
|
||||
| [@Prudhvivuda](https://github.com/Prudhvivuda) | #8032, #8220, #8250, #8309, #8441, #8467, #8488, #8493, #8494, #8495, #8586, #8587, #8606, #8607, #8608, #8611, #8805, #8806 |
|
||||
| [@QRcode1337](https://github.com/QRcode1337) | #7034 |
|
||||
| [@quanturbo](https://github.com/quanturbo) | #6780 |
|
||||
| [@rafaeldrincon](https://github.com/rafaeldrincon) | #8866, #8867 |
|
||||
| [@rafaumeu](https://github.com/rafaumeu) | #6813, #6979, #6982, #6983, #6987, #6988, #7001, #7808, #7815, #8071, #8113, #8179, #8184, #8185, #8190, #8195, #8196, #8203 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #7852, #7853, #7855, #7862, #7885, #7972, #7978, #8021, #8022, #8023, #8025, #8027, #8030, #8101, #8102, #8124, #8252, #8292, #8296, #8301, #8302, #8303, #8304, #8308 |
|
||||
| [@RCrushMe](https://github.com/RCrushMe) | #8151, #8378 |
|
||||
| [@ricatix](https://github.com/ricatix) | direct commit / report |
|
||||
| [@ridho9](https://github.com/ridho9) | #8310 |
|
||||
| [@rinseaid](https://github.com/rinseaid) | #8721, #8729, #8821 |
|
||||
| [@rixzkiye](https://github.com/rixzkiye) | direct commit / report |
|
||||
| [@rqzbeh](https://github.com/rqzbeh) | #8514, #8515 |
|
||||
| [@rushsinging](https://github.com/rushsinging) | #7256 |
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@samir-abis](https://github.com/samir-abis) | direct commit / report |
|
||||
| [@seanford](https://github.com/seanford) | #8194, #8218, #8232 |
|
||||
| [@SeaXen](https://github.com/SeaXen) | #7063, #7264, #7294 |
|
||||
| [@Securiteru](https://github.com/Securiteru) | #7683 |
|
||||
| [@SemonCat](https://github.com/SemonCat) | direct commit / report |
|
||||
| [@shixi-li](https://github.com/shixi-li) | #8569 |
|
||||
| [@SingCJ](https://github.com/SingCJ) | direct commit / report |
|
||||
| [@skutanjir](https://github.com/skutanjir) | #7865, #7939 |
|
||||
| [@spacesky-cell](https://github.com/spacesky-cell) | direct commit / report |
|
||||
| [@SteeleHu](https://github.com/SteeleHu) | #8747 |
|
||||
| [@sumanxg](https://github.com/sumanxg) | #8837, #8856 |
|
||||
| [@swingtempo](https://github.com/swingtempo) | #7790, #8349, #8766 |
|
||||
| [@Tasogarre](https://github.com/Tasogarre) | #7861, #8207 |
|
||||
| [@techsolutionmta](https://github.com/techsolutionmta) | direct commit / report |
|
||||
| [@tenshiak](https://github.com/tenshiak) | #7274, #7643 |
|
||||
| [@terrafirmbot-source](https://github.com/terrafirmbot-source) | #8685 |
|
||||
| [@thepigdestroyer](https://github.com/thepigdestroyer) | #7497 |
|
||||
| [@tianrking](https://github.com/tianrking) | #7353 |
|
||||
| [@tientien17](https://github.com/tientien17) | #7841, #7844, #7925 |
|
||||
| [@TitoTFP](https://github.com/TitoTFP) | #8838 |
|
||||
| [@tjengbudi](https://github.com/tjengbudi) | #4009 |
|
||||
| [@tmone](https://github.com/tmone) | #7806, #7933 |
|
||||
| [@TrackCrewGalore](https://github.com/TrackCrewGalore) | #6271, #8128 |
|
||||
| [@trfi](https://github.com/trfi) | direct commit / report |
|
||||
| [@TuyulSpam](https://github.com/TuyulSpam) | direct commit / report |
|
||||
| [@ViFigueiredo](https://github.com/ViFigueiredo) | #7301 |
|
||||
| [@vzts](https://github.com/vzts) | #7390 |
|
||||
| [@warelik](https://github.com/warelik) | direct commit / report |
|
||||
| [@way-art](https://github.com/way-art) | direct commit / report |
|
||||
| [@webmasterarbez](https://github.com/webmasterarbez) | #7504 |
|
||||
| [@wgordon17](https://github.com/wgordon17) | #8852 |
|
||||
| [@whale9820](https://github.com/whale9820) | direct commit / report |
|
||||
| [@Wibias](https://github.com/Wibias) | #7125 |
|
||||
| [@wilsonicdev](https://github.com/wilsonicdev) | direct commit / report |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790, #7901, #7902 |
|
||||
| [@XCrag](https://github.com/XCrag) | direct commit / report |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8122, #8261, #8262 |
|
||||
| [@xier2012](https://github.com/xier2012) | #7036, #7050, #7052, #7053, #7056, #7059, #7060, #7061, #7166, #7299 |
|
||||
| [@xxue-z](https://github.com/xxue-z) | #7098 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6714, #6727, #7004, #7012, #7027, #7673, #7700, #7747, #7776, #7843 |
|
||||
| [@yidecode](https://github.com/yidecode) | direct commit / report |
|
||||
| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -63,8 +63,8 @@
|
||||
|
||||
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
|
||||
| ------------------------- | :-----: | :---------: | :---------: |
|
||||
| 🌐 Providers | 290 | **339** | more queued |
|
||||
| 🧠 Documented models | 1185 | **1202** | — |
|
||||
| 🌐 Providers | 291 | **339** | more queued |
|
||||
| 🧠 Documented models | 500+ | **1200+** | — |
|
||||
| 🖼️ Modality Bridge | — | 🆕 vision | video |
|
||||
| 📡 Radar free catalog | — | — | 🔭 next |
|
||||
| ⚖️ Quota-aware scheduling | — | — | 🔭 next |
|
||||
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 339 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 339 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 105 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 339 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 339 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 105 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -449,7 +449,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
### 🧱 Resilience is built in (3 independent layers)
|
||||
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 10× / API-key 15× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 3× / API-key 5× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
|
||||
<sub>📖 [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)</sub>
|
||||
|
||||
@@ -603,7 +603,7 @@ Pix copia-e-cola:
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **339 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
> The most complete catalog of any open-source router: **339 providers**, **90+ with a free tier**, **40+ free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1061,7 +1061,7 @@ same process on one port, so there is no separate CLI-only package today.
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 117 domain modules, 145 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 95 domain modules, 145 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 339 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 339 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 105 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 339 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 339 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 40+ free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 105 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
|
||||
<desc>Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.</desc>
|
||||
<defs>
|
||||
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
@@ -73,7 +73,7 @@
|
||||
<circle cx="6.6" cy="6.6" r="1.4" fill="#fdcb6e" stroke="none"/>
|
||||
</g>
|
||||
<text x="862" y="170" font-size="18" font-weight="800" fill="#fdcb6e">$0 to start</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 56 free</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 40+ free</text>
|
||||
<text x="826" y="226" font-size="13.5" fill="#a1a1aa">forever — Qoder, Pollinations, Cloudflare,</text>
|
||||
<text x="826" y="248" font-size="13.5" fill="#a1a1aa">SiliconFlow… No card needed.</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -1195,8 +1195,8 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `TAILSCALED_BIN` | _(auto-detect)_ | `src/lib/tailscaleTunnel.ts` | Explicit path to the `tailscaled` daemon binary. |
|
||||
| `TAILSCALE_AUTHKEY` | _(unset)_ | `src/lib/tailscaleTunnel.ts` | Pre-shared Tailscale auth key for non-interactive / headless `tailscale up` (passed via `--auth-key=`). When unset, login falls back to the interactive browser auth URL. |
|
||||
| `NGROK_AUTHTOKEN` | _(unset)_ | `src/lib/ngrokTunnel.ts` | Authenticates outbound ngrok tunnels. |
|
||||
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | Maximum SQLite backup files retained on disk. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts`, `src/lib/db/migrationRunner.ts` | Maximum SQLite backup files retained on disk. Applies to manual/scheduled backups and to pre-migration snapshots. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts`, `src/lib/db/migrationRunner.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. Applies to manual/scheduled backups and to pre-migration snapshots. Overrides the value saved from Settings → Database backup retention. |
|
||||
| `OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS` | `30000` | `src/lib/jobs/backupScheduleJob.ts` | Tick interval (ms) of the server-side job that executes `backup-schedule.json`. Must stay well under the 1-minute cron granularity; values below `5000` or unparseable fall back to `30000`. |
|
||||
| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. |
|
||||
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | Container runtime hint for the entrypoint permission check. Set to `podman` for any Podman topology. Because the container cannot determine whether the engine is local or reached through Podman Machine, the warning stays topology-neutral and points to `contrib/podman/README.md`. |
|
||||
|
||||
@@ -117,6 +117,21 @@ export const OUTPUT_STYLE_CATALOG: Record<string, OutputStyle> = {
|
||||
full: `# Eu tenho TDAH — saída action-first\n\nO leitor tem TDAH. Molde a saída para que um cérebro TDAH consiga agir sobre ela:\n1. Comece pela próxima ação — comando, path ou snippet primeiro; contexto depois, se necessário.\n2. Numere trabalho multi-passo; cada passo é uma ação delimitada; use o menor número de passos que funcione.\n3. Termine com UMA próxima ação concreta executável em menos de dois minutos.\n4. Suprima tangentes: termine a primeira questão, ofereça a segunda como pergunta separada.\n5. Em trabalho multi-turno, reafirme onde as coisas estão ("passo 3 de 5 feito") — o leitor não guarda estado entre mensagens.\n6. Quando houver esforço humano, estime em unidades concretas (minutos, uma tarde), nunca "um pouco de trabalho".\n7. Torne vitórias visíveis: diga o que funciona agora e como testar.\n8. Erros de forma direta: causa e fix; nunca "Opa!".\n9. Listas com no máximo 5 itens; acima disso, divida em "agora" vs "depois".\n10. Sem preâmbulo, sem recap, sem despedidas ("Espero ter ajudado").\nExceções: pedido explícito de "explique" recebe corpo completo (ainda sem preâmbulo/despedida); ações destrutivas recebem confirmação antes; ambiguidade real recebe uma pergunta curta de esclarecimento. ${SHARED_BOUNDARIES}`,
|
||||
ultra: `# Eu tenho TDAH (ultra)\nAção primeiro: comando/path/snippet, prosa depois se precisar. Passos numerados e delimitados, o mínimo que funcione. UMA próxima ação <2 min no fim. Sem tangentes — pergunta separada. Multi-turno: reafirme o estado. Esforço humano: unidades concretas de tempo. Vitórias visíveis. Erros: causa + fix. Listas ≤5. Zero preâmbulo/recap/despedidas. "Explique" recebe corpo completo; ação destrutiva recebe confirmação; ambiguidade real recebe uma pergunta. ${SHARED_BOUNDARIES}`,
|
||||
},
|
||||
vi: {
|
||||
lite: `# Tôi bị ADHD (rút gọn)\nBắt đầu bằng hành động: lệnh, đường dẫn hoặc đoạn mã trước, văn xuôi sau. Đánh số công việc nhiều bước; mỗi bước là một hành động giới hạn. Kết thúc bằng MỘT hành động cụ thể tiếp theo. Không mở đầu, không tóm tắt lại, không lời chào cuối. ${SHARED_BOUNDARIES}`,
|
||||
full: `# Tôi bị ADHD — đầu ra ưu tiên hành động\n\nNgười đọc bị ADHD. Hãy định hình đầu ra để một bộ não ADHD có thể hành động ngay:\n1. Mở đầu bằng hành động kế tiếp — lệnh, đường dẫn hoặc đoạn mã trước; ngữ cảnh sau, nếu cần.\n2. Đánh số công việc nhiều bước; mỗi bước là một hành động giới hạn; dùng ít bước nhất mà vẫn chạy được.\n3. Kết thúc bằng MỘT hành động cụ thể làm được dưới hai phút.\n4. Chặn lạc đề: xong việc thứ nhất, việc thứ hai đưa ra thành câu hỏi riêng.\n5. Trong công việc nhiều lượt, nhắc lại đang ở đâu ("xong bước 3 trên 5") — người đọc không giữ trạng thái giữa các tin nhắn.\n6. Khi có công sức của con người, ước lượng bằng đơn vị cụ thể (phút, một buổi chiều), không bao giờ nói "hơi tốn công".\n7. Cho thấy kết quả: nói rõ cái gì đã chạy được và thử thế nào.\n8. Báo lỗi thẳng thắn: nguyên nhân và cách sửa; không "Ôi không".\n9. Danh sách tối đa 5 mục; nhiều hơn thì tách "làm ngay" và "để sau".\n10. Không mở đầu, không tóm tắt lại, không lời chào cuối ("Hy vọng giúp ích").\nNgoại lệ: yêu cầu "giải thích" thì viết đầy đủ (vẫn không mở đầu/chào cuối); hành động phá huỷ phải xác nhận trước; mơ hồ thật sự thì hỏi một câu ngắn. ${SHARED_BOUNDARIES}`,
|
||||
ultra: `# Tôi bị ADHD (siêu gọn)\nHành động trước: lệnh/đường dẫn/đoạn mã, văn xuôi sau nếu cần. Bước đánh số, giới hạn, ít nhất có thể. MỘT hành động <2 phút ở cuối. Không lạc đề — hỏi riêng. Nhiều lượt: nhắc lại trạng thái. Công sức người: đơn vị thời gian cụ thể. Kết quả rõ ràng. Lỗi: nguyên nhân + cách sửa. Danh sách ≤5. Không mở đầu/tóm tắt/chào cuối. "Giải thích" thì viết đầy đủ; hành động phá huỷ phải xác nhận; mơ hồ thật thì hỏi một câu. ${SHARED_BOUNDARIES}`,
|
||||
},
|
||||
ja: {
|
||||
lite: `# ADHDです(軽量)\n行動から始める:コマンド、パス、スニペットを先に、散文は後。複数手順は番号付き;各手順は一つの区切られた行動。最後は具体的な次の行動を一つ。前置きなし、要約の繰り返しなし、締めの挨拶なし。${SHARED_BOUNDARIES}`,
|
||||
full: `# ADHDです — 行動優先の出力\n\n読み手はADHDです。ADHDの脳が動けるように出力を整えること:\n1. 次の行動から始める — コマンド、パス、スニペットを先に;文脈は必要なら後。\n2. 複数手順は番号付き;各手順は一つの区切られた行動;動く最小の手順数で。\n3. 最後は2分以内でできる具体的な次の行動を一つ。\n4. 脱線を抑える:最初の件を終えてから、二件目は別の質問として出す。\n5. 複数ターンの作業では現在地を言い直す(「5つ中3つ完了」)— 読み手はメッセージ間で状態を保持できない。\n6. 人手がかかる場合は具体的な単位で見積もる(分、半日)。「少し手間」は禁止。\n7. 成果を見せる:今何が動くか、どう試すかを述べる。\n8. エラーは淡々と:原因と対処;「おっと」は禁止。\n9. リストは5項目まで;超えるなら「今やる」と「後で」に分ける。\n10. 前置きなし、要約の繰り返しなし、締めの挨拶なし(「お役に立てば幸いです」)。\n例外:明示的な「説明して」には本文を十分に書く(前置き・締めはなし);破壊的操作は先に確認;本当に曖昧なら短い確認質問を一つ。${SHARED_BOUNDARIES}`,
|
||||
ultra: `# ADHDです(超軽量)\n行動優先:コマンド/パス/スニペット、必要なら散文。番号付きの区切られた手順、動く最小限。最後に2分未満の次の行動を一つ。脱線なし — 別の質問へ。複数ターン:状態を言い直す。人手:具体的な時間単位。成果を明示。エラー:原因+対処。リストは5まで。前置き/要約/締めの挨拶はゼロ。「説明して」には本文を十分に;破壊的操作は確認;本当の曖昧さには質問を一つ。${SHARED_BOUNDARIES}`,
|
||||
},
|
||||
id: {
|
||||
lite: `# Saya punya ADHD (ringkas)\nMulai dari aksi: perintah, path, atau cuplikan kode dulu, prosa belakangan. Beri nomor untuk pekerjaan banyak langkah; tiap langkah satu aksi yang terbatas. Akhiri dengan SATU langkah berikutnya yang konkret. Tanpa pembuka, tanpa rekap, tanpa basa-basi penutup. ${SHARED_BOUNDARIES}`,
|
||||
full: `# Saya punya ADHD — keluaran yang mengutamakan aksi\n\nPembaca punya ADHD. Bentuk keluaran supaya otak ADHD bisa langsung bertindak:\n1. Mulai dari aksi berikutnya — perintah, path, atau cuplikan kode dulu; konteks belakangan, kalau perlu.\n2. Beri nomor untuk pekerjaan banyak langkah; tiap langkah satu aksi terbatas; pakai langkah sesedikit mungkin yang tetap jalan.\n3. Akhiri dengan SATU langkah konkret yang bisa dikerjakan di bawah dua menit.\n4. Tahan bahasan sampingan: selesaikan yang pertama, tawarkan yang kedua sebagai pertanyaan terpisah.\n5. Pada pekerjaan banyak giliran, ulangi posisi saat ini ("langkah 3 dari 5 selesai") — pembaca tidak menyimpan status antar pesan.\n6. Kalau ada usaha manusia, perkirakan dalam satuan konkret (menit, satu sore), jangan "agak butuh kerja".\n7. Tunjukkan hasil: sebutkan apa yang sekarang jalan dan cara mencobanya.\n8. Error apa adanya: sebab dan perbaikannya; jangan "Waduh".\n9. Daftar maksimal 5 butir; lebih dari itu pisahkan "sekarang" dan "nanti".\n10. Tanpa pembuka, tanpa rekap, tanpa basa-basi penutup ("Semoga membantu").\nPengecualian: permintaan eksplisit "jelaskan" dapat isi penuh (tetap tanpa pembuka/penutup); aksi merusak dikonfirmasi dulu; ambiguitas nyata dapat satu pertanyaan singkat. ${SHARED_BOUNDARIES}`,
|
||||
ultra: `# Saya punya ADHD (ultra)\nAksi dulu: perintah/path/cuplikan, prosa kalau perlu. Langkah bernomor dan terbatas, sesedikit mungkin. SATU langkah <2 menit di akhir. Tanpa bahasan sampingan — jadikan pertanyaan terpisah. Banyak giliran: ulangi status. Usaha manusia: satuan waktu konkret. Hasil terlihat. Error: sebab + perbaikan. Daftar ≤5. Nol pembuka/rekap/penutup. "Jelaskan" dapat isi penuh; aksi merusak dikonfirmasi; ambiguitas nyata dapat satu pertanyaan. ${SHARED_BOUNDARIES}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"terse-cjk": {
|
||||
|
||||
@@ -42,6 +42,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
|
||||
"accessTokens", // intentionally-internal: 4 rotas /api/cli/* (connect, whoami, tokens, tokens/[id]) + server/authz/accessTokenAuth.ts via import direto "@/lib/db/accessTokens" (Rule #2)
|
||||
"apiKeyColumnFallbacks", // db-internal: importado só por db/apiKeys.ts (API_KEY_COLUMN_FALLBACKS — fallbacks de coluna split do apiKeys.ts)
|
||||
"apiKeyUsageLimitFields", // db-internal: importado só por db/apiKeys.ts (helpers de campo de limite de uso split do apiKeys.ts; mig 101)
|
||||
"backupRetention", // db-internal: importado só por db/backup.ts e db/migrationRunner.ts (política de retenção compartilhada; mora fora de backup.ts porque core.ts importa migrationRunner.ts — importar backup.ts de lá fecharia um ciclo, #10421)
|
||||
"caseMapping", // db-internal: importado só por db/core.ts (toSnakeCase/toCamelCase/objToSnake — column-mapping snake↔camel split do core.ts, #4947)
|
||||
"cleanup", // intentionally-internal: 3 API routes (purge-quota-snapshots, purge-call-logs, purge-detailed-logs)
|
||||
"cliToolState", // intentionally-internal: 14+ API routes em /api/cli-tools/*-settings
|
||||
|
||||
@@ -175,15 +175,10 @@ function readCodeFacts() {
|
||||
"for(const x of (t?.scopes||[]))sc.add(x);",
|
||||
"const t=computeFreeModelTotals();const cli=Object.values(CLI_TOOLS);",
|
||||
"const by=(c)=>cli.filter(x=>x.category===c).length;",
|
||||
// "Free forever" = every provider whose free access renews or needs no key at all.
|
||||
// one-time-initial (signup credits) and discontinued pools are excluded on purpose.
|
||||
"const FOREVER=new Set(['recurring-monthly','recurring-daily','recurring-uncapped',",
|
||||
"'recurring-credit','keyless']);",
|
||||
"const ff=new Set();for(const m of t.perModel)if(FOREVER.has(m.freeType))ff.add(m.provider);",
|
||||
'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,',
|
||||
"freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,",
|
||||
"cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),",
|
||||
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size,freeForever:ff.size}));",
|
||||
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size}));",
|
||||
].join("");
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-counts-"));
|
||||
try {
|
||||
@@ -465,10 +460,6 @@ export function buildChecks() {
|
||||
),
|
||||
claim(f.mcpScopes, "MCP scopes", { pattern: /(\d+) scopes/gi }, ["README.md", "AGENTS.md"]),
|
||||
claim(f.cliTotal, "CLI tools", { pattern: /(\d+) tools(?=\s*\(\d+ CLI)/gi }, ["README.md"]),
|
||||
claim(f.freeForever, "free-forever providers", { pattern: /(\d+) free forever/gi }, [
|
||||
"README.md",
|
||||
"docs/diagrams/promise-pillars.svg",
|
||||
]),
|
||||
];
|
||||
})(),
|
||||
{
|
||||
|
||||
@@ -14,6 +14,13 @@ import {
|
||||
DATA_DIR,
|
||||
} from "./core";
|
||||
import { resetAllDbModuleState } from "./stateReset";
|
||||
import {
|
||||
MAX_DB_BACKUPS,
|
||||
DEFAULT_DB_BACKUP_RETENTION_DAYS,
|
||||
parsePositiveInt,
|
||||
parseNonNegativeInt,
|
||||
pruneBackupDirectory,
|
||||
} from "./backupRetention";
|
||||
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
|
||||
|
||||
type CountRow = { cnt?: number };
|
||||
@@ -22,22 +29,8 @@ type CountRow = { cnt?: number };
|
||||
|
||||
let _lastBackupAt = 0;
|
||||
const BACKUP_THROTTLE_MS = 60 * 60 * 1000; // 60 minutes
|
||||
const MAX_DB_BACKUPS = 20;
|
||||
const DEFAULT_DB_BACKUP_RETENTION_DAYS = 0;
|
||||
const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
|
||||
function parsePositiveInt(value: string | undefined, fallback: number) {
|
||||
if (!value) return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseNonNegativeInt(value: string | undefined, fallback: number) {
|
||||
if (value === undefined) return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
// #3834: the "Keep latest backups" UI value is persisted here so it survives a page
|
||||
// refresh / the loadStorageHealth() refetch. A dedicated namespace avoids any
|
||||
// cross-talk with the databaseSettings key_value store (which rewrites all of its own
|
||||
@@ -108,108 +101,16 @@ function getBackupDir() {
|
||||
return DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups");
|
||||
}
|
||||
|
||||
function getBackupFamilyBase(filename: string) {
|
||||
if (filename.endsWith("-wal") || filename.endsWith("-shm")) return filename.slice(0, -4);
|
||||
if (filename.endsWith("-journal")) return filename.slice(0, -8);
|
||||
return filename;
|
||||
}
|
||||
export function cleanupDbBackups(options?: {
|
||||
maxFiles?: number;
|
||||
retentionDays?: number;
|
||||
backupDir?: string;
|
||||
}) {
|
||||
const backupDir = options?.backupDir ?? getBackupDir();
|
||||
const maxFiles = options?.maxFiles ?? getDbBackupMaxFiles();
|
||||
const retentionDays = options?.retentionDays ?? getDbBackupRetentionDays();
|
||||
|
||||
function collectBackupFamilies(backupDir: string) {
|
||||
if (!fs.existsSync(backupDir)) return [];
|
||||
|
||||
const families = new Map<
|
||||
string,
|
||||
{
|
||||
base: string;
|
||||
hasPrimary: boolean;
|
||||
primaryMtimeMs: number;
|
||||
latestMtimeMs: number;
|
||||
files: string[];
|
||||
}
|
||||
>();
|
||||
|
||||
for (const name of fs.readdirSync(backupDir)) {
|
||||
if (!name.startsWith("db_")) continue;
|
||||
const base = getBackupFamilyBase(name);
|
||||
const filePath = path.join(backupDir, name);
|
||||
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.statSync(filePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const family = families.get(base) || {
|
||||
base,
|
||||
hasPrimary: false,
|
||||
primaryMtimeMs: 0,
|
||||
latestMtimeMs: 0,
|
||||
files: [],
|
||||
};
|
||||
|
||||
family.files.push(name);
|
||||
family.latestMtimeMs = Math.max(family.latestMtimeMs, stat.mtimeMs);
|
||||
if (name === base && name.endsWith(".sqlite")) {
|
||||
family.hasPrimary = true;
|
||||
family.primaryMtimeMs = stat.mtimeMs;
|
||||
}
|
||||
|
||||
families.set(base, family);
|
||||
}
|
||||
|
||||
return [...families.values()];
|
||||
}
|
||||
|
||||
export function cleanupDbBackups(options?: { maxFiles?: number; retentionDays?: number }) {
|
||||
const backupDir = getBackupDir();
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
return {
|
||||
deletedBackupFamilies: 0,
|
||||
deletedFiles: 0,
|
||||
keptBackupFamilies: 0,
|
||||
maxFiles: options?.maxFiles ?? getDbBackupMaxFiles(),
|
||||
retentionDays: options?.retentionDays ?? getDbBackupRetentionDays(),
|
||||
};
|
||||
}
|
||||
|
||||
const maxFiles = Math.max(1, options?.maxFiles ?? getDbBackupMaxFiles());
|
||||
const retentionDays = Math.max(0, options?.retentionDays ?? getDbBackupRetentionDays());
|
||||
const cutoffMs = retentionDays > 0 ? Date.now() - retentionDays * 24 * 60 * 60 * 1000 : 0;
|
||||
const families = collectBackupFamilies(backupDir);
|
||||
const primaryFamilies = families
|
||||
.filter((family) => family.hasPrimary)
|
||||
.sort((a, b) => b.primaryMtimeMs - a.primaryMtimeMs);
|
||||
const keepPrimaryBases = new Set(primaryFamilies.slice(0, maxFiles).map((family) => family.base));
|
||||
|
||||
let deletedBackupFamilies = 0;
|
||||
let deletedFiles = 0;
|
||||
|
||||
for (const family of families) {
|
||||
const isOverflowPrimary = family.hasPrimary && !keepPrimaryBases.has(family.base);
|
||||
const isExpired = retentionDays > 0 && family.latestMtimeMs < cutoffMs;
|
||||
const isOrphan = !family.hasPrimary;
|
||||
if (!isOverflowPrimary && !isExpired && !isOrphan) continue;
|
||||
|
||||
deletedBackupFamilies += 1;
|
||||
for (const name of family.files) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(backupDir, name));
|
||||
deletedFiles += 1;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deletedBackupFamilies,
|
||||
deletedFiles,
|
||||
keptBackupFamilies: collectBackupFamilies(backupDir).filter((family) => family.hasPrimary)
|
||||
.length,
|
||||
maxFiles,
|
||||
retentionDays,
|
||||
};
|
||||
return pruneBackupDirectory({ backupDir, maxFiles, retentionDays });
|
||||
}
|
||||
|
||||
function coerceBoolean(value: unknown): boolean | null {
|
||||
|
||||
158
src/lib/db/backupRetention.ts
Normal file
158
src/lib/db/backupRetention.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Backup retention primitives — pure filesystem work, no `core.ts` dependency.
|
||||
*
|
||||
* This module exists so BOTH backup call sites can share one retention policy:
|
||||
*
|
||||
* - `backup.ts` (manual/API/auto backups) — resolves the operator's settings from the
|
||||
* database and delegates here.
|
||||
* - `migrationRunner.ts` (pre-migration snapshots) — cannot import `backup.ts`, because
|
||||
* `core.ts` already imports `migrationRunner.ts` and `backup.ts` imports `core.ts`;
|
||||
* that edge would close a cycle. Keeping the policy here, free of `core`, lets the
|
||||
* migration path prune without one.
|
||||
*
|
||||
* Before #10421 the migration path had no retention at all and `db_backups/` grew
|
||||
* without bound (observed: 48.999 files / 204 GB against a 5,3 MB live database).
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export const MAX_DB_BACKUPS = 20;
|
||||
export const DEFAULT_DB_BACKUP_RETENTION_DAYS = 0;
|
||||
|
||||
export function parsePositiveInt(value: string | undefined, fallback: number) {
|
||||
if (!value) return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
export function parseNonNegativeInt(value: string | undefined, fallback: number) {
|
||||
if (value === undefined) return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* A backup "family" is the primary `.sqlite` file plus its SQLite sidecars
|
||||
* (`-wal` / `-shm` / `-journal`). Retention operates on families so a sidecar is never
|
||||
* orphaned from — or outlives — the snapshot it belongs to.
|
||||
*/
|
||||
export function getBackupFamilyBase(filename: string) {
|
||||
if (filename.endsWith("-wal") || filename.endsWith("-shm")) return filename.slice(0, -4);
|
||||
if (filename.endsWith("-journal")) return filename.slice(0, -8);
|
||||
return filename;
|
||||
}
|
||||
|
||||
export type BackupFamily = {
|
||||
base: string;
|
||||
hasPrimary: boolean;
|
||||
primaryMtimeMs: number;
|
||||
latestMtimeMs: number;
|
||||
files: string[];
|
||||
};
|
||||
|
||||
export function collectBackupFamilies(backupDir: string): BackupFamily[] {
|
||||
if (!fs.existsSync(backupDir)) return [];
|
||||
|
||||
const families = new Map<string, BackupFamily>();
|
||||
|
||||
for (const name of fs.readdirSync(backupDir)) {
|
||||
if (!name.startsWith("db_")) continue;
|
||||
const base = getBackupFamilyBase(name);
|
||||
const filePath = path.join(backupDir, name);
|
||||
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.statSync(filePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const family = families.get(base) || {
|
||||
base,
|
||||
hasPrimary: false,
|
||||
primaryMtimeMs: 0,
|
||||
latestMtimeMs: 0,
|
||||
files: [],
|
||||
};
|
||||
|
||||
family.files.push(name);
|
||||
family.latestMtimeMs = Math.max(family.latestMtimeMs, stat.mtimeMs);
|
||||
if (name === base && name.endsWith(".sqlite")) {
|
||||
family.hasPrimary = true;
|
||||
family.primaryMtimeMs = stat.mtimeMs;
|
||||
}
|
||||
|
||||
families.set(base, family);
|
||||
}
|
||||
|
||||
return [...families.values()];
|
||||
}
|
||||
|
||||
export type PruneResult = {
|
||||
deletedBackupFamilies: number;
|
||||
deletedFiles: number;
|
||||
keptBackupFamilies: number;
|
||||
maxFiles: number;
|
||||
retentionDays: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete backup families beyond `maxFiles` (newest kept), older than `retentionDays`
|
||||
* (0 disables the age rule), or orphaned (sidecars whose primary is already gone).
|
||||
*/
|
||||
export function pruneBackupDirectory(options: {
|
||||
backupDir: string;
|
||||
maxFiles: number;
|
||||
retentionDays: number;
|
||||
}): PruneResult {
|
||||
const { backupDir } = options;
|
||||
const maxFiles = Math.max(1, options.maxFiles);
|
||||
const retentionDays = Math.max(0, options.retentionDays);
|
||||
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
return {
|
||||
deletedBackupFamilies: 0,
|
||||
deletedFiles: 0,
|
||||
keptBackupFamilies: 0,
|
||||
maxFiles,
|
||||
retentionDays,
|
||||
};
|
||||
}
|
||||
|
||||
const cutoffMs = retentionDays > 0 ? Date.now() - retentionDays * 24 * 60 * 60 * 1000 : 0;
|
||||
const families = collectBackupFamilies(backupDir);
|
||||
const primaryFamilies = families
|
||||
.filter((family) => family.hasPrimary)
|
||||
.sort((a, b) => b.primaryMtimeMs - a.primaryMtimeMs);
|
||||
const keepPrimaryBases = new Set(primaryFamilies.slice(0, maxFiles).map((family) => family.base));
|
||||
|
||||
let deletedBackupFamilies = 0;
|
||||
let deletedFiles = 0;
|
||||
|
||||
for (const family of families) {
|
||||
const isOverflowPrimary = family.hasPrimary && !keepPrimaryBases.has(family.base);
|
||||
const isExpired = retentionDays > 0 && family.latestMtimeMs < cutoffMs;
|
||||
const isOrphan = !family.hasPrimary;
|
||||
if (!isOverflowPrimary && !isExpired && !isOrphan) continue;
|
||||
|
||||
deletedBackupFamilies += 1;
|
||||
for (const name of family.files) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(backupDir, name));
|
||||
deletedFiles += 1;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deletedBackupFamilies,
|
||||
deletedFiles,
|
||||
keptBackupFamilies: collectBackupFamilies(backupDir).filter((family) => family.hasPrimary)
|
||||
.length,
|
||||
maxFiles,
|
||||
retentionDays,
|
||||
};
|
||||
}
|
||||
@@ -29,6 +29,15 @@ import {
|
||||
OPTIONAL_FTS5_MIGRATION_VERSIONS,
|
||||
} from "./migrationRunner/constants";
|
||||
import { getExtraMigrationFiles } from "./migrationRunner/extraDirs";
|
||||
// Retention primitives live in their own `core`-free module: `core.ts` imports this file,
|
||||
// so importing `backup.ts` (which imports `core.ts`) here would close a dependency cycle.
|
||||
import {
|
||||
MAX_DB_BACKUPS,
|
||||
DEFAULT_DB_BACKUP_RETENTION_DAYS,
|
||||
parsePositiveInt,
|
||||
parseNonNegativeInt,
|
||||
pruneBackupDirectory,
|
||||
} from "./backupRetention";
|
||||
|
||||
const isNodeTestRunnerChild = typeof process.env.NODE_TEST_CONTEXT === "string";
|
||||
|
||||
@@ -805,6 +814,56 @@ function rehomeLegacyVersionSlotMigrations(
|
||||
return repaired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a persisted `dbBackup` retention setting through the adapter that is ALREADY open
|
||||
* for this migration run.
|
||||
*
|
||||
* `backup.ts`'s equivalent goes through `getDbInstance()`, which is unsafe here: this
|
||||
* code runs from inside database initialization, so asking for the singleton would
|
||||
* re-enter it. Reading off `db` keeps the same stored values without that risk. A DB too
|
||||
* old to have `key_value` yet simply falls back to the default.
|
||||
*/
|
||||
function readStoredBackupSetting(db: SqliteAdapter, key: string, min: number): number | undefined {
|
||||
try {
|
||||
const row = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
|
||||
.get("dbBackup", key) as { value?: string } | undefined;
|
||||
if (!row?.value) return undefined;
|
||||
const parsed = JSON.parse(row.value);
|
||||
return Number.isInteger(parsed) && parsed >= min ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce the backup retention budget after a pre-migration snapshot (#10421).
|
||||
*
|
||||
* Precedence matches `backup.ts`: env override → persisted operator setting → default.
|
||||
* Never throws: a migration must not fail because housekeeping did.
|
||||
*/
|
||||
function pruneMigrationBackups(db: SqliteAdapter, backupDir: string): void {
|
||||
try {
|
||||
const maxFiles = process.env.DB_BACKUP_MAX_FILES
|
||||
? parsePositiveInt(process.env.DB_BACKUP_MAX_FILES, MAX_DB_BACKUPS)
|
||||
: (readStoredBackupSetting(db, "maxFiles", 1) ?? MAX_DB_BACKUPS);
|
||||
const retentionDays = process.env.DB_BACKUP_RETENTION_DAYS
|
||||
? parseNonNegativeInt(process.env.DB_BACKUP_RETENTION_DAYS, DEFAULT_DB_BACKUP_RETENTION_DAYS)
|
||||
: (readStoredBackupSetting(db, "retentionDays", 0) ?? DEFAULT_DB_BACKUP_RETENTION_DAYS);
|
||||
|
||||
const result = pruneBackupDirectory({ backupDir, maxFiles, retentionDays });
|
||||
if (result.deletedFiles > 0) {
|
||||
console.log(
|
||||
`[Migration] Pruned ${result.deletedFiles} old backup file(s) ` +
|
||||
`(${result.keptBackupFamilies} kept, maxFiles=${maxFiles}, retentionDays=${retentionDays}).`
|
||||
);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[Migration] Failed to prune old backups: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pre-migration backup of the SQLite database using VACUUM INTO.
|
||||
* Returns the backup path on success, null on failure.
|
||||
@@ -825,6 +884,12 @@ function createPreMigrationBackup(db: SqliteAdapter): string | null {
|
||||
|
||||
db.exec(`VACUUM INTO '${escapedBackupPath}'`);
|
||||
console.log(`[Migration] Pre-migration backup created: ${backupPath}`);
|
||||
|
||||
// #10421: apply the operator's retention budget right here. Without this the
|
||||
// migration path was the one backup producer that never pruned, so every process
|
||||
// start with a pending migration added ~5 MB forever (observed: 49k files / 204 GB).
|
||||
pruneMigrationBackups(db, backupDir);
|
||||
|
||||
return backupPath;
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
@@ -121,12 +121,13 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
|
||||
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
|
||||
});
|
||||
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 39 audited modules", () => {
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 40 audited modules", () => {
|
||||
const expected = [
|
||||
"_rowTypes",
|
||||
"accessTokens",
|
||||
"apiKeyColumnFallbacks",
|
||||
"apiKeyUsageLimitFields",
|
||||
"backupRetention",
|
||||
"caseMapping",
|
||||
"cleanup",
|
||||
"cliToolState",
|
||||
|
||||
@@ -70,6 +70,37 @@ describe("i-have-adhd output style", () => {
|
||||
assert.ok(/preâmbulo/.test(pt.full), "pt-BR.full mentions preâmbulo");
|
||||
});
|
||||
|
||||
it("has i18n maps for pt-BR, vi, ja and id (parity with ponytail)", () => {
|
||||
assert.ok(ADHD.i18n, "i18n must be defined");
|
||||
for (const lang of ["pt-BR", "vi", "ja", "id"]) {
|
||||
const levels = ADHD.i18n[lang];
|
||||
assert.ok(levels, `${lang} must exist`);
|
||||
assertString(levels.lite, `${lang}.lite`);
|
||||
assertString(levels.full, `${lang}.full`);
|
||||
assertString(levels.ultra, `${lang}.ultra`);
|
||||
assert.ok(
|
||||
levels.full.includes("Code blocks"),
|
||||
`${lang}.full must keep the shared boundaries clause`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("each translation is written in its own language, not copied English", () => {
|
||||
// Native-script / native-word anchors: a level that merely duplicated the
|
||||
// English text would pass the structural checks above but fail here.
|
||||
const anchors: Record<string, RegExp> = {
|
||||
vi: /hành động/, // "action"
|
||||
ja: /[-ヿ]/, // kana — Japanese-exclusive
|
||||
id: /aksi|langkah/, // "action" / "step"
|
||||
};
|
||||
for (const [lang, pattern] of Object.entries(anchors)) {
|
||||
const levels = ADHD.i18n?.[lang];
|
||||
assert.ok(levels, `${lang} i18n must exist`);
|
||||
assert.ok(pattern.test(levels.full), `${lang}.full must use ${lang} vocabulary`);
|
||||
assert.ok(pattern.test(levels.ultra), `${lang}.ultra must use ${lang} vocabulary`);
|
||||
}
|
||||
});
|
||||
|
||||
it("carries no locale gate", () => {
|
||||
assert.equal(outputStyleMeta("i-have-adhd").locale, undefined);
|
||||
});
|
||||
|
||||
141
tests/unit/compression/output-styles-i18n-matrix.test.ts
Normal file
141
tests/unit/compression/output-styles-i18n-matrix.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Guard for the output-style × language matrix.
|
||||
*
|
||||
* Why this exists: the compression INPUT engines understand 10 languages (rule
|
||||
* packs under open-sse/services/compression/rules/ + the detector), but the
|
||||
* OUTPUT styles only instruct in a subset. `less-code` shipped English-only and
|
||||
* nobody noticed for months, because every other test is per-style. This test
|
||||
* is per-MATRIX: it pins the expected coverage so a new style cannot silently
|
||||
* be born English-only, and so an existing style cannot silently lose a locale.
|
||||
*
|
||||
* Adding a language to a style: extend BASELINE_LANGUAGES below (the assertion
|
||||
* is "at least these", so growth never fails the gate).
|
||||
* Adding a NEW style: it must cover REQUIRED_LANGUAGES, or be listed in
|
||||
* KNOWN_ENGLISH_ONLY with a tracking issue.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
OUTPUT_STYLE_CATALOG,
|
||||
OUTPUT_STYLE_IDS,
|
||||
outputStyleMeta,
|
||||
} from "../../../open-sse/services/compression/outputStyles/catalog.ts";
|
||||
|
||||
/** Minimum i18n coverage every new non-locale-gated style must ship with. */
|
||||
const REQUIRED_LANGUAGES = ["pt-BR"];
|
||||
|
||||
/**
|
||||
* Styles that predate this guard and are still English-only.
|
||||
* Do NOT add entries here without an issue — fix the coverage instead.
|
||||
*/
|
||||
const KNOWN_ENGLISH_ONLY: Record<string, string> = {
|
||||
// 9router port that never got translated. Tracked in the compression i18n
|
||||
// backlog; the fix is mechanical (same shape as ponytail/i-have-adhd).
|
||||
"less-code": "pre-existing gap — English-only since the 9router port",
|
||||
};
|
||||
|
||||
/**
|
||||
* Frozen per-style coverage. The assertion is a SUPERSET check, so adding a
|
||||
* language is always allowed; removing one fails the gate.
|
||||
*/
|
||||
const BASELINE_LANGUAGES: Record<string, string[]> = {
|
||||
// terse-prose reuses CAVEMAN_INSTRUCTION_BY_LANGUAGE (outputMode.ts), which
|
||||
// localizes to pt-BR/ja/id — keep the two in sync when adding a language.
|
||||
"terse-prose": ["pt-BR", "ja", "id"],
|
||||
"less-code": [],
|
||||
ponytail: ["pt-BR", "vi", "ja", "id"],
|
||||
"i-have-adhd": ["pt-BR", "vi", "ja", "id"],
|
||||
// locale-gated to zh: the single-language instruction IS the feature.
|
||||
"terse-cjk": [],
|
||||
};
|
||||
|
||||
function languagesOf(id: string): string[] {
|
||||
return Object.keys(outputStyleMeta(id).i18n ?? {});
|
||||
}
|
||||
|
||||
test("every catalog style is covered by the matrix baseline", () => {
|
||||
for (const id of OUTPUT_STYLE_IDS) {
|
||||
assert.ok(
|
||||
id in BASELINE_LANGUAGES,
|
||||
`style "${id}" is missing from BASELINE_LANGUAGES — add its expected languages ` +
|
||||
`(and translate it: a new style must cover ${REQUIRED_LANGUAGES.join(", ")})`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("no style loses a language it already had", () => {
|
||||
for (const [id, expected] of Object.entries(BASELINE_LANGUAGES)) {
|
||||
if (!OUTPUT_STYLE_CATALOG[id]) continue; // style removed — covered by the catalog tests
|
||||
const actual = languagesOf(id);
|
||||
for (const lang of expected) {
|
||||
assert.ok(
|
||||
actual.includes(lang),
|
||||
`style "${id}" lost its "${lang}" translation (has: ${actual.join(", ") || "none"})`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("a non-locale-gated style ships the required languages, or is a known gap", () => {
|
||||
for (const id of OUTPUT_STYLE_IDS) {
|
||||
const meta = outputStyleMeta(id);
|
||||
// A locale-gated style is only ever offered under its own locale, so a
|
||||
// single-language instruction is correct by design (e.g. terse-cjk → zh).
|
||||
if (meta.locale) continue;
|
||||
if (id in KNOWN_ENGLISH_ONLY) continue;
|
||||
const actual = languagesOf(id);
|
||||
for (const lang of REQUIRED_LANGUAGES) {
|
||||
assert.ok(
|
||||
actual.includes(lang),
|
||||
`style "${id}" must ship a "${lang}" translation (has: ${actual.join(", ") || "none"}). ` +
|
||||
`English-only styles need an entry in KNOWN_ENGLISH_ONLY with a tracking issue.`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("every declared translation has all three intensity levels", () => {
|
||||
for (const id of OUTPUT_STYLE_IDS) {
|
||||
const i18n = outputStyleMeta(id).i18n ?? {};
|
||||
for (const [lang, levels] of Object.entries(i18n)) {
|
||||
for (const level of ["lite", "full", "ultra"] as const) {
|
||||
assert.equal(
|
||||
typeof levels[level],
|
||||
"string",
|
||||
`${id}.i18n["${lang}"].${level} must be a string`
|
||||
);
|
||||
assert.ok(levels[level].length > 0, `${id}.i18n["${lang}"].${level} must be non-empty`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("every translated level carries the shared boundaries clause", () => {
|
||||
// The boundary clause is what keeps code, paths, commands, errors and URLs
|
||||
// verbatim. A translation that drops it would let the model rewrite them.
|
||||
const anchor = "Code blocks";
|
||||
for (const id of OUTPUT_STYLE_IDS) {
|
||||
const i18n = outputStyleMeta(id).i18n ?? {};
|
||||
for (const [lang, levels] of Object.entries(i18n)) {
|
||||
for (const level of ["lite", "full", "ultra"] as const) {
|
||||
assert.ok(
|
||||
levels[level].includes(anchor),
|
||||
`${id}.i18n["${lang}"].${level} is missing the shared boundaries clause`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("KNOWN_ENGLISH_ONLY does not hide a style that is actually translated", () => {
|
||||
// Stale-allowlist guard: once a gap is fixed, its entry must be removed.
|
||||
for (const id of Object.keys(KNOWN_ENGLISH_ONLY)) {
|
||||
if (!OUTPUT_STYLE_CATALOG[id]) continue;
|
||||
assert.equal(
|
||||
languagesOf(id).length,
|
||||
0,
|
||||
`style "${id}" now has translations — remove it from KNOWN_ENGLISH_ONLY`
|
||||
);
|
||||
}
|
||||
});
|
||||
258
tests/unit/db-pre-migration-backup-retention-10421.test.ts
Normal file
258
tests/unit/db-pre-migration-backup-retention-10421.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// #10421 — pre-migration backups were created on every migration run and never pruned,
|
||||
// so `db_backups/` grew without bound (observed: 48.999 files / 204 GB against a 5,3 MB
|
||||
// live database). The pruning logic already existed in `cleanupDbBackups()` but nothing
|
||||
// on the migration path ever reached it. These tests pin the retention step to the
|
||||
// backup call site so the operator's maxFiles/retentionDays budget is honored there too.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const serial = { concurrency: false };
|
||||
|
||||
async function importFresh(modulePath: string) {
|
||||
const url = pathToFileURL(path.resolve(modulePath)).href;
|
||||
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
||||
}
|
||||
|
||||
function withMockedMigrationFs(files: Record<string, string>, fn: () => void) {
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalReaddirSync = fs.readdirSync;
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
|
||||
const isMigrationDir = (target: unknown) =>
|
||||
String(target).replaceAll("\\", "/").endsWith("/src/lib/db/migrations") ||
|
||||
String(target).replaceAll("\\", "/").endsWith("/migrations");
|
||||
|
||||
fs.existsSync = ((target: unknown) => {
|
||||
if (isMigrationDir(target)) return true;
|
||||
const fileName = path.basename(String(target));
|
||||
if (Object.hasOwn(files, fileName)) return true;
|
||||
return originalExistsSync(target as string);
|
||||
}) as typeof fs.existsSync;
|
||||
|
||||
fs.readdirSync = ((target: string, options?: unknown) => {
|
||||
if (isMigrationDir(target)) return Object.keys(files);
|
||||
return originalReaddirSync(target, options as never);
|
||||
}) as typeof fs.readdirSync;
|
||||
|
||||
fs.readFileSync = ((target: unknown, options?: unknown) => {
|
||||
const fileName = path.basename(String(target));
|
||||
if (Object.hasOwn(files, fileName)) return files[fileName];
|
||||
return originalReadFileSync(target as string, options as never);
|
||||
}) as typeof fs.readFileSync;
|
||||
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
fs.existsSync = originalExistsSync;
|
||||
fs.readdirSync = originalReaddirSync;
|
||||
fs.readFileSync = originalReadFileSync;
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal SqliteAdapter over a real on-disk file (VACUUM INTO needs a file, not :memory:). */
|
||||
function createFileDb(sqlitePath: string) {
|
||||
const db = new Database(sqlitePath);
|
||||
|
||||
return {
|
||||
driver: "better-sqlite3",
|
||||
get open() {
|
||||
return db.open;
|
||||
},
|
||||
get name() {
|
||||
return db.name;
|
||||
},
|
||||
prepare: (sql: string) => db.prepare(sql),
|
||||
exec: (sql: string) => db.exec(sql),
|
||||
pragma: (str: string, options?: unknown) => db.pragma(str, options as never),
|
||||
transaction: (fn: (...args: unknown[]) => unknown) => {
|
||||
const tx = db.transaction((...args: unknown[]) => fn(...args));
|
||||
return (...args: unknown[]) => tx(...args);
|
||||
},
|
||||
immediate: (fn: () => void) => fn(),
|
||||
async backup() {},
|
||||
checkpoint() {},
|
||||
close: () => db.close(),
|
||||
get raw() {
|
||||
return db;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DB that already has migrations applied (so the pre-migration backup path is
|
||||
* reached: it requires `applied.size > 0`) plus one pending migration to trigger a run.
|
||||
*/
|
||||
function seedAppliedDb(db: ReturnType<typeof createFileDb>) {
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE combos (id TEXT PRIMARY KEY);
|
||||
CREATE TABLE call_logs (id TEXT PRIMARY KEY);
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record 001 as applied in the runner's own ledger table. `runMigrations` only takes a
|
||||
* pre-migration backup when `applied.size > 0`, so this is what puts the test on the
|
||||
* code path under exercise.
|
||||
*/
|
||||
function seedAppliedMigration(db: ReturnType<typeof createFileDb>) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS _omniroute_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO _omniroute_migrations (version, name, applied_at) VALUES (?, ?, ?)"
|
||||
).run("001", "initial_schema", new Date().toISOString());
|
||||
}
|
||||
|
||||
function makeTempDataDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-backup-retention-"));
|
||||
fs.mkdirSync(path.join(dir, "db_backups"), { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** Pre-existing backups, oldest first, with distinct mtimes so retention ordering is stable. */
|
||||
function seedBackups(backupDir: string, count: number) {
|
||||
const names: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = `db_2026-08-${String(i + 1).padStart(2, "0")}T00-00-00-000Z_pre-migration.sqlite`;
|
||||
const filePath = path.join(backupDir, name);
|
||||
fs.writeFileSync(filePath, "x");
|
||||
const t = new Date(2026, 7, i + 1).getTime() / 1000;
|
||||
fs.utimesSync(filePath, t, t);
|
||||
names.push(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function countBackups(backupDir: string) {
|
||||
return fs.readdirSync(backupDir).filter((n) => n.startsWith("db_")).length;
|
||||
}
|
||||
|
||||
function withEnv(vars: Record<string, string | undefined>, fn: () => void) {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
saved[k] = process.env[k];
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
"#10421 runMigrations prunes pre-migration backups to the configured maxFiles",
|
||||
serial,
|
||||
async () => {
|
||||
const dataDir = makeTempDataDir();
|
||||
const backupDir = path.join(dataDir, "db_backups");
|
||||
const sqlitePath = path.join(dataDir, "storage.sqlite");
|
||||
const db = createFileDb(sqlitePath);
|
||||
|
||||
try {
|
||||
seedAppliedDb(db);
|
||||
seedBackups(backupDir, 30);
|
||||
assert.equal(countBackups(backupDir), 30, "precondition: 30 stale backups on disk");
|
||||
|
||||
const { runMigrations } = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
|
||||
withEnv(
|
||||
{
|
||||
DB_BACKUP_MAX_FILES: "5",
|
||||
DB_BACKUP_RETENTION_DAYS: "0",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: undefined,
|
||||
},
|
||||
() => {
|
||||
withMockedMigrationFs(
|
||||
{
|
||||
"001_initial_schema.sql": "SELECT 1;",
|
||||
"002_retention_probe.sql": "CREATE TABLE retention_probe_10421 (id INTEGER);",
|
||||
},
|
||||
() => {
|
||||
// Mark 001 as applied so `applied.size > 0` and the backup path is reached.
|
||||
seedAppliedMigration(db);
|
||||
|
||||
runMigrations(db);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const remaining = countBackups(backupDir);
|
||||
assert.ok(
|
||||
remaining <= 5,
|
||||
`expected retention to cap db_backups at 5 files, found ${remaining} — ` +
|
||||
`pre-migration backups are accumulating unbounded (#10421)`
|
||||
);
|
||||
} finally {
|
||||
db.close();
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
test("#10421 the newest pre-migration backup survives pruning", serial, async () => {
|
||||
const dataDir = makeTempDataDir();
|
||||
const backupDir = path.join(dataDir, "db_backups");
|
||||
const sqlitePath = path.join(dataDir, "storage.sqlite");
|
||||
const db = createFileDb(sqlitePath);
|
||||
|
||||
try {
|
||||
seedAppliedDb(db);
|
||||
seedBackups(backupDir, 10);
|
||||
|
||||
const { runMigrations } = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
|
||||
withEnv(
|
||||
{
|
||||
DB_BACKUP_MAX_FILES: "3",
|
||||
DB_BACKUP_RETENTION_DAYS: "0",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: undefined,
|
||||
},
|
||||
() => {
|
||||
withMockedMigrationFs(
|
||||
{
|
||||
"001_initial_schema.sql": "SELECT 1;",
|
||||
"002_retention_probe.sql": "CREATE TABLE retention_probe_10421b (id INTEGER);",
|
||||
},
|
||||
() => {
|
||||
seedAppliedMigration(db);
|
||||
|
||||
runMigrations(db);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const remaining = fs.readdirSync(backupDir).filter((n) => n.startsWith("db_"));
|
||||
assert.ok(remaining.length <= 3, `expected <=3 backups, found ${remaining.length}`);
|
||||
|
||||
// The backup written by THIS run must be among the survivors — pruning must never
|
||||
// discard the snapshot that protects the migration it was taken for.
|
||||
const seededNames = new Set(
|
||||
Array.from({ length: 10 }, (_, i) => {
|
||||
return `db_2026-08-${String(i + 1).padStart(2, "0")}T00-00-00-000Z_pre-migration.sqlite`;
|
||||
})
|
||||
);
|
||||
const fresh = remaining.filter((n) => !seededNames.has(n));
|
||||
assert.equal(fresh.length, 1, `expected the run's own backup to survive, got ${fresh.length}`);
|
||||
} finally {
|
||||
db.close();
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user