Compare commits

..

5 Commits

Author SHA1 Message Date
diegosouzapw
918fba5e39 fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks)
_tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing
slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential
_tasks symlink can slip in via git add -A and, once pulled, checkout materializes
it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks
ignores the symlink too, preventing re-capture.
2026-08-08 01:18:49 -03:00
Diego Rodrigues de Sa e Souza
026e1cadaa fix(deps): bump nanoid, dompurify for Dependabot #189, #190
Closes Dependabot #189 (dompurify 3.4.13) and #190 (nanoid 3.3.17). npm audit → 0.
2026-08-08 00:10:31 -03:00
diegosouzapw
b090b601a5 fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190)
Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13
(with monaco-editor scoped override). Closes Dependabot #189, #190.

Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge —
awaiting Dependabot re-scan.

npm audit → 0 vulnerabilities.
2026-08-08 00:08:57 -03:00
Diego Rodrigues de Sa e Souza
153f453b0b fix(deps): bump deps for 13 Dependabot + audit cleanup on main
Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici) + brace-expansion, js-yaml v4, mermaid. npm audit → 0 vulnerabilities. Closes Dependabot #161-#188.
2026-08-06 23:13:28 -03:00
diegosouzapw
9233a9483c fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main
Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici)
applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies,
and mermaid.

npm audit: 6→0 vulnerabilities.
Closes Dependabot #161-#166.
2026-08-06 18:58:51 -03:00
260 changed files with 1532 additions and 12400 deletions

View File

@@ -1,197 +0,0 @@
# codebase-memory-mcp ignore list
#
# Padrão gitignore-style. Linhas começando com `#` são comentários.
# Barra final (`/`) = só diretório. Sem barra = casa arquivo OU diretório.
#
# O CBM também lê `.gitignore` automaticamente — esta lista deixa explícito o que
# os hooks do CBM vão pular. Se uma regra entrar em conflito entre os dois arquivos,
# vale a união. Editar este arquivo é mais barato do que confiar na herança implícita.
#
# Última reconciliação: 2026-07-31, status `ready` (513k nodes / 689k edges),
# `auto_index_limit=50000`, total indexável medido ≈11.546 arquivos (folga 4,3×).
#
# Fontes cruzadas:
# - `codebase-memory-mcp cli index_status --project home-diegosouzapw-dev-proxys-OmniRoute`
# → `not_indexed.dirs` (27) + `not_indexed.files` (336), todos `BY DESIGN`.
# - `.gitignore` deste repo (5.691 B) — fonte canônica secundária.
#
# Como auditar mudanças: depois de editar este arquivo, rodar `index_repository`
# (ou esperar `auto_watch` re-indexar) e re-checar `cli index_status` → comparar
# contagens em `not_indexed.dirs_count` e `not_indexed.files_count`.
# ─────────────────────────────────────────────────────────────────────────────
# 1. Diretorios de runtime / pacote — nao sao codigo-fonte
# ─────────────────────────────────────────────────────────────────────────────
node_modules/
node_modules
# Builds e artefatos reproduziveis (Layer 1 Next.js / Electron)
.build/
dist/
.next/
out/
# Electron especifico
electron/dist-electron/
electron/node_modules/
icon.iconset/
# Workspaces internos que tem proprio node_modules
@omniroute/opencode-plugin/dist/
@omniroute/opencode-plugin/node_modules/
@omniroute/opencode-provider/dist/
@omniroute/opencode-provider/node_modules/
# Recursos nativos compilados (C/JNI/wasm)
src/mitm/tproxy/native/build/
# Artefatos locais do Stryker / Playwright / coverage
.stryker-tmp/
reports/mutation/
stryker-output-*.json
.playwright-mcp/
test-results/
playwright-report/
blob-report/
# Analise / linters / caches
.analysis/
.sisyphus/
.plans/
.gitnexus
.worktrees
.codegraph/
# Quality artifacts (gerados por npm run lint --cache etc)
.eslintcache
.eslintcache-complexity
# Claude Code local state
.claude/scheduled_tasks.lock
.claude/scheduled_tasks/
.claude/sessions/
.claude/state.json
.claude/settings.local.json
# Serena / Antigravity / outras tools locais
.serena/
.antigravitycli/
.gemini/
.config/
# ─────────────────────────────────────────────────────────────────────────────
# 2. Diretorios com prefixo `_` — locais / privados (regra global do .gitignore)
# ─────────────────────────────────────────────────────────────────────────────
_*/
_artifacts/
_cache/
_mono_repo/
_references/
_tasks/
# ─────────────────────────────────────────────────────────────────────────────
# 3. Diretorios de tooling IA (state local, nao codigo)
# ─────────────────────────────────────────────────────────────────────────────
.agents/
.claude/
.vscode/
.idea/
.junie/
.omc/
.data/
.data-dev/
.local-data/
.logs/
.artifacts/
.source/
.superpowers/
.claude-flow/
.omnivscodeagent/
omnirouteCloud/
omnirouteSite/
.omniroute/
.stent/
# Subpaths especificos do Claude Code que nao estao em .claude/ (criados sob repo)
.claude/worktrees/
# ─────────────────────────────────────────────────────────────────────────────
# 4. Diretorios de dados / runtime locais (storage, env, secrets, scratch)
# ─────────────────────────────────────────────────────────────────────────────
data/
src/lib/env/
src/app/api/agent-skills/coverage/
src/app/api/cloud/
src/app/api/sync/cloud/
src/app/api/system/env/
tests/golden-set/data/
# Logs e saida de teste
logs/*
test_output.log
home-diegosouzapw-dev-automacoes-*.txt
# ─────────────────────────────────────────────────────────────────────────────
# 5. Diretorios do monorepo por subprojeto (nao fazem parte do app principal)
# ─────────────────────────────────────────────────────────────────────────────
security-analysis/
vscode-extension/
obsidian-plugin/node_modules/
# ─────────────────────────────────────────────────────────────────────────────
# 6. Diretorios de documentacao interna / workflow
# ─────────────────────────────────────────────────────────────────────────────
docs/superpowers/
# ─────────────────────────────────────────────────────────────────────────────
# 7. Arquivos especificos (nao diretorios inteiros)
# ─────────────────────────────────────────────────────────────────────────────
# Segredos e env — NUNCA indexar
.env
.env.*
!.env.example
!.env.homolog.example
# TypeScript build info e next env declaration
*.tsbuildinfo
next-env.d.ts
typescript
# SQLite transient files (WAL/SHM/journal)
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal
# Mapas e source maps
*.map
# Bun / npm lockfiles ruidosos
bun.lock
# `cheaper-inference-gateway.svg` e arquivos de midia na raiz/asset ja cobertos
# pelos `ignored-suffix` do indexador (svg/png/jpg/ico/etc >50kB ou >500linhas);
# manter a regra explicita aqui ajuda a auditar:
cheaper-inference-gateway.svg
cheaper-inference-gateway-*.svg
# Husky internals
.husky/_/
# CI / quality metric artifacts
config/quality/quality-metrics.json
config/quality/test-impact-map.json
audit-report.json
.gh-discussions.json
# i18n audit (gerado por npm run scripts)
scripts/i18n/_audit.json
scripts/i18n/_pending-keys.json
# Cli binario local (scratch)
bin/omniroute.mjs
# Deploy / docker backups
deploy.sh
docker-compose.yml.bak
docker-compose.minimal.yml

View File

@@ -1414,6 +1414,10 @@ APP_LOG_TO_FILE=true
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
# OMNIROUTE_PLUGIN_PATH=
# Allow plugins to request the 'exec' permission (spawn child processes from the
# plugin worker sandbox). Disabled by default; set to 1 to enable (local operator only).
# OMNIROUTE_PLUGINS_ALLOW_EXEC=0
# ── Prompt cache (system prompt deduplication) ──
# Used by: open-sse/services — caches identical system prompts across requests.
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)

View File

@@ -9,14 +9,11 @@
## Validation
Choose the change type and focused loop from the
[Contribution Golden Path](../docs/dev/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
Run only the focused loop for what you changed — the full unit suite, Vitest, the
60% coverage gate, and the production build all run in CI on this PR (#8329):
- [ ] Change type: provider / routing / UI / i18n / CLI / DB / build-deploy / other
- [ ] Focused tests and category gates from the golden path
- [ ] Focused tests for the change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- [ ] `npm run lint`
- [ ] Reconciled with the current active release base; focused checks rerun afterward
- [ ] Production-code changes include a new or updated automated test in this PR
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
@@ -32,4 +29,4 @@ Vitest, the 60% coverage gate, and the production build all run in CI on this PR
## Reviewer Notes
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.

71
.github/workflows/build-fork.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Publish Fork Image to GHCR
on:
push:
branches: [main]
tags:
- "v*"
workflow_dispatch:
# Least-privilege default: read-only at the top level; the build job that pushes to
# GHCR grants packages: write itself (Scorecard TokenPermissions).
permissions:
contents: read
env:
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
jobs:
build:
name: Build and Push Fork Image
if: github.repository == 'kang-heewon/OmniRoute'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix=sha-
type=ref,event=tag
labels: |
org.opencontainers.image.title=omniroute
org.opencontainers.image.description=Unified AI proxy/router — fork image
org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute
org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute
org.opencontainers.image.licenses=MIT
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
target: runner-base
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -0,0 +1,39 @@
name: Build Rinseaid OmniRoute image
on:
push:
branches: [build-k3-reasoning-image]
paths:
- Dockerfile
- package-lock.json
- package.json
- open-sse/**
- scripts/build/**
- .github/workflows/build-rinseaid-image.yml
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
target: runner-base
platforms: linux/amd64
push: true
tags: ghcr.io/rinseaid/omniroute:k3-reasoning-${{ github.sha }}

View File

@@ -42,18 +42,6 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
# Refuse a PR that targets its own head branch before spending anything on it. #8912 has
# head == base == release/v3.8.50: no diff, can never merge, and it sits in the queue with
# a full check board attached on every push to that branch. One field comparison.
- name: Reject a PR that targets its own branch
if: github.event_name == 'pull_request'
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node scripts/check/check-pr-self-target.mjs
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
@@ -137,17 +125,6 @@ jobs:
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:tracked-artifacts
# (gap 30) Also lives in quality.yml's PR-only "Merge integrity" job — because the
# CHANGELOG half of that job needs a base to diff against. This half does NOT: the
# generator either reproduces the committed SKILL.md files or it does not.
#
# Keeping it PR-only left a real hole. This cycle's merge trains landed in batches with
# `--admin`, which bypasses required checks, so three SKILL.md files drifted from the route
# catalog, rode the release squash into `main`, and the next cycle's sync-back turned them
# into a base-red that blocked EVERY PR into release/v3.8.50 until #8954. Running it here
# means a push to `main` catches the drift at the source instead of the next cycle
# inheriting it.
- run: npm run check:agent-skills-sync
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
@@ -353,16 +330,8 @@ jobs:
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official download script
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin.
# PINNED on purpose. Unpinned, the runner installed whatever PyPI served that day and
# measured 1 finding MORE than the devbox on the identical commit (190 vs 189) during
# the v3.8.49 cycle — which cost a second rebaseline push per release, chasing a
# number that was never the code's. The ratchet compares counts across machines, so
# the auditor version has to be the same on both. Bump this deliberately, and
# rebaseline in the same commit: check-workflows.mjs now prints `zizmorVersion=` next
# to the count so the new number is traceable to the tool that produced it.
ZIZMOR_VERSION=1.25.2
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin
pipx install zizmor || pip install --user zizmor
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
@@ -751,13 +720,7 @@ jobs:
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 25
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
@@ -829,13 +792,7 @@ jobs:
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
@@ -1272,8 +1229,6 @@ jobs:
- name: Generate dashboard
env:
EVENT_NAME: ${{ github.event_name }}
# Workflow-controlled data (job results), not user input — safe to read here.
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
status() {
case "$1" in
@@ -1288,29 +1243,6 @@ jobs:
echo "# 🚀 CI Dashboard" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
# (gap 12) A cancelled job never reported a verdict, and in a long table that reads the
# same as a green one. `cancel-in-progress` plus incremental fixing cancels jobs on every
# push, and this cycle the Vitest job was cancelled in rounds 1, 2 and 3 — it only ran to
# completion in round 4, where it revealed a suite that had been broken the whole cycle
# plus two production bugs. A gate that never finishes is indistinguishable from one that
# passes, so name them at the TOP instead of leaving them to be spotted mid-table.
CANCELLED_JOBS=$(printf '%s' "$NEEDS_JSON" \
| jq -r 'to_entries | map(select(.value.result == "cancelled")) | .[].key' 2>/dev/null \
| sort | paste -sd", " -) || CANCELLED_JOBS=""
if [ -n "$CANCELLED_JOBS" ]; then
{
echo "> ### ⚫ Cancelled — no verdict was reported"
echo ">"
echo "> \`$CANCELLED_JOBS\`"
echo ">"
echo "> These did not fail; they never finished, so nothing was checked. Treat this"
echo "> run as INCOMPLETE for those gates. If the cancellation came from"
echo "> \`cancel-in-progress\` on a newer push, the newer run covers it — otherwise"
echo "> re-run them before reading this dashboard as green."
echo ""
} >> "$GITHUB_STEP_SUMMARY"
fi
echo "## 🧱 Core Checks" >> "$GITHUB_STEP_SUMMARY"
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -120,18 +120,6 @@ jobs:
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
NODE_OPTIONS: "--max_old_space_size=6144"
# Linux builds with webpack, not Turbopack. Turbopack's production build
# allocates natively (Rust, off the V8 heap), so --max_old_space_size does
# not bound it, and on this module graph it peaks above what the hosted
# runner can give — the VM is reclaimed mid-compile with "The runner has
# received a shutdown signal", no exit code. That is what silently took the
# whole desktop channel out of v3.8.49: the linux leg died, `release` was
# skipped, and the release shipped with ZERO assets. Measured on a 32 GB
# box the same build passes and peaks past 14 GB. The webpack fallback is
# the project's documented escape hatch for RAM-constrained machines
# (docs/reference/ENVIRONMENT.md, #6409) and is the same remedy already
# applied to nightly-compat's Node 26 build (#8090).
OMNIROUTE_USE_TURBOPACK: ${{ matrix.platform == 'linux' && '0' || '1' }}
run: npm run build
- name: Sync version in electron/package.json
@@ -229,16 +217,6 @@ jobs:
release:
name: Create Release
needs: [validate, build]
# Fail-partial, not fail-closed. `build` is a 4-leg matrix with `fail-fast: false`,
# so the legs that succeed still upload their artifacts — but a default `needs:`
# gate skips this job the moment ANY leg fails, discarding all of them. That is
# exactly what happened to v3.8.49: the linux leg died and the release shipped with
# ZERO assets, throwing away 1.7 GB of good Windows/macOS installers **and** the
# source archives + SBOM, which do not depend on a build at all. The result was
# indistinguishable from "this version has no desktop channel".
# Now: attach everything that did build, then fail the job loudly (see the last
# step) so an incomplete channel is visible instead of silent.
if: ${{ !cancelled() && needs.validate.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release creates the GitHub Release
@@ -249,33 +227,11 @@ jobs:
persist-credentials: false
fetch-depth: 0
# `merge-multiple` is deliberately OFF. It resolves same-name collisions by ARRIVAL
# ORDER, and the two macOS jobs each emit their own `latest-mac.yml` listing only their
# own dmg (measured: 338 and 350 bytes, different content, identical name). One silently
# overwrote the other — arm64 won in the published v3.8.48, and since the Intel dmg
# carries no arch suffix in its name, electron-updater's
# `files.find(url includes process.arch) ?? files.shift()` sends every Intel Mac to the
# ARM dmg. Downloading into per-artifact subdirectories keeps both, so they can be
# merged on purpose instead of by luck.
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
# Writes release-assets/latest-mac.yml with BOTH dmgs, un-suffixed entry first (that is
# the one electron-updater can only reach through its fallback). Refuses to write when the
# inputs disagree on version — a manifest stitched from two builds is worse than none.
- name: Merge the per-arch macOS updater manifests
run: node scripts/release/merge-mac-update-manifest.mjs artifacts release-assets
# Everything else moves across as-is. The partial latest-mac.yml files are excluded so
# they cannot clobber the merged one; -n is a second belt on the same braces.
- name: Collect the remaining artifacts
run: |
mkdir -p release-assets
find artifacts -type f ! -name latest-mac.yml -exec cp -n {} release-assets/ \;
echo "release-assets:"
ls -la release-assets/
path: release-assets
merge-multiple: true
- name: Create source archives
env:
@@ -319,47 +275,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
verify-desktop-assets:
name: Verify desktop assets landed
needs: [validate, release]
# Deliberately a SEPARATE job, not a final step of `release`: failing inside
# `release` would cascade into `publish-npm` (which gates on `needs: release`) and
# block the npm channel over a desktop-only gap. Here the assets are attached, npm
# still publishes, and an incomplete desktop channel shows up as a red job instead
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
# green, because nothing ever asserted the release HAS binaries.
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Assert every platform is present on the release
env:
# Regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in the `validate` job, and
# passed via env rather than interpolated into the script body.
VERSION: ${{ needs.validate.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
names=$(gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name')
echo "Assets on $VERSION:"
echo "$names" | sed 's/^/ /'
missing=""
# `[ ... ] && missing=...` as the last command in a branch returns 1 and
# would abort the whole script under Actions' default `set -e`. Use if/fi.
for want in '\.exe$' '\.dmg$' '\.AppImage$' '\.deb$' '^latest.*\.yml$' '\.source\.tar\.gz$'; do
if ! echo "$names" | grep -qE "$want"; then
missing="$missing $want"
fi
done
if [ -n "$missing" ]; then
echo "::error::Desktop channel incomplete on $VERSION — no asset matching:$missing"
exit 1
fi
echo "✓ every platform present on $VERSION"
publish-npm:
name: Publish to npm
needs: [validate, release]

View File

@@ -159,17 +159,6 @@ jobs:
# `head_sha` is the tree-equality guarantee: same commit, same tree.
# Best-effort by design (retention is 1 day): every miss falls through to the build
# step below, which is why the dynamic runner above matters as the backstop.
#
# The `head_repository.full_name == env.REPO` clause is a supply-chain guard, not a
# filter refinement. This artifact becomes the published npm tarball. `pull_request`
# runs from forks execute in THIS repository's context and upload their own
# `next-build` built from fork-controlled source, and the runs API returns them for a
# matching `head_sha` — 57 such runs exist in this repo today. Without the clause,
# anything that made a fork's head commit coincide with the publish commit could put
# attacker-built bytes on npm. Requiring the run to originate from this repository
# excludes every fork run while keeping the fast path intact (verified: the same
# single run is selected either way for the current tip).
# CodeQL: actions/artifact-poisoning/critical.
- name: Reuse CI's next-build artifact (skips the heavy rebuild)
if: steps.resolve.outputs.skip != 'true'
continue-on-error: true
@@ -179,36 +168,14 @@ jobs:
REPO: ${{ github.repository }}
run: |
set -uo pipefail
# The question is "which run HAS the artifact", not "which run passed" (gap 16).
# Requiring `conclusion == "success"` on the whole run discarded a perfectly good tree
# whenever any unrelated shard went red — one flaky test then pushed the publish into
# the 40-minute build this step exists to avoid. The artifact is only uploaded if the
# Build job itself succeeded, so its PRESENCE is the accurate signal; the run's overall
# conclusion is noise from jobs that have nothing to do with the tree.
#
# `head_repository.full_name == env.REPO` stays, and it is not a filter refinement:
# this tree becomes the published npm tarball, and fork `pull_request` runs execute in
# THIS repository's context uploading their own next-build. That clause is the
# supply-chain guard (CodeQL actions/artifact-poisoning).
CANDIDATES=$(gh api "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&per_page=100" \
--jq '[.workflow_runs[]
| select(.name == "CI"
and .head_repository.full_name == env.REPO)]
| sort_by(.run_started_at) | reverse | .[0:5] | .[].id') || CANDIDATES=""
if [ -z "$CANDIDATES" ]; then
echo "::notice::no CI run from this repository for $HEAD_SHA — falling back to a full build"
RUN=$(gh api "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&per_page=100" \
--jq '[.workflow_runs[] | select(.name == "CI" and .conclusion == "success")] | .[0].id // empty') || RUN=""
if [ -z "$RUN" ]; then
echo "::notice::no successful CI run for $HEAD_SHA — falling back to a full build"
exit 0
fi
RUN=""
for candidate in $CANDIDATES; do
if gh run download "$candidate" --repo "$REPO" --name next-build --dir /tmp/next-build 2>/dev/null; then
RUN="$candidate"
break
fi
echo " run $candidate carries no usable next-build — trying the next"
done
if [ -z "$RUN" ]; then
echo "::notice::none of the candidate runs still carries next-build (1-day retention) — falling back to a full build"
if ! gh run download "$RUN" --repo "$REPO" --name next-build --dir /tmp/next-build; then
echo "::notice::next-build artifact unavailable for run $RUN (expired?) — falling back to a full build"
exit 0
fi
tar -xzf /tmp/next-build/e2e-build.tar.gz -C .
@@ -256,18 +223,6 @@ jobs:
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-boot
# The boot-smoke above proves a CLEAN install boots. It does not prove the path that
# actually broke us: installing over an existing version, where ~110 SQLite migrations
# run against a populated database. v3.8.48 shipped as a hotfix because the published
# 3.8.47 crashed on boot, and the v3.8.49 upgrade path was first exercised end-to-end
# by hand on a real 3.8.48 box (VPS .16) — after publishing, which is exactly backwards.
# Runs BEFORE `npm stage publish` so a broken upgrade never reaches the registry at all;
# a staged package that is never approved simply expires, with no `npm deprecate` needed.
- name: Prove clean-install AND upgrade-over-previous both boot
if: steps.resolve.outputs.skip != 'true'
timeout-minutes: 30
run: npm run check:install-upgrade
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
# parks the exact bytes on the registry WITHOUT making them installable; the
# owner then verifies and approves with 2FA (`npm stage approve`), moving the

View File

@@ -112,20 +112,7 @@ jobs:
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
# expression was DEAD CONFIGURATION: across 160 quality.yml runs the job never once landed on
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
# not at fault: in the same window ci.yml's Build demonstrably ran on omniroute-113-7 and
# omniroute-113-6, so self-hosted runs are visible when they happen.
#
# And if it ever HAD fired it would have inherited the measured penalty, because this job's
# first two steps are exactly the bottleneck: actions/setup-node + npm ci took 20m06s on .113
# with 4 concurrent runners versus 16s hosted (npm cache restore saturating the link). Median
# here is 5.6 min hosted across 72 successful runs.
#
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
@@ -179,81 +166,6 @@ jobs:
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
# The god-file refactor happens in PRs→release/**; without these, the release
# rail never sees a new import cycle, dead code, duplication or a security
# regression until the release PR to main. Deliberately NOT brought here:
# bundle-size (self-skips without a build — this rail's build job is advisory
# and uploads nothing, so it would be dead configuration) and the coverage
# run (fast-unit already runs the full suite; the coverage ratchet stays on
# the main rail via --allow-missing in lint-guard).
- run: npm run check:cycles
- run: npm run check:lockfile
- name: Duplication ratchet
run: npm run check:duplication
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet
run: npm run check:compression-budget
# Security scanners — same hardened install as ci.yml quality-extended
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
# gates below SKIP (exit 0) when their binary is absent — only a measured
# regression vs config/quality/quality-baseline.json blocks.
- name: Install security scanners (gitleaks/osv/actionlint/zizmor/oasdiff)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
mkdir -p "$HOME/.local/bin"
# Ratchets compare scanner COUNTS across runs. Pin every auditor: a rule-set
# update must be an explicit PR that re-measures/rebaselines, never a random
# red (or green) caused by whatever "latest" served that morning.
GITLEAKS_VERSION=v8.30.1
OSV_SCANNER_VERSION=v2.3.8
ACTIONLINT_VERSION=v1.7.12
ZIZMOR_VERSION=1.25.2
OASDIFF_VERSION=v1.19.1
# gitleaks — pinned linux x64 tarball via gh (authed), extract binary
rm -rf /tmp/gl && mkdir -p /tmp/gl
gh release download "$GITLEAKS_VERSION" --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
# osv-scanner — pinned linux amd64 bare binary via gh (authed)
rm -rf /tmp/osv && mkdir -p /tmp/osv
gh release download "$OSV_SCANNER_VERSION" --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official installer from a pinned release tag (never main)
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" "$HOME/.local/bin"
# zizmor — pinned PyPI package (same version as ci.yml quality-extended)
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# oasdiff — pinned linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download "$OASDIFF_VERSION" --repo Tufin/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# BASE_REF is read by the script from the env (never interpolated into a
# shell body) — workflow-injection-safe. actions/checkout fetches remote
# refs, not a local branch named github.base_ref, so prefix origin/ or this
# gate self-skips every PR with reason=base-unresolved.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: npm run check:openapi-breaking -- --ratchet
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
@@ -348,13 +260,7 @@ jobs:
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -390,13 +296,7 @@ jobs:
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
@@ -439,12 +339,6 @@ jobs:
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
# quality-gate job). contents: read keeps checkout working.
permissions:
contents: read
security-events: read
steps:
- uses: actions/checkout@v7
with:
@@ -466,29 +360,6 @@ jobs:
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
# inventory, two consumers; same reason ci.yml chains lint → quality-gate).
# The coverage-report artifact does not exist on this rail, so both ratchet
# invocations run --allow-missing: coverage.* metrics skip gracefully while
# the deterministic ones (eslint / openapi-coverage / i18n-ui) stay BLOCKING.
# Coverage authority remains on the main rail (ci.yml test-coverage → quality-gate).
- run: npm run quality:collect
- name: Ratchet check (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
- name: Require-tighten (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
# CodeQL alerts ratchet — same semantics as ci.yml quality-gate: exits 1 ONLY
# on a real regression (open alerts > baseline in quality-baseline.json);
# a measurement failure (gh/auth/api) self-skips with exit 0.
- name: CodeQL alerts ratchet (blocking)
run: npm run check:codeql-ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append ratchet summary
if: always()
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" || true
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come

12
.gitignore vendored
View File

@@ -171,6 +171,7 @@ config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
.worktrees
bin/omniroute.mjs
# Consistent with .dockerignore / .npmignore
.omc/
@@ -200,10 +201,7 @@ scripts/i18n/_pending-keys.json
.codegraph/
# Fumadocs generated source
/.source/
# Temporary local worktrees used to build unpublished npm tarballs
/.deploy-build-*/
.source/
# AI agent local settings and configs
.agents/
@@ -252,4 +250,8 @@ tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/
docker-compose.yml.bak
.playwright-cli/
# _tasks e um repo git SEPARADO (ver AGENTS.md). A linha _tasks/ (com barra) NAO
# ignora um SYMLINK chamado _tasks; /_tasks (ancorado) cobre arquivo/symlink/dir na raiz
# e impede que um git add -A recapture o symlink (incidente 2026-08-08).
/_tasks

8
.source/dynamic.ts Normal file
View File

@@ -0,0 +1,8 @@
// @ts-nocheck
import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
import * as Config from '../source.config';
const create = await dynamic<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
DocData: {
}
}>(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}});

22
.source/source.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
// source.config.ts
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
var docs = defineDocs({
dir: "docs",
docs: {
files: [
"./architecture/**/*.md",
"./guides/**/*.md",
"./reference/**/*.md",
"./frameworks/**/*.md",
"./routing/**/*.md",
"./security/**/*.md",
"./compression/**/*.md",
"./ops/**/*.md"
]
}
});
var source_config_default = defineConfig();
export {
source_config_default as default,
docs
};

1
AMIT Normal file
View File

@@ -0,0 +1 @@

View File

@@ -8,18 +8,6 @@
---
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1440,6 +1428,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -45,7 +45,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
| Database | `src/lib/db/` | SQLite domain modules (95 files, 110 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 13-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -332,7 +332,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (13-factor scoring, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
| Auto-Combo (12-factor scoring, 18 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
@@ -461,18 +461,10 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# Reuse the main checkout's node_modules to skip a per-worktree npm install.
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
runners all keep passing — the error names "filesystem root", not the worktree, so it
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.

View File

@@ -2,11 +2,6 @@
Thank you for your interest in contributing! This guide covers everything you need to get started.
For the official per-change workflow, start with the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
UI/UX, i18n, CLI, database, and build/deploy changes to their contracts, focused tests, CI
coverage, and reconciliation steps.
---
## Development Setup
@@ -203,11 +198,10 @@ Coverage notes:
### Pull Request Requirements
Before opening a PR, use the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
what you changed. The full unit suite (4 CI shards), Vitest, the **60%+** coverage gate, and
the production build are CI's responsibility — running them locally adds no signal the PR
checks will not already give you, and on smaller machines it can saturate the host (#8084):
Before opening a PR, run the focused loop for what you changed. The full unit suite
(4 CI shards), Vitest, the **60%+** coverage gate, and the production build are CI's
responsibility — running them locally adds no signal the PR checks will not already
give you, and on smaller machines it can saturate the host (#8084):
- Run the test files that cover your change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- Run `npm run lint`
@@ -277,7 +271,7 @@ src/ # TypeScript (.ts / .tsx)
│ ├── a2a/ # Agent-to-Agent v0.3 protocol server
│ ├── acp/ # Agent Communication Protocol registry
│ ├── compliance/ # Compliance policy engine
│ ├── db/ # SQLite domain modules + 130 migrations
│ ├── db/ # SQLite database layer (21 modules + 16 migrations)
│ ├── memory/ # Persistent conversational memory
│ ├── oauth/ # OAuth providers, services, and utilities
│ ├── skills/ # Extensible skill framework
@@ -287,7 +281,7 @@ src/ # TypeScript (.ts / .tsx)
├── mitm/ # MITM proxy (cert, DNS, target routing)
├── shared/
│ ├── components/ # React components (.tsx)
│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies
│ ├── constants/ # Provider definitions (177), MCP scopes, 14 routing strategies
│ ├── utils/ # Circuit breaker, sanitizer, auth helpers
│ └── validation/ # Zod v4 schemas
└── sse/ # SSE proxy pipeline
@@ -295,7 +289,7 @@ src/ # TypeScript (.ts / .tsx)
open-sse/ # @omniroute/open-sse workspace
├── executors/ # 14 provider-specific request executors
├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.)
├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes)
├── mcp-server/ # MCP server (25 tools, 3 transports, 10 scopes)
├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.)
├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama)
├── transformer/ # Responses API transformer

136
README.md
View File

@@ -67,46 +67,32 @@
<table>
<tr>
<td align="right"><b>🚀 Start</b></td>
<td align="center"><a href="#-quick-start">🚀 Quick Start</a></td>
<td align="center"><a href="#-more-install-methods--docker-source-pnpm-arch">📦 Install</a></td>
<td align="center"><a href="#-works-the-second-you-install-it--no-keys-no-config">🆓 Zero-config</a></td>
<td align="center"><a href="#-quick-start"><b>🚀 Quick Start</b></a></td>
<td align="center"><a href="#-combos--the-flagship"><b>🎯 Combos</b></a></td>
<td align="center"><a href="#-290-ai-providers--90-free"><b>🌐 Providers</b></a></td>
</tr>
<tr>
<td align="right"><b>💡 Learn</b></td>
<td align="center"><a href="#-full-cli--a2a--mcp"><b>🔌 CLI &amp; MCP</b></a></td>
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically"><b>🗜️ Compression</b></a></td>
<td align="center"><a href="https://omniroute.online"><b>🌍 Website</b></a></td>
</tr>
</table>
<table>
<tr>
<td align="center"><a href="#-the-promise">💥 The Promise</a></td>
<td align="center"><a href="#-why-omniroute">🤔 Why OmniRoute</a></td>
<td align="center"><a href="#-why-omniroute">🤔 Why</a></td>
<td align="center"><a href="#-what-sets-omniroute-apart">🏆 What Sets Apart</a></td>
</tr>
<tr>
<td align="right"><b>⚙️ Features</b></td>
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
<td align="center"><a href="#-290-ai-providers--90-free">🌐 Providers</a></td>
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI &amp; MCP</a></td>
</tr>
<tr>
<td align="right"></td>
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically">🗜️ Compression</a></td>
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
<td align="center"><a href="#%EF%B8%8F-where-omniroute-runs--anywhere">🖥️ Where It Runs</a></td>
<td align="center"><a href="#-private--local-first">🔒 Private</a></td>
</tr>
<tr>
<td align="right"><b>👀 See it</b></td>
<td align="center"><a href="#-omniroute-in-action">🎬 In Action</a></td>
<td align="center"><a href="#-whats-new">✨ What's New</a></td>
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
</tr>
<tr>
<td align="right"><b>💚 Support</b></td>
<td align="center"><a href="#-support-omniroute">💚 Support / Donate</a></td>
<td align="center"><a href="#-community--help">💬 Community</a></td>
<td align="center"><a href="#-sponsors">💖 Sponsors</a></td>
</tr>
<tr>
<td align="right"><b>📦 Project</b></td>
<td align="center"><a href="#%EF%B8%8F-tech-stack">🛠️ Tech Stack</a></td>
<td align="center"><a href="#-documentation">📖 Docs</a></td>
<td align="center"><a href="#-500-contributors">👥 Contributors</a></td>
<td align="center"><a href="#-dashboard-screenshots">📸 Screenshots</a></td>
<td align="center"><a href="#-support--community">📧 Support</a></td>
</tr>
</table>
@@ -241,53 +227,12 @@ curl http://localhost:20128/v1/chat/completions \
<b>What Kimi's support powers:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
</td>
</tr>
<tr>
<td align="center" width="150">
<a href="https://cheaperinference.com/?utm_source=omniroute">
<img src="public/providers/cheaperinference.svg" width="64" alt="Cheaper Inference"/>
</a>
<br/><b>Cheaper Inference</b><br/><sub>cheaperinference.com</sub><br/><br/>
<img src="https://img.shields.io/badge/Open_Source_Friend-31f889?style=flat-square&labelColor=04170d" alt="Open Source Friend"/>
</td>
<td>
Thanks to <b>Cheaper Inference</b>, an OmniRoute Open Source Friend, for backing this project! Cheaper Inference is a cost-ranked gateway that resells 42 frontier models — Claude, GPT-5.x, Gemini, Kimi K3, GLM, DeepSeek, Grok and MiniMax — behind one OpenAI-compatible endpoint, routing each request to the cheapest eligible provider without ever charging above the model maker's list price.
<br/><br/>
<b>First-class support in OmniRoute:</b> Chat Completions, the native <code>/v1/responses</code> endpoint, vision, tool calling and 3 image models (<code>grok-imagine</code>, <code>nano-banana-pro</code>, <code>nano-banana-2</code>, reachable as <code>cheaperinference/&lt;model&gt;</code>). <a href="https://cheaperinference.com/?utm_source=omniroute"><b>Get an API key →</b></a>
</td>
</tr>
</table>
<sub>Links tagged <code>aff=omniroute</code> are partner links. They fund the project at no extra cost to you.</sub>
<br/>
<details open>
<summary><sub><b>🎟️ Affiliates Promo</b> — free signup coupons from providers we don't sponsor (click to expand)</sub></summary>
<sub><i>This section is for referral/coupon codes only. Sponsored partnerships live in <b>🤝 Supported by our Open Source Friends</b> above. OmniRoute has no sponsorship or partnership with the providers listed here — these are public coupons anyone can use.</i></sub>
<table>
<tr>
<td align="center" width="120">
<a href="https://agentrouter.org/register?aff=70LM">
<img src="public/providers/agentrouter.png" width="32" alt="AgentRouter"/>
</a>
<br/><sub><b>AgentRouter</b></sub><br/><sub>agentrouter.org</sub>
</td>
<td>
<sub><b><a href="https://agentrouter.org/register?aff=70LM">AgentRouter</a></b> — affiliate signup · <b>$100 free credits</b> on signup (free server, expect higher latency — best for testing, not production). First-class support in OmniRoute since <b>v3.8.50</b>: Chat Completions, the Anthropic-compatible wire format and the OpenAI-compatible path. Available models include <code>claude-opus-4-8</code>, <code>claude-opus-5</code>, <code>gpt-5.6-sol</code> and more. <b><a href="https://agentrouter.org/register?aff=70LM">Grab your $100 →</a></b></sub>
<br/><br/>
<sub>⚠️ <i>Affiliate link — OmniRoute has no sponsorship or partnership with this provider.</i></sub>
</td>
</tr>
</table>
<sub>Know another provider with a generous free signup coupon that benefits OmniRoute users? Open an issue and we'll add it here.</sub>
</details>
<br/>
<div align="center">
## 🎯 Combos — The Flagship
@@ -445,50 +390,13 @@ All **19** strategies — mix & match per combo step:
<br/>
## 💚 Support OmniRoute
## ❤️ Support
OmniRoute is MIT-licensed and maintained in the open. If it saves you time or money, here's how to keep it independent — pick whatever fits you. Sponsorship never affects routing priority; it buys visibility, not ranking.
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:
<table>
<tr><td nowrap>⭐ <b>Star the repo</b></td><td>Free — genuinely helps visibility</td><td><a href="https://github.com/diegosouzapw/OmniRoute">Star OmniRoute</a></td></tr>
<tr><td nowrap>🐙 <b>GitHub Sponsors</b></td><td>One-off or monthly · zero platform fee</td><td><a href="https://github.com/sponsors/diegosouzapw">github.com/sponsors/diegosouzapw</a></td></tr>
<tr><td nowrap>🏢 <b>Open Collective</b></td><td><b>Companies</b> — issues an invoice/receipt · transparent books</td><td><a href="https://opencollective.com/omniroute">opencollective.com/omniroute</a></td></tr>
<tr><td nowrap>☕ <b>Ko-fi</b></td><td>Quick one-off tip, no signup for the donor</td><td><a href="https://ko-fi.com/diegosouzapw">ko-fi.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🧋 <b>Buy Me a Coffee</b></td><td>Small, informal gesture</td><td><a href="https://www.buymeacoffee.com/diegosouzapw">buymeacoffee.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🖐 <b>Liberapay</b></td><td>Recurring · non-profit · open source</td><td><a href="https://liberapay.com/diegosouzapw">liberapay.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🇧🇷 <b>PIX</b> (Brazil)</td><td>Instant, no fees</td><td>key &amp; QR below</td></tr>
<tr><td nowrap>₿ <b>Crypto</b></td><td>BTC · ETH · USDT-TRC20 · USDC-Solana</td><td>addresses below</td></tr>
</table>
**🇧🇷 PIX** — instant, no fees (Brazil)
<img src="docs/assets/pix-qr.png" width="140" align="right" alt="OmniRoute PIX QR code"/>
Key (random): `5d865059-bc44-483a-962d-43ceb80126eb`
Pix copia-e-cola:
```
00020101021126580014br.gov.bcb.pix01365d865059-bc44-483a-962d-43ceb80126eb5204000053039865802BR5922OMNIROUTE CONTRIBUICAO6006BRASIL62070503***630475DD
```
<br clear="right"/>
<details>
<summary><b>₿ Crypto</b> — BTC · ETH · USDT-TRC20 · USDC-Solana (click to expand)</summary>
<table>
<tr><td nowrap><b>₿ BTC</b></td><td nowrap>Bitcoin (SegWit)</td><td><code>bc1qh00smz004sy85wyl28v77tenkt3ckl6eaep7fd</code></td></tr>
<tr><td nowrap><b>Ξ ETH</b></td><td nowrap>Ethereum (ERC20)</td><td><code>0x64Cf6B68A6Ff34288e89172950a2d00102337a84</code></td></tr>
<tr><td nowrap><b>₮ USDT</b></td><td nowrap>Tron (TRC20)</td><td><code>TKAF41JpuQrHbKTnsQa9svJE2T192Hvsc2</code></td></tr>
<tr><td nowrap><b>$ USDC</b></td><td nowrap>Solana</td><td><code>2emNNZzVVWQc3FQ2wk9M6qXUQmW8AKdjjL174fXR28Tu</code></td></tr>
</table>
<sub>⚠️ Send each coin only on the network shown — sending on the wrong network can lose the funds.</sub>
</details>
🐛 Found a bug or have feedback? Open a [Discussion](https://github.com/diegosouzapw/OmniRoute/discussions).
-**Star the repo** — it genuinely helps visibility
- 💖 **[GitHub Sponsors](https://github.com/sponsors/diegosouzapw)** — fund ongoing maintenance and new providers
- 🐛 **Report bugs and share feedback** in [Discussions](https://github.com/diegosouzapw/OmniRoute/discussions)
<br/>
@@ -997,7 +905,7 @@ same process on one port, so there is no separate CLI-only package today.
<div align="center">
# 📧 Community & Help
# 📧 Support & Community
> Everything in one place — follow the maintainer, chat with the community, or open an issue.
@@ -1013,7 +921,7 @@ same process on one port, so there is no separate CLI-only package today.
| 📦 **Source code** | [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) |
| 🐛 **Report a bug** | [open an issue](https://github.com/diegosouzapw/OmniRoute/issues) — attach `npm run system-info` output |
| 🤝 **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [Branching & Release Model](docs/ops/BRANCHING_MODEL.md) · pick a `good first issue` |
| 💚 **Support the project** | [Ways to support ↑](#-support-omniroute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
| **Support the project** | [Star the repo](https://github.com/diegosouzapw/OmniRoute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
</div>

View File

@@ -130,7 +130,7 @@ async function openSqliteDatabase(dbPath, options = {}) {
try {
return new loaded.Database(dbPath, options);
} catch (error) {
return openWithSyncDriverFallback(dbPath, options, error);
throw createSqliteNativeError(error);
}
}

View File

@@ -1 +0,0 @@
- **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))

View File

@@ -1 +0,0 @@
- fix(cli): fall back to node:sqlite when better-sqlite3 constructor throws at runtime (#8826)

View File

@@ -1 +0,0 @@
- **fix(ci):** the reconciliation helper no longer bounds its scan with `git describe --tags` — releases squash-merge, so that range re-listed 1361 commits instead of the cycle's real 22, which is how ~200 PRs once slipped through without a changelog bullet. The base is now resolved from the commit that opened the cycle, and a new `sweep:stale-fragments` gate removes `changelog.d/` fragments that a back-merge from `main` resurrected after they had already been folded in ([#8985](https://github.com/diegosouzapw/OmniRoute/pull/8985))

View File

@@ -1 +0,0 @@
- **fix(ci):** fixed a live auto-update defect where **Intel Macs downloaded the ARM dmg** — the two macOS jobs each emitted their own `latest-mac.yml` and `merge-multiple` let one silently overwrite the other by arrival order, leaving `electron-updater`'s arch fallback pointing at the wrong build. The manifests are now merged deliberately, un-suffixed entry first. Also: test jobs pinned to hosted runners (`setup-node` measured 20m06s self-hosted vs 16s hosted), the npm publish no longer discards a valid build artifact because an unrelated shard was flaky, the agent-skills gate now runs on pushes to `main` instead of PRs only, and the CI summary names every job that ended cancelled ([#8988](https://github.com/diegosouzapw/OmniRoute/pull/8988))

View File

@@ -1 +0,0 @@
- **chore(sse):** dropped the leftover `iflow` entry from the token-refresh TTL map — the provider was removed from the product but its 24-hour refresh lead outlived it, and the identifier had exactly two occurrences left repo-wide ([#8966](https://github.com/diegosouzapw/OmniRoute/pull/8966))

View File

@@ -1 +0,0 @@
- **chore(ci):** removed two fork-owned image-publish workflows that had ridden into the repo as unrelated extra files in on-topic PRs — `build-fork.yml` (`ghcr.io/kang-heewon`, job-level guard, so it instantiated a skipped run on every push to main and every tag) and `build-rinseaid-image.yml` (`ghcr.io/rinseaid`, no guard, never fired). Neither could authenticate against this repository's token; a new policy guard now fails CI on any workflow targeting a foreign registry namespace ([#8967](https://github.com/diegosouzapw/OmniRoute/pull/8967))

View File

@@ -1 +0,0 @@
- **test(ci):** fixed the intermittent `spawnSync bash EPIPE` failure in the `:latest` promotion guard — the script exits on a pre-release version before reading stdin, so the harness's pipe-backed `input:` raced that exit; stdin is now file-backed, which makes the race structurally impossible ([#8977](https://github.com/diegosouzapw/OmniRoute/pull/8977))

File diff suppressed because it is too large Load Diff

View File

@@ -177,7 +177,7 @@
"tests/unit/account-fallback-service.test.ts": 1563,
"tests/unit/batch_api.test.ts": 1324,
"tests/unit/cc-compatible-provider.test.ts": 1217,
"tests/unit/chatcore-translation-paths.test.ts": 2776,
"tests/unit/chatcore-translation-paths.test.ts": 2769,
"tests/unit/chatgpt-web.test.ts": 3148,
"tests/unit/combo-routing-engine.test.ts": 3449,
"tests/unit/db-migration-runner.test.ts": 1499,
@@ -342,7 +342,7 @@
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"open-sse/executors/antigravity.ts": 1528,
"open-sse/executors/base.ts": 1578,
"open-sse/executors/base.ts": 1562,
"open-sse/executors/chatgpt-web.ts": 3241,
"open-sse/executors/codex.ts": 1534,
"open-sse/executors/cursor.ts": 1560,
@@ -365,7 +365,7 @@
"open-sse/services/rateLimitManager.ts": 1060,
"open-sse/translator/response/openai-responses.ts": 1174,
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
"open-sse/utils/stream.ts": 2889,
"open-sse/utils/stream.ts": 2887,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1381,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
@@ -413,6 +413,5 @@
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file.",
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests."
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts."
}

View File

@@ -1,6 +0,0 @@
{
"_doc": "Tables that exist ONLY in databases upgraded from an older version — residue whose CREATE left the migration set in some past cycle but survives where it already existed. Harmless (nothing references them), but recorded here so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.",
"residualTables": {
"cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left in place rather than dropped: a DROP migration on a table we cannot prove is unused everywhere is the riskier change. Revisit when the cache subsystem is next touched."
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

View File

@@ -1,229 +0,0 @@
---
title: "Contribution Golden Path"
---
# Contribution Golden Path
Use this guide to choose the smallest reliable development loop for a pull request. It does not
replace the area-specific architecture and security documents linked below; it connects each common
change type to its contracts, focused checks, and CI coverage.
## The path every change follows
1. **Choose the base before editing.** Find the highest active `release/v*` branch and branch from
its tip. Target that branch, not `main`. If a release freeze is active, do not target the frozen
branch; use the next active cycle described in
[Branching & Release Model](../ops/BRANCHING_MODEL.md).
2. **Name the contracts.** Identify every catalog, schema, generated artifact, public API, or user
interface that the change affects. The table below gives the minimum starting set.
3. **Write or update focused tests.** Production changes in `src/`, `open-sse/`, `electron/`, or
`bin/` require an automated test in the same PR. Run the smallest test files that prove the
behavior, then the listed focused gates.
4. **Let CI run the broad matrix.** The complete unit shards, Vitest, coverage ratchet, and
production build run on the PR. Run a broad suite locally only when a focused failure points to
wider impact or when the change spans several subsystems.
5. **Reconcile before review.** Fetch the active base, inspect its new commits and your diff against
it, then rebase or merge the base according to the contributor workflow. Resolve generated-file
and catalog conflicts from their source, regenerate them, rerun the focused loop, and confirm the
PR still targets the active release branch.
6. **Record evidence.** In the PR template, list the commands run, every test file added or changed,
migrations or feature flags, and any CI-only validation still pending.
## Golden paths by change type
Commands below are minimum focused checks, not permission to skip a test that directly covers the
behavior you changed.
### Provider
**Contracts**
- Provider definition in `src/shared/constants/providers/` and its composition in
`src/shared/constants/providers.ts`.
- Models and capabilities in `open-sse/config/providerRegistry.ts` or its extracted registry files.
- Executor/translator selection, OAuth or API-key configuration, dashboard assets, and generated
provider reference when applicable.
- Public credentials must use `resolvePublicCred()`; error responses must use the shared sanitized
error helpers. See [Public Credentials](../security/PUBLIC_CREDS.md) and
[Error Sanitization](../security/ERROR_SANITIZATION.md).
**Focused loop**
```bash
npm run check:provider-consistency
npm run check:provider-assets
node --import tsx/esm --test tests/unit/provider-translate-path-golden.test.ts
node --import tsx/esm --test tests/unit/<provider-or-executor>.test.ts
npm run gen:provider-reference # when the catalog changes; commit the generated diff
npm run lint
```
Also test every affected request family: chat, Responses, images, embeddings, audio, or video.
Review generated catalog and golden diffs as contract changes; do not accept them blindly.
### Routing
**Contracts**
- Public strategy values and UI metadata in `src/shared/constants/routingStrategies.ts`.
- Dispatch and ordering under `open-sse/services/combo.ts` and `open-sse/services/combo/`.
- Combo schemas, persistence, resilience state, model capabilities, and API/UI controls.
- [Auto-Combo Engine](../routing/AUTO-COMBO.md) and resilience documentation when behavior changes.
**Focused loop**
```bash
node --import tsx/esm --test tests/unit/combo-<behavior>.test.ts
npm run test:combo:matrix # strategy or dispatch changes
npm run check:known-symbols # strategy registration changes
npm run lint
```
Use deterministic mocked-upstream tests locally. Live combo smokes require credentials and are
manual, not CI substitutes.
### UI / UX
**Contracts**
- Next.js route/page and shared component boundaries under `src/app/` and
`src/shared/components/`.
- API response shapes, loading/empty/error states, keyboard and screen-reader behavior,
responsive layout, theming, and locale expansion.
- English UI source strings in `src/i18n/messages/en.json`; do not hard-code new user-facing copy.
**Focused loop**
```bash
node --import tsx --test tests/unit/dashboard/<feature>.test.ts
npx vitest run --config vitest.config.ts tests/unit/ui/<component>.test.tsx
npm run check:dashboard-typecheck
npm run lint
```
Run the app for interaction or visual changes and check both narrow and wide viewports. CI runs the
production build and broader suites; visual behavior still needs a focused component, Playwright,
or documented manual check appropriate to the change.
### i18n
**Contracts**
- `src/i18n/messages/en.json` is the UI source; `config/i18n.json` is the locale source.
- CLI catalogs live separately under `bin/cli/locales/`.
- Preserve ICU placeholders and tags exactly. Do not translate product/provider/model names,
protocol and header names, commands, code/JSON identifiers, URLs, environment variables, or
protected terms such as `OmniRoute`, `OAuth`, `MCP`, and `A2A`. The current source list is
`scripts/i18n/glossary/protected-terms.json`.
**Focused loop**
```bash
npm run i18n:sync-ui:dry
npm run i18n:check-ui-coverage
npm run i18n:check-value-drift
npm run i18n:check-glossary
npm run check:cli-i18n # when CLI strings/catalogs change
npm run lint
```
This is guidance for the existing system, not an invitation to expand its tooling or key model.
Keep i18n patches surgical while the replacement system is being designed. Do not run translation
commands that call external services unless the task explicitly requires generated translations and
you have reviewed the resulting diff.
### CLI
**Contracts**
- Public commands and flags in `bin/cli/`, generated API commands, exit codes, stdout/stderr and
JSON output shapes, config/environment behavior, and packaged files.
- CLI user-facing strings must use the CLI i18n layer and keep `en`/`pt-BR` catalogs aligned.
- Preserve Node as the supported runtime and the published binary contract.
**Focused loop**
```bash
node --import tsx/esm --test tests/unit/cli/<command>.test.ts
npm run check:cli-i18n
npm run build:cli # generated/bundled CLI changes
npm run check:pack-policy # package-surface changes
npm run lint
```
Use the exact command in a temporary data directory when behavior depends on parsing, files, or exit
status. CI performs the broader package artifact and ecosystem checks.
### Database
**Contracts**
- Domain modules under `src/lib/db/`; `src/lib/localDb.ts` remains a re-export layer only.
- Numbered, idempotent SQL migrations under `src/lib/db/migrations/`, transaction safety, upgrade
behavior, indexes, and every caller affected by the schema.
- Routes and handlers never issue raw SQL directly.
**Focused loop**
```bash
npm run check:migration-numbering
npm run check:db-rules
node --import tsx/esm --test tests/unit/db/<domain>.test.ts
node --import tsx/esm --test tests/unit/db/migration-<number>.test.ts
npm run lint
```
Test both a fresh database and upgrade from the prior schema when adding a migration. Database tests
must close handles and call `resetDbInstance()` during cleanup. Run `npm run test:bun:db` only when
the best-effort Bun adapter path changes; Node remains authoritative.
### Build / deploy
**Contracts**
- Root and workspace manifests/lockfile, `scripts/build/`, Next.js standalone assembly, `dist/`
package contents, Electron platform metadata, CI workflows, and deployment sentinels.
- Supported Node ranges and the allow-listed Bun use in `CLAUDE.md` must remain intact.
- Build artifacts stay untracked; dependency, license, workflow, and package policies apply.
**Focused loop**
```bash
node --import tsx/esm --test tests/unit/build/<behavior>.test.ts
npm run check:build-scope
npm run check:lockfile # dependency or lockfile changes
npm run check:pack-policy # published package surface changes
npm run lint
```
Use `npm run build` locally only when the change affects compilation, standalone assembly, assets,
or runtime bundling. Use `npm run build:release` only for release/deploy validation. CI's build is
the final cross-platform signal; platform-specific Electron changes need the matching focused build
or smoke evidence.
## Local loop versus CI
| Run locally for each patch | CI supplies the broad signal |
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Direct behavior tests and category gates above | Sharded full unit suite and serial tests |
| `npm run lint` | Vitest suites and coverage/quality ratchets |
| Typecheck or build only when the affected contract calls for it | Production build, security, docs, dependency, and PR-policy gates |
| Manual interaction/live checks only when automation cannot prove the behavior | Cross-job integration and platform checks configured by workflow |
A green focused loop is evidence about the changed contract, not proof that unrelated CI checks
will pass. Conversely, do not make every local edit wait for the full repository matrix.
## Reconciliation checklist
Before requesting review:
- Confirm the PR base is still the highest active `release/v*` branch.
- Fetch that base and review commits that landed since you branched.
- Review `git diff <active-base>...HEAD` for accidental or generated churn.
- Resolve catalog and generated-document conflicts by updating the source and regenerating output.
- Rerun every focused test/gate listed in the PR description after reconciliation.
- Never weaken assertions or drop required tests merely to match a moved base.
For release-freeze and retargeting rules, use
[Branching & Release Model](../ops/BRANCHING_MODEL.md). For the complete CI inventory, use
[Quality Gates Reference](../architecture/QUALITY_GATES.md).

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -8,18 +8,6 @@
---
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1154,7 +1142,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1440,6 +1427,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -461,18 +461,10 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# Reuse the main checkout's node_modules to skip a per-worktree npm install.
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
runners all keep passing — the error names "filesystem root", not the worktree, so it
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -415,18 +415,10 @@ git push -u origin feat/your-feature
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# 复用主工作区 node_modules省去每个 worktree 的 npm install
# 必须用硬链接(`cp -al`),绝不能用符号链接:整棵树约 5 秒,几乎不占额外磁盘
# inode 是共享的),而且与符号链接不同,它不会破坏开发服务器。
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# 主工作区符号链接 node_modules省去每个 worktree 的 npm install
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
**绝不要对 node_modules 使用 `ln -s`。** Turbopack 会拒绝解析到项目根目录之外的符号链接,
因此 `npm run dev` 会以 FATAL panic 崩溃(`Symlink [project]/node_modules is invalid, it
points out of the filesystem root`),而 typecheck、lint 和测试运行器却都照常通过 —— 错误信息
提到的是 "filesystem root" 而不是 worktree看起来像 Next/构建的 bug排查会浪费大量时间
(事故 2026-07-31#9043
在 Claude Code 中优先使用原生的 `EnterWorktree` 工具(它已经在 `.claude/worktrees/` 下创建 worktree先用上述命令创建 worktree然后用其 `path` 调用 `EnterWorktree`。
3. **工作、提交、推送、发起 PR — 全部在 worktree 内部完成。** 绝不在另一个会话可能共享的 worktree 内 `git checkout` 不同分支。

View File

@@ -6,18 +6,6 @@
## [3.8.31] — 2026-06-20
## [3.8.50] — TBD
_Living section — cycle opened at the v3.8.49 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
### ✨ New Features
### 🐛 Bug Fixes
### 📝 Maintenance
---
## [3.8.49] — 2026-07-28
_Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62333b0 → tip). Bullets carry the merged PR and its author; direct pushes listed separately. Finalized at the v3.8.49 release._
@@ -1152,7 +1140,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- **fix(api):** stop JSON.stringify-ing messages before estimateTokens (restores #8368 image estimate) [recovered from #8599] ([#8740](https://github.com/diegosouzapw/OmniRoute/pull/8740))
- **fix(quality):** let --update remove baseline entries that already fit the cap ([#8741](https://github.com/diegosouzapw/OmniRoute/pull/8741)) — thanks @MumuTW
- **fix:** escape angle brackets in i18n messages to prevent next-intl INVALID_MESSAGE errors ([#8747](https://github.com/diegosouzapw/OmniRoute/pull/8747)) — thanks @SteeleHu
- **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
@@ -1438,6 +1425,10 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- 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:

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.8.50
version: 3.8.49
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

View File

@@ -414,6 +414,7 @@ detection above).
| `OMNIROUTE_HTTP_TIMEOUT_MS` | `30000` | `bin/cli/api.mjs` | Per-attempt HTTP timeout (ms) for CLI → server requests. |
| `OMNIROUTE_VERBOSE` | `0` | `bin/cli/api.mjs` | Set to `1` to print retry/backoff diagnostics to stderr during CLI commands. |
| `OMNIROUTE_PLUGIN_PATH` | _(unset)_ | `bin/cli/plugins.mjs` | Custom directory for CLI plugin discovery (`omniroute-cmd-*` packages). Defaults to `~/.omniroute/plugins/` when unset. |
| `OMNIROUTE_PLUGINS_ALLOW_EXEC` | `0` | `src/lib/plugins/pluginWorker.ts` | Set to `1` to allow plugins to request the `exec` permission (spawn child processes from the worker sandbox). Local operator only. |
---

View File

@@ -1,14 +1,14 @@
---
title: "Provider Reference"
version: 3.8.50
lastUpdated: 2026-07-30
version: 3.8.49
lastUpdated: 2026-07-28
---
# Provider Reference
> **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand.
> Regenerate with: `npm run gen:provider-reference`
> **Last generated:** 2026-07-30
> **Last generated:** 2026-07-28
Total providers: **290**. See category breakdown below.

View File

@@ -167,21 +167,21 @@ The Auto-Combo Engine dynamically selects the best provider/model for each reque
> Source: [diagrams/auto-combo-12factor.mmd](../diagrams/auto-combo-12factor.mmd) (regenerate via `npm run docs:render-diagrams`). Diagram/filename predate the `cacheAffinity` factor added by #8008 and still show 12 factors.
| Factor | Default Weight | Description |
| :-------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `health` | 0.20 | Health score from circuit breaker (CLOSED=1.0, HALF_OPEN=0.5, OPEN=0.0) |
| `quota` | 0.15 | Remaining quota / rate-limit headroom [0..1] |
| `costInv` | 0.15 | Inverse **blended** cost (60% input + 40% output token price, normalized) — cheaper = higher score |
| `latencyInv` | 0.12 | Inverse p95 latency normalized to pool — faster = higher score |
| `taskFit` | 0.08 | Task-type fitness (coding, review, planning, analysis, debugging, docs) |
| `stability` | 0.05 | Variance-based stability (low latency stdDev / error rate) |
| `tierPriority` | 0.05 | Account-tier priority — Ultra=1.0, Pro=0.67, Standard=0.33, Free=0.0 |
| `tierAffinity` | 0.05 | Affinity between the candidate's tier and the manifest-recommended tier |
| `specificityMatch` | 0.05 | Match between request specificity (manifest hint) and model tier |
| `contextAffinity` | 0.05 | Affinity between the request's context-window need and the model's context window |
| `connectionDensity` | 0.05 | Spreads load across connections of the same provider (anti-concentration) |
| Factor | Default Weight | Description |
| :-------------------- | :------------- | :------------------------------------------------------------------------------------------------- |
| `health` | 0.20 | Health score from circuit breaker (CLOSED=1.0, HALF_OPEN=0.5, OPEN=0.0) |
| `quota` | 0.15 | Remaining quota / rate-limit headroom [0..1] |
| `costInv` | 0.15 | Inverse **blended** cost (60% input + 40% output token price, normalized) — cheaper = higher score |
| `latencyInv` | 0.12 | Inverse p95 latency normalized to pool — faster = higher score |
| `taskFit` | 0.08 | Task-type fitness (coding, review, planning, analysis, debugging, docs) |
| `stability` | 0.05 | Variance-based stability (low latency stdDev / error rate) |
| `tierPriority` | 0.05 | Account-tier priority — Ultra=1.0, Pro=0.67, Standard=0.33, Free=0.0 |
| `tierAffinity` | 0.05 | Affinity between the candidate's tier and the manifest-recommended tier |
| `specificityMatch` | 0.05 | Match between request specificity (manifest hint) and model tier |
| `contextAffinity` | 0.05 | Affinity between the request's context-window need and the model's context window |
| `connectionDensity` | 0.05 | Spreads load across connections of the same provider (anti-concentration) |
| `cacheAffinity` | 0.00 | Rendezvous-hash affinity toward the connection likeliest to already hold this request's prompt-cache prefix (`open-sse/services/combo/promptCacheAffinity.ts`); disabled by default (#8008) |
| `resetWindowAffinity` | 0.00 | Bias toward connections whose quota reset window is favorable (disabled by default) |
| `resetWindowAffinity` | 0.00 | Bias toward connections whose quota reset window is favorable (disabled by default) |
**Sum:** `0.20 + 0.15 + 0.15 + 0.12 + 0.08 + 0.05 + 0.05 + 0.05 + 0.05 + 0.05 + 0.05 + 0.00 + 0.00 = 1.0` (validated by `validateWeights()`).
@@ -215,11 +215,11 @@ combo's stored config. These apply only to the `auto` strategy and only for the
that carries them; the combo's saved `modePack`/`budgetCap`/`budgetFallback` are used
when the header is absent.
| Header | Accepts | Effect |
| :---------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-OmniRoute-Mode` | a preset alias (`fast`, `balanced`, `quality`, `cheap`, `reliable`, `offline`) or a raw pack name (`ship-fast`, `cost-saver`, `quality-first`, `offline-friendly`, `reliability-first`) | Overrides the scoring weights for this request. `balanced`/`default` force the default weights (no pack). Unknown values are ignored (config preserved). |
| `X-OmniRoute-Budget` | a positive number (max USD per request) | Hard cost ceiling: candidates whose estimated cost exceeds it are filtered before selection. What happens when **every** candidate exceeds it is controlled by `X-OmniRoute-Budget-Fallback` below. |
| `X-OmniRoute-Budget-Fallback` | `cheapest` (default, aliases: `cheapest-viable`, `soft`) or `strict` (aliases: `block`, `hard`) | `cheapest`: falls back to the globally cheapest candidate even though it still exceeds the cap (legacy behavior). `strict`: refuses to select — the request fails fast with `HTTP 402` instead of silently overspending. Unknown values are ignored. |
| Header | Accepts | Effect |
| :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-OmniRoute-Mode` | a preset alias (`fast`, `balanced`, `quality`, `cheap`, `reliable`, `offline`) or a raw pack name (`ship-fast`, `cost-saver`, `quality-first`, `offline-friendly`, `reliability-first`) | Overrides the scoring weights for this request. `balanced`/`default` force the default weights (no pack). Unknown values are ignored (config preserved). |
| `X-OmniRoute-Budget` | a positive number (max USD per request) | Hard cost ceiling: candidates whose estimated cost exceeds it are filtered before selection. What happens when **every** candidate exceeds it is controlled by `X-OmniRoute-Budget-Fallback` below. |
| `X-OmniRoute-Budget-Fallback` | `cheapest` (default, aliases: `cheapest-viable`, `soft`) or `strict` (aliases: `block`, `hard`) | `cheapest`: falls back to the globally cheapest candidate even though it still exceeds the cap (legacy behavior). `strict`: refuses to select — the request fails fast with `HTTP 402` instead of silently overspending. Unknown values are ignored. |
```bash
# Force the fastest profile, cap this request at $0.05, and hard-block instead of overspending
@@ -240,27 +240,27 @@ resolved values feed the engine's existing `config.modePack` / `config.budgetCap
OmniRoute's combo engine supports **19 routing strategies** (declared in `src/shared/constants/routingStrategies.ts``ROUTING_STRATEGY_VALUES`). The Auto Combo engine itself is exposed under the `auto` strategy; the others are available for persisted combos.
| Strategy | Description |
| :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `priority` | First-target ordered list with explicit priority |
| `weighted` | Weighted random by per-target weight |
| `round-robin` | Cycle through targets in order |
| `context-relay` | Hand off context across targets (long conversations) |
| `fill-first` | Fill each target's quota before moving to next |
| `p2c` | Power-of-2-choices random load balancing |
| `random` | Uniform random selection |
| `least-used` | Pick target with lowest current load |
| `cost-optimized` | Minimize $ per request given catalog pricing |
| `reset-aware` ⭐ | Prioritize by quota reset time — short reset windows ranked higher |
| `reset-window` | Prefer targets whose quota window resets soonest |
| `headroom` | Pick the target with the most remaining quota headroom |
| `strict-random` | Random without deduplication of repeats |
| `auto` | Use Auto Combo scoring (9-factor) — **recommended** |
| `lkgp` | Last-Known-Good Path (sticky route to last successful target) |
| `context-optimized` | Pick target with best fit for current context size |
| Strategy | Description |
| :------------------ | :--------------------------------------------------------------------------------------------------------------------------- |
| `priority` | First-target ordered list with explicit priority |
| `weighted` | Weighted random by per-target weight |
| `round-robin` | Cycle through targets in order |
| `context-relay` | Hand off context across targets (long conversations) |
| `fill-first` | Fill each target's quota before moving to next |
| `p2c` | Power-of-2-choices random load balancing |
| `random` | Uniform random selection |
| `least-used` | Pick target with lowest current load |
| `cost-optimized` | Minimize $ per request given catalog pricing |
| `reset-aware` ⭐ | Prioritize by quota reset time — short reset windows ranked higher |
| `reset-window` | Prefer targets whose quota window resets soonest |
| `headroom` | Pick the target with the most remaining quota headroom |
| `strict-random` | Random without deduplication of repeats |
| `auto` | Use Auto Combo scoring (9-factor) — **recommended** |
| `lkgp` | Last-Known-Good Path (sticky route to last successful target) |
| `context-optimized` | Pick target with best fit for current context size |
| `cache-optimized` | Reorder targets by prompt-cache affinity — the connection likeliest to already hold this request's cached prefix is tried first (`open-sse/services/combo/promptCacheAffinity.ts`, #8008) |
| `fusion` 🧬 | Fan out to a panel of models in parallel, then synthesize one answer via a judge (see below) |
| `pipeline` | Run targets sequentially, threading each step's output into the next step's input; only the final answer is returned (#6396) |
| `fusion` 🧬 | Fan out to a panel of models in parallel, then synthesize one answer via a judge (see below) |
| `pipeline` | Run targets sequentially, threading each step's output into the next step's input; only the final answer is returned (#6396) |
⭐ = New in v3.8.0 · 🧬 = New in v3.8.36
@@ -679,11 +679,11 @@ See `docs/marketing/TIERS.md` for tier definitions and provider classification.
### Deterministic routing-decision matrix (`npm run test:combo:matrix`)
`tests/integration/combo-matrix/*.test.ts` proves the routing **decision** of all 19
`tests/integration/combo-matrix/*.test.ts` proves the routing **decision** of all 18
public strategies end-to-end through the real combo pipeline with a mocked upstream.
Coverage includes:
- All 19 `ROUTING_STRATEGY_VALUES` strategies (ordered, weighted, cost, context, fusion, …).
- All 18 `ROUTING_STRATEGY_VALUES` strategies (ordered, weighted, cost, context, fusion, …).
- `quota-share` (internal) end-to-end: DRR fairness + saturation deprioritization via the
real `selectQuotaShareTarget` seam (`registerQuotaFetcher` / `setLKGP` /
`__setHeadroomSaturationFetcherForTests`).

View File

@@ -1,91 +0,0 @@
# agentrouter.org WAF (Web Application Firewall)
The `agentrouter` upstream gateway runs a keyword-based content filter on
`messages[].content`. The filter is partially deterministic (always blocks
certain phrases) and partially probabilistic (burst-sensitive — becomes
more aggressive after rapid requests, recovers after a cooldown).
When the WAF blocks a request it returns:
```
HTTP/1.1 400 Bad Request
{"error":{"code":"content-blocked","message":"content-blocked (request id: ...)","param":"","type":"agent_router_api_error"}}
```
## Scope of the filter
The WAF inspects `messages[].content` only. It does **not** inspect:
- The `system` prompt
- Structured content blocks (`tool_result`, `tool_use`, `thinking`, `image`)
- Tool `description` and `input_schema` fields
- Request metadata, headers, or model id
## Always-blocked patterns (case-insensitive)
| Pattern | Notes |
|-------------------------------|----------------------------------------|
| Any `Lorem ipsum` variant | Full Latin lorem vocabulary is blocked |
| `language model` (alone) | "the language model" and "large language model" pass |
| `virtual assistant` | "AI assistant" passes |
| `I'm here to help` | "here to help" alone also blocks |
| `Claude, made by Anthropic` | Full phrase only |
## Almost-always-blocked patterns
| Pattern | Notes |
|-------------------|---------------------------------------------------------|
| `placeholder` | When it stands alone (not as a parameter name, etc.) |
| `dummy data` | Common seed phrase for fixtures |
| `foo bar baz` | Canonical placeholder phrase |
| Repeated short tokens (`AAA BBB CCC`, `test test test`) | Detector for keyword stuffing |
## Behavior under load
After ~5 rapid requests in a short window, the WAF begins blocking content
that would normally pass. The bucket relaxes after ~510 seconds of idle
time. This is the same IP-and-key-bound rate limiter that causes
intermittent `400 content-blocked` errors when Claude Code or Codex CLI
makes multiple tool-use / message-send calls in quick succession.
## Mitigations already applied in OmniRoute
1. **`open-sse/services/wafRateLimit.ts`** — burst guard that enforces a
500 ms minimum gap between outbound requests to any `agentrouter:*`
URL. The gap is well below human perception of latency and prevents
the WAF from activating on normal traffic.
2. **`BaseExecutor.WAF_RETRY_CONFIG`** — when an upstream returns
`400 content-blocked`, the executor retries the same URL with
exponential backoff (1.5 s, 3.0 s, max 2 attempts). After the backoff
the WAF usually relaxes and the retry succeeds.
3. **`tests/unit/compression/harness.test.ts`** — the test fixture
`longInput` was changed from `"lorem ipsum dolor sit amet ".repeat(40)`
to `"example content for testing purposes ".repeat(40)` so that when
Claude Code reads this file via the `Read` tool, the file contents
do not flow back through a `tool_result` block and trip the WAF.
## Guidance for prompts and tool output
If a Claude Code or Codex CLI session repeatedly hits
`400 content-blocked`, check the most recent user message and the most
recent tool result for any of the patterns above and rephrase. Common
workarounds:
- Replace `Lorem ipsum …` with `example text …` or the actual content
the test or fixture is trying to model.
- Replace `placeholder` (when standing alone) with `example value`,
`sample value`, or the real value.
- Replace `language model` with `large language model` or `the model`.
- Replace `dummy data` with `sample data` or realistic seed values.
- Replace `I'm here to help` / `here to help` with a more specific
opener (e.g. "I'll review the file you mentioned").
## Reporting the false positives upstream
The current filter is overly aggressive — it blocks "Lorem ipsum" in
`tool_result` blocks even though the operator clearly did not intend to
inject a prompt. Operators who want this fixed at the source should
contact `agentrouter.org` to report the false positives. The blocklist
above is the empirical result of probing the upstream as of 2026-08-03.

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute-desktop",
"version": "3.8.50",
"version": "3.8.49",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": {

View File

@@ -13,29 +13,6 @@ const TO_NUMBER_RESTRICTION = {
"canonical coercion shape and the `toNumberOrNull`/`toNumberArray` variants.",
};
const LOCAL_DB_IMPORT_RESTRICTION = {
regex: "^(?:@/lib/localDb(?:\\.ts)?|(?:\\.\\.?/)+(?:lib/)?localDb(?:\\.ts)?)$",
message:
"The localDb compatibility barrel is restricted — import the owning domain module " +
"from `@/lib/db/` instead.",
};
const EXECUTOR_IMPORT_RESTRICTION = {
regex: "^(?:@omniroute/)?open-sse/executors(?:/|$)",
message:
"Executor implementations must stay behind an open-sse handler or service boundary.",
};
const PROP_TYPES_RESTRICTION = {
name: "prop-types",
message: "PropTypes are deprecated. Use TypeScript types/interfaces instead.",
};
const IMPORT_BOUNDARY_RESTRICTIONS = {
paths: [PROP_TYPES_RESTRICTION],
patterns: [LOCAL_DB_IMPORT_RESTRICTION],
};
/** @type {import("eslint").Linter.Config[]} */
const eslintConfig = [
...nextVitals,
@@ -62,32 +39,15 @@ const eslintConfig = [
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-restricted-imports": ["error", IMPORT_BOUNDARY_RESTRICTIONS],
},
},
// G14: DB internals may use the compatibility barrel while it is decomposed; all
// other source files must import the owning src/lib/db domain module directly.
{
files: ["src/lib/db/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"error",
{
paths: [PROP_TYPES_RESTRICTION],
},
],
},
},
// G14: App routes/components must delegate provider execution through handlers or
// services instead of reaching into executor implementations.
{
files: ["src/app/**/*.{ts,tsx,js,jsx}"],
rules: {
"no-restricted-imports": [
"error",
{
...IMPORT_BOUNDARY_RESTRICTIONS,
patterns: [LOCAL_DB_IMPORT_RESTRICTION, EXECUTOR_IMPORT_RESTRICTION],
paths: [
{
name: "prop-types",
message: "PropTypes are deprecated. Use TypeScript types/interfaces instead.",
},
],
},
],
},

View File

@@ -1,9 +1,4 @@
import {
DEFAULT_CODEX_CLIENT_VERSION,
getCodexCliRsHeaders as buildCodexCliRsHeaders,
} from "@/shared/constants/codexClient";
export { DEFAULT_CODEX_CLIENT_VERSION } from "@/shared/constants/codexClient";
const DEFAULT_CODEX_CLIENT_VERSION = "0.144.1";
const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200";
const DEFAULT_CODEX_USER_AGENT_ARCH = "x64";
const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION";
@@ -47,10 +42,6 @@ export function getCodexDefaultHeaders(): Record<string, string> {
};
}
export function getCodexCliRsHeaders(): Record<string, string> {
return buildCodexCliRsHeaders(getCodexClientVersion());
}
export function normalizeCodexSessionId(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.trim();

View File

@@ -19,9 +19,10 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts";
export const FREE_CATALOG_CURATED_AT = "2026-07-22";
export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-opus-4-6", displayName: "Claude 4.6 Opus", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "claude-haiku-4-5-20251001", displayName: "Claude 4.5 Haiku", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "glm-5.1", displayName: "GLM 5.1", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agentrouter", modelId: "deepseek-v3.2", displayName: "DeepSeek V3.2", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" },
{ provider: "agy", modelId: "claude-opus-4-6-thinking", displayName: "Claude Opus 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },
{ provider: "agy", modelId: "gemini-3.1-pro-low", displayName: "Gemini 3.1 Pro (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" },

View File

@@ -11,7 +11,6 @@ import { KIE_IMAGE_MODELS } from "./providers/registry/kie/imageModels.ts";
import { FREEPIK_IMAGE_PROVIDER } from "./providers/registry/freepik/index.ts";
import { STABILITY_AI_IMAGE_MODELS } from "./providers/registry/stability-ai/imageModels.ts";
import { GEMINI_IMAGEN_PROVIDER } from "./providers/registry/gemini/imageModels.ts";
import { CHEAPERINFERENCE_IMAGE_PROVIDER } from "./providers/registry/cheaperinference/imageModels.ts";
interface ImageModelEntry {
id: string;
@@ -715,13 +714,6 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1:1", "16:9", "9:16", "4:3", "3:4", "1024x1024", "1792x1024", "1024x1792"],
},
// Cheaper Inference (OSS-sponsor gateway). Declared AFTER adobe-firefly on
// purpose: it shares the nano-banana-pro / nano-banana-2 ids, and parseImageModel
// resolves a bare id by first-match over this object's iteration order, so
// Firefly keeps the bare ids and these are prefix-only. See the module for the
// full collision note.
cheaperinference: CHEAPERINFERENCE_IMAGE_PROVIDER,
// Keep Bailian Coding Plan after existing duplicate model owners so adding
// explicit `bailian-coding-plan/` and `bcp/` routes does not change
// historical bare-model routing.

View File

@@ -117,7 +117,6 @@ import { uncloseaiProvider } from "./registry/uncloseai/index.ts";
import { nscaleProvider } from "./registry/nscale/index.ts";
import { chatgpt_webProvider } from "./registry/chatgpt-web/index.ts";
import { openrouterProvider } from "./registry/openrouter/index.ts";
import { cheaperinferenceProvider } from "./registry/cheaperinference/index.ts";
import { openvectaProvider } from "./registry/openvecta/index.ts";
import { orcarouterProvider } from "./registry/orcarouter/index.ts";
import { copilot_webProvider } from "./registry/copilot-web/index.ts";
@@ -338,7 +337,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
nscale: nscaleProvider,
"chatgpt-web": chatgpt_webProvider,
openrouter: openrouterProvider,
cheaperinference: cheaperinferenceProvider,
openvecta: openvectaProvider,
orcarouter: orcarouterProvider,
"copilot-web": copilot_webProvider,

View File

@@ -1,5 +1,4 @@
import type { RegistryEntry } from "../../shared.ts";
import { getCodexCliRsHeaders } from "../../../codexClient.ts";
export const agentrouterProvider: RegistryEntry = {
id: "agentrouter",
@@ -9,22 +8,6 @@ export const agentrouterProvider: RegistryEntry = {
baseUrl: "https://agentrouter.org/v1/messages",
authType: "apikey",
authHeader: "x-api-key",
alternateFormats: [
{
format: "openai",
baseUrl: "https://agentrouter.org/v1/chat/completions",
authHeader: "bearer",
headers: getCodexCliRsHeaders(),
label: "OpenAI-compatible (Codex)",
},
{
format: "openai-responses",
baseUrl: "https://agentrouter.org/v1/responses",
authHeader: "bearer",
headers: getCodexCliRsHeaders(),
label: "OpenAI Responses (Codex)",
},
],
defaultContextLength: 128000,
// No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code
// wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are
@@ -32,9 +15,10 @@ export const agentrouterProvider: RegistryEntry = {
// own baseUrl + x-api-key auth. A static fingerprint here would drift and
// trip AgentRouter's WAF ("unauthorized client detected").
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-opus-5", name: "Claude Opus 5" },
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol" },
{ id: "claude-opus-4-6", name: "Claude 4.6 Opus" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
{ id: "glm-5.1", name: "GLM 5.1" },
{ id: "deepseek-v3.2", name: "DeepSeek V3.2" },
],
passthroughModels: true,
};

View File

@@ -1,34 +0,0 @@
/**
* Cheaper Inference image provider registry entry.
* Extracted into its own module to keep open-sse/config/imageRegistry.ts
* under the file-size cap (god-file decomposition; semantic split) — same
* pattern as FREEPIK_IMAGE_PROVIDER / SEGMIND_IMAGE_PROVIDER.
*
* 3 image models measured from GET /v1/models?type=image on 2026-07-31.
*
* COLLISION NOTE: nano-banana-pro and nano-banana-2 are ALSO adobe-firefly model
* ids. parseImageModel() resolves a bare id by first-match over IMAGE_PROVIDERS
* iteration order, so this entry is spread into that object AFTER adobe-firefly:
* bare `nano-banana-2` keeps routing to Firefly (pre-existing behaviour) and these
* models are reachable only as `cheaperinference/<id>` / `cinf/<id>`. Do NOT add
* IMAGE_MODEL_ALIASES entries for them — that would silently re-route Firefly
* users. Guarded by tests/unit/cheaperinference-image-models.test.ts.
*
* The endpoint ignores `response_format:"url"` and always returns `b64_json`
* (measured twice). The OpenAI image path already handles b64_json; this is not a
* bug to "fix". /v1/images/edits returns 404 upstream, so no edit support.
*/
export const CHEAPERINFERENCE_IMAGE_PROVIDER = {
id: "cheaperinference",
alias: "cinf",
baseUrl: "https://api.cheaperinference.com/v1/images/generations",
authType: "apikey",
authHeader: "bearer",
format: "openai",
models: [
{ id: "grok-imagine", name: "Grok Imagine (Cheaper Inference)" },
{ id: "nano-banana-pro", name: "Nano Banana Pro (Cheaper Inference)" },
{ id: "nano-banana-2", name: "Nano Banana 2 (Cheaper Inference)" },
],
supportedSizes: ["1024x1024", "2048x2048", "4096x4096"],
};

View File

@@ -1,247 +0,0 @@
import type { RegistryEntry, RegistryModel } from "../../shared.ts";
/**
* Cheaper Inference (https://api.cheaperinference.com) — cost-ranked OpenAI-compatible
* gateway, OmniRoute Open Source Friend.
*
* Catalog captured from a live `GET /v1/models` on 2026-07-31 (42 entries: these 39
* `type:"text"` models plus 3 `type:"image"` models that live in imageRegistry.ts —
* sending an image model here returns HTTP 400 "Use POST /v1/images/generations").
* `supportsVision`/`supportsReasoning` mirror each entry's `capabilities` object
* verbatim; they are not inferred from the model name.
*
* The gateway also serves a native `/v1/responses` endpoint (`responsesBaseUrl`).
* It is stateless and REQUIRES `store:false` — see executors/cheaperinference.ts,
* which injects it and resolves the URL from the per-model `targetFormat` tag.
*/
export const CHEAPERINFERENCE_MODELS: RegistryModel[] = [
{ id: "aion-labs.aion-2-0", name: "Aion 2.0", supportsReasoning: true, toolCalling: true },
{
id: "claude-fable-5",
name: "Claude Fable 5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-haiku-4.5",
name: "Claude Haiku 4.5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4-7-fast",
name: "Claude Opus 4.7 Fast",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4-8-fast",
name: "Claude Opus 4.8 Fast",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4.5",
name: "Claude Opus 4.5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4.6",
name: "Claude Opus 4.6",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4.7",
name: "Claude Opus 4.7",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-4.8",
name: "Claude Opus 4.8",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-5",
name: "Claude Opus 5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-opus-5-fast",
name: "Claude Opus 5 Fast",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true, toolCalling: true },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true, toolCalling: true },
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "gemini-3-5-flash",
name: "Gemini 3.5 Flash",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "gemini-3.1-flash-lite",
name: "Gemini 3.1 Flash Lite",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "gemini-3.1-pro",
name: "Gemini 3.1 Pro",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "gemini-3.1-pro-preview",
name: "Gemini 3.1 Pro Preview",
supportsReasoning: true,
toolCalling: true,
},
{ id: "glm-4.5", name: "GLM-4.5", supportsReasoning: true, toolCalling: true },
{ id: "glm-4.5-air", name: "GLM-4.5 Air", supportsReasoning: true, toolCalling: true },
{ id: "glm-4.6", name: "GLM-4.6", supportsReasoning: true, toolCalling: true },
{ id: "glm-4.7", name: "GLM-4.7", supportsReasoning: true, toolCalling: true },
{ id: "glm-5", name: "GLM-5", supportsReasoning: true, toolCalling: true },
{ id: "glm-5.1", name: "GLM-5.1", supportsReasoning: true, toolCalling: true },
{ id: "glm-5.2", name: "GLM-5.2", supportsReasoning: true, toolCalling: true },
{
id: "google/gemini-3.5-flash-lite",
name: "Gemini 3.5 Flash Lite",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
// The GPT-5.x family is tagged for the gateway's native /v1/responses endpoint —
// that is the surface OpenAI-family clients (Codex-style) expect for tool loops.
{
id: "gpt-5.4",
name: "GPT-5.4",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "gpt-5.4-mini",
name: "GPT-5.4 Mini",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "gpt-5.5",
name: "GPT-5.5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "gpt-5.6-terra",
name: "GPT-5.6 Terra",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
targetFormat: "openai-responses",
},
{
id: "grok-4.5",
name: "Grok 4.5",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{
id: "kimi-k3",
name: "Kimi K3",
supportsVision: true,
supportsReasoning: true,
toolCalling: true,
},
{ id: "minimax-m2.7", name: "MiniMax M2.7", supportsReasoning: true, toolCalling: true },
];
export const cheaperinferenceProvider: RegistryEntry = {
id: "cheaperinference",
alias: "cinf",
format: "openai",
executor: "cheaperinference",
baseUrl: "https://api.cheaperinference.com/v1/chat/completions",
// The gateway serves a native, STATELESS /v1/responses endpoint alongside
// /v1/chat/completions. Consumed by CheaperInferenceExecutor.buildUrl for the
// models tagged targetFormat: "openai-responses" above.
responsesBaseUrl: "https://api.cheaperinference.com/v1/responses",
authType: "apikey",
authHeader: "bearer",
models: CHEAPERINFERENCE_MODELS,
};

View File

@@ -34,7 +34,6 @@ import {
resolveAccountKey,
isFreeVariantModel,
} from "../services/openrouterFreeWindow.ts";
import { gateOutboundRequest } from "../services/wafRateLimit.ts";
import type { PoolConfig } from "../services/sessionPool/types.ts";
import type { Session } from "../services/sessionPool/session.ts";
import { SessionPool } from "../services/sessionPool/sessionPool.ts";
@@ -46,7 +45,6 @@ import {
} from "../services/apiKeyRotator.ts";
import type { KeyHealth } from "../services/apiKeyRotator.ts";
import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts";
import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts";
import {
runWithOnPersist,
getRefreshLeadMs,
@@ -251,10 +249,7 @@ function collectThinkingConfigs(body: unknown): Array<Record<string, unknown>> {
if (!body || typeof body !== "object") return [];
const root = body as Record<string, unknown>;
const configs: Array<Record<string, unknown>> = [];
const envelopes: unknown[] = [
root.generationConfig,
(root.request as Record<string, unknown> | undefined)?.generationConfig,
];
const envelopes: unknown[] = [root.generationConfig, (root.request as Record<string, unknown> | undefined)?.generationConfig];
for (const env of envelopes) {
if (!env || typeof env !== "object") continue;
const tc = (env as Record<string, unknown>).thinkingConfig;
@@ -450,12 +445,6 @@ export class BaseExecutor {
);
}
protected usesClaudeCodeProtocol(credentials: ProviderCredentials | null): boolean {
if (!isClaudeCodeCompatible(this.provider)) return false;
const format = this.resolveAlternate(credentials)?.format;
return format !== "openai" && format !== "openai-responses";
}
/**
* Resolve the effective API key via extra-keys round-robin rotation.
* Mutates `credentials.providerSpecificData.selectedKeyId` on rotation.
@@ -600,15 +589,6 @@ export class BaseExecutor {
// Intra-URL retry config: retry same URL before falling back to next node
static readonly RETRY_CONFIG = { maxAttempts: 2, delayMs: 2000 };
// WAF (400 content-blocked) retry config: agentrouter.org's WAF is burst-sensitive
// and recovers after a short cooldown. Use exponential backoff with a higher
// starting delay than the generic 429 retry (which is 2s) because the WAF
// needs more time to clear its per-IP suspicion bucket.
static readonly WAF_RETRY_CONFIG = {
maxAttempts: 2,
delayMs: 1500,
backoffMultiplier: 2,
};
// Timeout for receiving the initial upstream response headers. Once the response
// starts streaming, STREAM_IDLE_TIMEOUT_MS / Undici bodyTimeout handle stalls.
static FETCH_START_TIMEOUT_MS = FETCH_TIMEOUT_MS;
@@ -854,16 +834,15 @@ export class BaseExecutor {
);
}
const usesClaudeCodeProtocol = this.usesClaudeCodeProtocol(requestCredentials);
const fingerprintProvider =
usesCcWireImage(this.provider) && !usesClaudeCodeProtocol ? "codex" : this.provider;
const ccRequestDefaults = usesClaudeCodeProtocol
const ccRequestDefaults = isClaudeCodeCompatible(this.provider)
? getClaudeCodeCompatibleRequestDefaults(requestCredentials?.providerSpecificData)
: {};
const shouldForwardExtendedContext =
extendedContext && modelSupportsContext1mBeta(model) && !usesClaudeCodeProtocol;
extendedContext &&
modelSupportsContext1mBeta(model) &&
!isClaudeCodeCompatible(this.provider);
const shouldForwardCcCompatibleContext1m =
usesClaudeCodeProtocol &&
isClaudeCodeCompatible(this.provider) &&
ccRequestDefaults.context1m === true &&
!modelHasNativeContext1m(model);
if (shouldForwardExtendedContext || shouldForwardCcCompatibleContext1m) {
@@ -943,8 +922,8 @@ export class BaseExecutor {
!activeCredentials?.apiKey;
if (
((this.provider === "claude" && (isClaudeCodeClient || hasClaudeOAuthToken)) ||
usesClaudeCodeProtocol) &&
this.provider === "claude" &&
(isClaudeCodeClient || hasClaudeOAuthToken) &&
typeof transformedBody === "object" &&
transformedBody !== null
) {
@@ -1221,11 +1200,6 @@ export class BaseExecutor {
if (ccKeysLower.has(key.toLowerCase())) delete headers[key];
}
Object.assign(headers, ccHeaders);
if (usesCcWireImage(this.provider) && usesClaudeCodeProtocol) {
delete headers["Authorization"];
headers["x-api-key"] =
activeCredentials?.apiKey || activeCredentials?.accessToken || "";
}
delete headers["X-Stainless-Helper-Method"];
// OS/arch follow the host running the signed binary. Runtime version
@@ -1272,7 +1246,7 @@ export class BaseExecutor {
// (tool_result must be in immediately next message).
// Only apply for Claude/Claude-compatible — OpenAI allows results
// spread across multiple subsequent messages.
const isClaude = this.provider === "claude" || usesClaudeCodeProtocol;
const isClaude = this.provider === "claude" || isClaudeCodeCompatible(this.provider);
// For Claude, fixToolAdjacency may strip tool_use blocks whose
// tool_result isn't in the next message; re-run fixToolPairs to
// drop any tool_result orphaned by that strip (discussion #2410).
@@ -1291,7 +1265,7 @@ export class BaseExecutor {
// at this final dispatch point — the single chokepoint every Claude
// routing mode (grouped/raw/combo) and the native passthrough share,
// before fingerprinting and CCH signing serialize the body.
if (this.provider === "claude" || usesClaudeCodeProtocol) {
if (this.provider === "claude" || isClaudeCodeCompatible(this.provider)) {
enforceThinkingTemperature(transformedBody as Record<string, unknown>);
}
@@ -1308,7 +1282,7 @@ export class BaseExecutor {
// `contextEditingDisabled` (set by the 400-fallback) suppresses re-injection
// when a fresh `transformedBody` is built for a retry/fallback URL.
if (
(this.provider === "claude" || usesClaudeCodeProtocol) &&
(this.provider === "claude" || isClaudeCodeCompatible(this.provider)) &&
contextEditing?.enabled &&
!contextEditingDisabled
) {
@@ -1324,17 +1298,17 @@ export class BaseExecutor {
let bodyString = JSON.stringify(transformedBody);
const shouldFingerprint =
isCliCompatEnabled(fingerprintProvider) ||
isCliCompatEnabled(this.provider) ||
(this.provider === "claude" && (isClaudeCodeClient || hasClaudeOAuthToken));
if (shouldFingerprint) {
const fingerprinted = applyFingerprint(fingerprintProvider, headers, transformedBody);
const fingerprinted = applyFingerprint(this.provider, headers, transformedBody);
finalHeaders = fingerprinted.headers;
bodyString = fingerprinted.bodyString;
}
// CCH signing — replaces the cch=00000 placeholder in the billing
// header with an xxHash64 integrity token over the serialized body.
if (usesClaudeCodeProtocol || this.provider === "claude") {
if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") {
bodyString = await signRequestBody(bodyString);
}
@@ -1399,13 +1373,6 @@ export class BaseExecutor {
recordFreeWindowAttempt(openrouterFreeWindowAccountKey);
}
// WAF burst guard: agentrouter.org's content filter becomes more
// aggressive after rapid requests. Enforce a small inter-request gap
// to avoid tripping it. See open-sse/services/wafRateLimit.ts.
if (this.provider === "agentrouter") {
await gateOutboundRequest(`agentrouter:${url}`);
}
let response = await fetchWithStartTimeout(url, fetchOptions);
if (openrouterFreeWindowAccountKey) {
@@ -1429,7 +1396,7 @@ export class BaseExecutor {
contextEditingDisabled = true;
delete (transformedBody as Record<string, unknown>).context_management;
let retryBody = JSON.stringify(transformedBody);
if (usesClaudeCodeProtocol || this.provider === "claude") {
if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") {
retryBody = await signRequestBody(retryBody);
}
log?.debug?.(
@@ -1468,7 +1435,7 @@ export class BaseExecutor {
thinkingBudgetClampedMax = upstreamMax;
if (clampNestedThinkingBudget(transformedBody, upstreamMax)) {
let retryBody = JSON.stringify(transformedBody);
if (usesClaudeCodeProtocol || this.provider === "claude") {
if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") {
retryBody = await signRequestBody(retryBody);
}
log?.info?.(
@@ -1499,7 +1466,7 @@ export class BaseExecutor {
strippedFields.add(offending);
delete (transformedBody as Record<string, unknown>)[offending];
let retryBody = JSON.stringify(transformedBody);
if (usesClaudeCodeProtocol || this.provider === "claude") {
if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") {
retryBody = await signRequestBody(retryBody);
}
log?.debug?.(
@@ -1524,7 +1491,7 @@ export class BaseExecutor {
addParamToBlocklist(this.provider, autoLearned, model);
delete (transformedBody as Record<string, unknown>)[autoLearned];
let retryBody = JSON.stringify(transformedBody);
if (usesClaudeCodeProtocol || this.provider === "claude") {
if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") {
retryBody = await signRequestBody(retryBody);
}
log?.info?.(
@@ -1543,34 +1510,6 @@ export class BaseExecutor {
}
}
// Intra-URL retry: agentrouter.org WAF returns 400 content-blocked
// intermittently (burst-sensitive, recovers after cooldown). Retry the
// same URL with exponential backoff before falling through to the
// 429/401/fallback chain. See docs/security/AGENTROUTER_WAF.md.
if (
!skipUpstreamRetry &&
response.status === HTTP_STATUS.BAD_REQUEST &&
(retryAttemptsByUrl[urlIndex] ?? 0) < BaseExecutor.WAF_RETRY_CONFIG.maxAttempts
) {
const wafErrText = await response
.clone()
.text()
.catch(() => "");
if (/content[_-]blocked/i.test(wafErrText)) {
retryAttemptsByUrl[urlIndex] = (retryAttemptsByUrl[urlIndex] ?? 0) + 1;
const wafAttempt = retryAttemptsByUrl[urlIndex];
const wafBackoff = BaseExecutor.WAF_RETRY_CONFIG.delayMs *
Math.pow(BaseExecutor.WAF_RETRY_CONFIG.backoffMultiplier, wafAttempt - 1);
log?.debug?.(
"WAF_RETRY",
`400 content-blocked intra-retry ${wafAttempt}/${BaseExecutor.WAF_RETRY_CONFIG.maxAttempts} on ${url} — waiting ${wafBackoff}ms`
);
await new Promise((resolve) => setTimeout(resolve, wafBackoff));
urlIndex--; // re-run this urlIndex on the next loop iteration
continue;
}
}
// Intra-URL retry: if 429 and we haven't exhausted per-URL retries, wait and retry the same URL
if (
!skipUpstreamRetry &&

View File

@@ -1,69 +0,0 @@
import { BaseExecutor, type ProviderCredentials } from "./base.ts";
import { PROVIDERS } from "../config/constants.ts";
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
/**
* CheaperInferenceExecutor — api.cheaperinference.com.
*
* The gateway is OpenAI-compatible on both surfaces, so everything else comes from
* BaseExecutor. Two provider-specific facts need handling (both measured against the
* live API on 2026-07-31, not inferred from docs):
*
* 1. `/v1/responses` is STATELESS and REQUIRES `store:false`. Omitting it returns
* HTTP 400 ("This Responses-compatible endpoint is stateless. Send store=false…").
* chatCore.ts deletes `store` for every provider except "openai" — a strip shared
* by ~290 providers that must not be special-cased — so we re-add it here, after
* that strip has run. A client-supplied `store:true` is overwritten rather than
* forwarded: the endpoint cannot honour it, and forwarding would 400.
*
* 2. Chat and Responses live at DIFFERENT URLs (unlike providers that switch on a
* path suffix). The per-model `targetFormat` registry tag is the single source of
* truth for which surface a model uses — the same tag chatCore reads to translate
* the body — so resolving the URL from it keeps URL and payload in lockstep.
* Same pattern as executors/xai.ts (9router#2439).
*/
export class CheaperInferenceExecutor extends BaseExecutor {
constructor(provider = "cheaperinference") {
super(provider, PROVIDERS[provider]);
}
/**
* True when this model is served by the native /v1/responses endpoint.
*
* PROVIDER_MODELS is keyed by provider ALIAS ("cinf"), while PROVIDERS is keyed by
* provider ID ("cheaperinference") — so `this.provider` cannot be passed straight
* through the way executors/xai.ts does (there the alias equals the id, which hides
* the distinction). Resolve the alias first or every lookup silently returns null
* and every Responses request 400s upstream.
*/
private usesResponsesEndpoint(model: string): boolean {
const alias = PROVIDER_ID_TO_ALIAS[this.provider] || this.provider;
return getModelTargetFormat(alias, model) === "openai-responses";
}
buildUrl(model: string, _stream: boolean, _urlIndex = 0): string {
if (this.usesResponsesEndpoint(model)) {
return this.config.responsesBaseUrl || this.config.baseUrl;
}
return this.config.baseUrl;
}
transformRequest(
model: string,
body: unknown,
stream: boolean,
credentials: ProviderCredentials
): unknown {
const cleanedBody = super.transformRequest(model, body, stream, credentials);
if (!cleanedBody || typeof cleanedBody !== "object" || Array.isArray(cleanedBody)) {
return cleanedBody;
}
if (!this.usesResponsesEndpoint(model)) {
// Chat Completions rejects unknown params — never add `store` on that surface.
return cleanedBody;
}
return { ...(cleanedBody as Record<string, unknown>), store: false };
}
}
export default CheaperInferenceExecutor;

View File

@@ -55,7 +55,6 @@ import { forwardOpencodeClientHeaders } from "../utils/opencodeHeaders.ts";
import { resolveZaiUrl } from "./default/zaiFormatOverride.ts";
import { acquireNvidiaConcurrencySlot } from "./default/nvidiaConcurrencyGate.ts";
import { resolveAlibabaProviderBaseUrl } from "@/shared/constants/alibabaProviderRegions";
import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts";
import type { PoolConfig } from "../services/sessionPool/types.ts";
@@ -292,10 +291,6 @@ export class DefaultExecutor extends BaseExecutor {
case "minimax":
case "minimax-cn":
return `${this.config.baseUrl}?beta=true`;
case "agentrouter":
return this.usesClaudeCodeProtocol(credentials)
? `${this.config.baseUrl}?beta=true`
: this.config.baseUrl;
case "gemini":
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
default: {
@@ -418,7 +413,7 @@ export class DefaultExecutor extends BaseExecutor {
applyClineAuthHeaders(headers, credentials, effectiveKey, clientHeaders, false);
break;
default:
if (this.usesClaudeCodeProtocol(credentials)) {
if (isClaudeCodeCompatible(this.provider)) {
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
credentials?.providerSpecificData
);
@@ -428,10 +423,6 @@ export class DefaultExecutor extends BaseExecutor {
credentials?.providerSpecificData?.ccSessionId,
{ redactThinking: ccRequestDefaults.redactThinking === true }
);
if (usesCcWireImage(this.provider)) {
delete ccHeaders["Authorization"];
ccHeaders["x-api-key"] = effectiveKey || credentials.accessToken || "";
}
// CC nodes are also anthropic-compatible-*, so honor operator custom
// headers here (the early return skips the shared block below).
applyCustomHeaders(ccHeaders, credentials.providerSpecificData?.customHeaders);

View File

@@ -50,7 +50,6 @@ import { PoeWebExecutor } from "./poe-web.ts";
import { VeniceWebExecutor } from "./venice-web.ts";
import { NotionWebExecutor } from "./notion-web.ts";
import { V0VercelWebExecutor } from "./v0-vercel-web.ts";
import { CheaperInferenceExecutor } from "./cheaperinference.ts";
import { KimiWebExecutor } from "./kimi-web.ts";
import { DoubaoWebExecutor } from "./doubao-web.ts";
import { QwenWebExecutor } from "./qwen-web.ts";
@@ -166,8 +165,6 @@ const executors = {
"kimi-coding": new KimiExecutor(), // Alias
moonshot: new MoonshotExecutor(),
kimi: new MoonshotExecutor("kimi"), // Hidden legacy Moonshot provider id
cheaperinference: new CheaperInferenceExecutor(),
cinf: new CheaperInferenceExecutor("cheaperinference"), // Alias
"doubao-web": new DoubaoWebExecutor(),
db: new DoubaoWebExecutor(), // Alias
"qwen-web": new QwenWebExecutor(),
@@ -285,5 +282,4 @@ export { ZenmuxFreeExecutor } from "./zenmux-free.ts";
export { HyperAgentExecutor } from "./hyperagent.ts";
export { XaiExecutor } from "./xai.ts";
export { MoonshotExecutor } from "./moonshot.ts";
export { CheaperInferenceExecutor } from "./cheaperinference.ts";
export { PromptQlExecutor } from "./promptql.ts";

View File

@@ -71,7 +71,6 @@ import { checkHeapPressureGuard } from "../utils/heapPressure.ts";
import { normalizeHeaders } from "../utils/headers.ts";
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts";
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
import { translateRequest, needsTranslation } from "../translator/index.ts";
@@ -342,6 +341,7 @@ import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import {
buildClaudeCodeCompatibleRequest,
isClaudeCodeCompatibleProvider,
resolveClaudeCodeCompatibleSessionId,
} from "../services/claudeCodeCompatible.ts";
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
@@ -751,7 +751,6 @@ export async function handleChatCore({
provider,
resolvedModel,
apiFormat,
sourceFormat,
customModelTargetFormat,
providerSpecificData: credentials?.providerSpecificData,
});
@@ -1921,7 +1920,7 @@ export async function handleChatCore({
let translatedBody = body;
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
const isClaudeCodeCompatible = usesClaudeBridge(provider, targetFormat, credentials);
const isClaudeCodeCompatible = isClaudeCodeCompatibleProvider(provider);
const isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
provider,
sourceFormat,
@@ -2497,7 +2496,10 @@ export async function handleChatCore({
log?.debug?.("PARAMS", `Renamed max_completion_tokens to max_tokens for ${model}`);
}
stripStore(translatedBody, provider, targetFormat);
// OpenAI's `store` parameter is not supported by most compatible providers and breaks them
if (provider !== "openai" && "store" in translatedBody) {
delete translatedBody.store;
}
// Chat clients may send stream_options.include_usage, but OpenAI Responses
// upstreams (including Azure AI Foundry /responses) reject stream_options.

View File

@@ -1,35 +0,0 @@
/**
* Per-request AgentRouter protocol decisions kept outside the chatCore orchestration god-file.
* AgentRouter exposes Claude Messages, OpenAI Chat, and OpenAI Responses as distinct upstream
* protocols, so its dynamic target format must override the connection's default Claude wire image.
*/
import { isClaudeCodeCompatibleProvider } from "../../services/claudeCodeCompatible.ts";
import { FORMATS } from "../../translator/formats.ts";
export function usesClaudeBridge(
provider: string,
targetFormat: string,
credentials: unknown
): boolean {
const configuredTargetFormat = (
credentials as { providerSpecificData?: { targetFormat?: unknown } | null } | null | undefined
)?.providerSpecificData?.targetFormat;
const effectiveFormat = provider === "agentrouter" ? targetFormat : configuredTargetFormat;
return (
isClaudeCodeCompatibleProvider(provider) &&
effectiveFormat !== FORMATS.OPENAI &&
effectiveFormat !== FORMATS.OPENAI_RESPONSES
);
}
export function stripStore(
body: Record<string, unknown>,
provider: string,
targetFormat: string
): void {
const supportsStore =
provider === "openai" ||
(provider === "agentrouter" && targetFormat === FORMATS.OPENAI_RESPONSES);
if (!supportsStore && "store" in body) delete body.store;
}

View File

@@ -5,8 +5,8 @@
* Pure builder extracted from handleChatCore: derives the per-execution credentials object from the
* resolved request context. Applies the native-Codex passthrough endpoint override, forces
* apiType=responses (and the responses-upstream marker) for Azure AI Foundry / OCI when the model
* routes to the OpenAI Responses format, synchronizes AgentRouter's per-request alternate protocol,
* and threads the Claude Code session id when present. Side-effect-free.
* routes to the OpenAI Responses format, and threads the Claude Code session id when present.
* Side-effect-free; behaviour is byte-identical to the previous inline closure.
*/
import { getKimiCodeStaticThinkingPolicy } from "../../config/providers/registry/kimi/coding/runtime.ts";
@@ -128,16 +128,6 @@ export function resolveExecutionCredentials(opts: {
providerSpecificData.targetFormat = targetFormat;
}
// AgentRouter exposes Claude, OpenAI Chat, and OpenAI Responses on distinct URLs with distinct
// auth schemes. Keep the executor's URL/header resolution synchronized with chatCore's resolved
// per-request protocol without persisting the inferred selection back to the connection.
if (
provider === "agentrouter" &&
(targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES)
) {
providerSpecificData.targetFormat = targetFormat;
}
applyKimiExecutionMetadata(providerSpecificData, provider, targetFormat, modelInfo);
const withApiType = {

View File

@@ -4,11 +4,10 @@
*
* Pure resolution of the provider alias + the upstream target format used to translate the request:
* apiFormat==="responses" forces OpenAI Responses; otherwise the model's registry target format, then
* the per-model custom override (#2905), then AgentRouter's matching inbound protocol when the
* connection has no explicit override, then the provider default. Returns both `alias` (reused by
* the per-model custom override (#2905), then the provider default. Returns both `alias` (reused by
* the handler when stripping the `alias/` prefix off the upstream model id) and `targetFormat`.
* Side-effect-free; sits alongside the other request-setup resolvers
* (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
* Side-effect-free; byte-identical to the previous inline block. Sits alongside the other
* request-setup resolvers (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
*/
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts";
@@ -19,38 +18,16 @@ export function resolveChatCoreTargetFormat(opts: {
provider: string;
resolvedModel: string;
apiFormat: string | undefined;
sourceFormat?: string;
customModelTargetFormat: string | undefined;
providerSpecificData: unknown;
}) {
const {
provider,
resolvedModel,
apiFormat,
sourceFormat,
customModelTargetFormat,
providerSpecificData,
} = opts;
const { provider, resolvedModel, apiFormat, customModelTargetFormat, providerSpecificData } = opts;
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
const explicitConnectionTargetFormat = (
providerSpecificData as { targetFormat?: unknown } | null | undefined
)?.targetFormat;
const inferredAgentRouterTargetFormat =
provider === "agentrouter" &&
!(typeof explicitConnectionTargetFormat === "string" && explicitConnectionTargetFormat) &&
(sourceFormat === FORMATS.OPENAI_RESPONSES ||
sourceFormat === FORMATS.OPENAI ||
sourceFormat === FORMATS.CLAUDE)
? sourceFormat
: undefined;
const targetFormat =
apiFormat === "responses"
? FORMATS.OPENAI_RESPONSES
: modelTargetFormat ||
customModelTargetFormat ||
inferredAgentRouterTargetFormat ||
getTargetFormat(provider, providerSpecificData);
: modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
return { alias, targetFormat };
}

View File

@@ -1,6 +1,6 @@
{
"name": "@omniroute/open-sse",
"version": "3.8.50",
"version": "3.8.49",
"description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
"type": "module",
"main": "index.js",

View File

@@ -1,68 +0,0 @@
// open-sse/services/combo/strategyDispatch.ts
// Runtime source of truth for the known-symbols combo gate (G1).
//
// HISTÓRICO: `scripts/check/check-known-symbols.ts` (seção 2) costumava descobrir quais
// estratégias de roteamento têm branch de despacho lendo a fonte dos arquivos do combo e
// extraindo literais `strategy === "..."` por regex. Isso quebra quando o despacho vira um
// registry (R0.3): não há mais `strategy === "X"` para casar. Este módulo substitui essa
// enumeração por regex-over-source por uma enumeração EXPLÍCITA em runtime, colada ao lado
// do código de despacho real.
//
// Importamos as funções reais de ordenação/despacho (não apenas strings) para amarrar a
// enumeração ao código vivo: se a maquinaria de despacho for reestruturada ou um módulo
// quebrar, a importação falha no load do gate em vez de casar silenciosamente uma regex
// obsoleta. As chaves em HANDLED_COMBO_STRATEGIES DEVEM casar exatamente o conjunto
// canônico (ROUTING_STRATEGY_VALUES INTERNAL_ROUTING_STRATEGY_VALUES).
//
// Ao adicionar uma estratégia canônica, fie-a no despacho (aqui ou em combo.ts) e inclua-a
// nesta lista; ao remover um branch, retire a entrada — o gate acusa qualquer divergência
// nas duas direções (canonicalSemDespacho / despachoNaoCanonico).
import { applyStrategyOrdering } from "./applyStrategyOrdering.ts";
import { resolveAutoStrategyOrder } from "./resolveAutoStrategy.ts";
import { tryFusionDispatch, tryPipelineDispatch } from "./dispatchPrelude.ts";
import { resolveComboTargetPipeline } from "./targetResolution.ts";
/**
* As funções reais que implementam o despacho/ordenação de estratégias. Referenciadas
* aqui para (a) provar ao gate que a maquinaria resolve e (b) servir de âncora viva para
* a enumeração abaixo — o registry que o R0.3 vai passar a consumir para o branching
* `strategy === ...` nasce destas mesmas funções.
*/
export const COMBO_STRATEGY_DISPATCH_LEAVES = {
applyStrategyOrdering,
resolveAutoStrategyOrder,
tryFusionDispatch,
tryPipelineDispatch,
resolveComboTargetPipeline,
} as const;
/**
* Conjunto exato de estratégias de roteamento que possuem implementação de despacho real.
*
* Cobertura esperada (em `main` do gate): este set IMPLICIT_DEFAULT_STRATEGIES deve
* igualar o canônico. Atualmente todas as 20 estratégias canônicas têm branch — então
* HANDLED_COMBO_STRATEGIES já contém as 20 e IMPLICIT_DEFAULT_STRATEGIES está vazio.
*/
export const HANDLED_COMBO_STRATEGIES: readonly string[] = [
"priority",
"weighted",
"round-robin",
"context-relay",
"fill-first",
"p2c",
"random",
"least-used",
"cost-optimized",
"reset-aware",
"reset-window",
"headroom",
"strict-random",
"auto",
"lkgp",
"context-optimized",
"cache-optimized",
"fusion",
"pipeline",
"quota-share",
] as const;

View File

@@ -120,44 +120,7 @@ for (const [aliasOrId, models] of Object.entries(PROVIDER_MODELS)) {
}
}
const KNOWN_MODEL_IDS = new Set(MODEL_TO_PROVIDERS.keys());
// Bare Codex CLI defaults must always route to the `codex` provider (chatgpt.com
// OAuth) even when other providers that also catalog the model id (e.g.
// `agentrouter`, `openai`) are active. The Codex cookie quota on the user's
// account is the source of truth for capacity, and bare-id requests from
// `codex` (CLI)/`Codex` (web) would otherwise silently fan out to whichever
// provider won the inference race — leaving the user wondering why the
// canonical ChatGPT subscription stopped working. Override per-request by
// prefixing the model id (e.g. `agentrouter/gpt-5.6-sol`,
// `openai/gpt-5.6-sol`) — the prefix path always wins.
export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([
"codex-auto-review",
"gpt-5.6-sol",
"gpt-5.6-sol-ultra",
"gpt-5.6-sol-max",
"gpt-5.6-sol-xhigh",
"gpt-5.6-sol-high",
"gpt-5.6-sol-medium",
"gpt-5.6-sol-low",
"gpt-5.6-terra",
"gpt-5.6-terra-ultra",
"gpt-5.6-terra-max",
"gpt-5.6-terra-xhigh",
"gpt-5.6-terra-high",
"gpt-5.6-terra-medium",
"gpt-5.6-terra-low",
"gpt-5.6-luna",
"gpt-5.6-luna-max",
"gpt-5.6-luna-xhigh",
"gpt-5.6-luna-high",
"gpt-5.6-luna-medium",
"gpt-5.6-luna-low",
"gpt-5.5",
"gpt-5.5-xhigh",
"gpt-5.5-high",
"gpt-5.5-medium",
"gpt-5.5-low",
"gpt-5.3-codex-spark",
]);
export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set(["codex-auto-review"]);
interface ProviderConnectionLike {
provider?: unknown;
@@ -571,19 +534,7 @@ async function resolveModelByProviderInference(modelId: string, extendedContext:
getActiveSyncedProvidersForModel(modelId),
getPreferClaudeCodeForUnprefixedClaudeModels(),
]);
// #FIX: synced catalogs (populated from `/v1/models` per connection) can
// claim ownership of models the provider does not actually serve (e.g. a
// `kiro` upstream briefly advertising `claude-opus-5` before it was
// vendored into the registry). Without this filter the bare-routing path
// would forward traffic to providers that 404 on the upstream call.
// Auto-discovery still wins when no static registry entry exists for the
// model id — only entries that conflict with the static catalog are dropped.
const staticCatalogProviders = MODEL_TO_PROVIDERS.get(modelId) || [];
const validatedSyncedProviders =
staticCatalogProviders.length > 0
? activeSyncedProviders.filter((p) => staticCatalogProviders.includes(p))
: activeSyncedProviders;
const providers = getInferredProvidersForModel(modelId, validatedSyncedProviders);
const providers = getInferredProvidersForModel(modelId, activeSyncedProviders);
const nonOpenAIProviders = providers.filter((p) => p !== "openai");
// Bare model IDs from Codex CLI do not preserve OmniRoute's `cx/` prefix.

View File

@@ -109,50 +109,15 @@ export const REFRESH_LEAD_MS: Record<string, number> = {
"gitlab-duo": 5 * 60 * 1000, // GitLab token family revocation on misuse
kiro: 5 * 60 * 1000, // AWS SSO OIDC issues one-time-use refresh tokens
"kimi-coding": 5 * 60 * 1000, // Moonshot rotates per-refresh
// Non-rotating providers — longer lead is safe.
iflow: 24 * 60 * 60 * 1000, // 24 hours
// Google OAuth refresh_tokens are permanent (non-rotating) — longer lead
// is safe and reduces unnecessary upstream chatter.
antigravity: 15 * 60 * 1000,
agy: 15 * 60 * 1000, // same Google backend as antigravity (non-rotating refresh tokens)
"gemini-cli": 15 * 60 * 1000, // legacy stored connections; provider is no longer public
};
/**
* Upstream providers that stored connections may still name, but that this build no
* longer serves. They are NOT routable — absent from PROVIDERS, from the chat REGISTRY,
* and without an executor — so keeping their token fresh maintains a credential that can
* never answer a request.
*
* Deprecation, not deletion: a connection here becomes terminal with a reason that names
* where to go instead, rather than silently sitting at `active` doing nothing. The
* migration target must be routable — `tests/unit/gemini-cli-deprecation.test.ts` asserts
* that, so the notice can never point somewhere useless.
*/
export const DEPRECATED_PROVIDERS: Readonly<
Record<string, { readonly migrateTo: string; readonly reason: string }>
> = {
"gemini-cli": {
migrateTo: "gemini",
// The legacy path redeemed the token with PROVIDERS.gemini's client — the very same
// public Gemini CLI / Code Assist OAuth client — which is why re-adding the account
// under `gemini` is a real migration and not a suggestion to start over.
reason:
"The gemini-cli provider was discontinued and is not routable. Re-add this account " +
"under the `gemini` provider — it uses the same Google OAuth client, so the same " +
"login works and the account becomes usable again.",
},
};
/** Whether `provider` is a deprecated upstream that must not be refreshed. */
export function isDeprecatedProvider(provider: string): boolean {
return Boolean(provider) && Object.prototype.hasOwnProperty.call(DEPRECATED_PROVIDERS, provider);
}
/** The migration notice for a deprecated provider, or null when it is not deprecated. */
export function getDeprecationNotice(
provider: string
): { migrateTo: string; reason: string } | null {
return isDeprecatedProvider(provider) ? DEPRECATED_PROVIDERS[provider] : null;
}
/**
* Get the proactive refresh lead time (ms) for a given provider.
*
@@ -298,27 +263,17 @@ export async function refreshAccessToken(
*/
async function _getAccessTokenInternal(provider, credentials, log, proxyConfig: unknown = null) {
switch (provider) {
case "gemini-cli": {
// Deprecated (see DEPRECATED_PROVIDERS). This used to refresh successfully against
// PROVIDERS.gemini's client, but the provider is not routable, so the fresh token
// had nowhere to go — periodic upstream calls maintaining an unusable credential.
//
// Return the ESTABLISHED unrecoverable contract, so every existing caller
// (isUnrecoverableRefreshError, the manual-refresh route) already stops retrying —
// but with a code that says WHY and a target to migrate to. A bare `null` here would
// read as a transient failure and be retried forever.
const notice = DEPRECATED_PROVIDERS[provider];
log?.warn?.(
"TOKEN_REFRESH",
`${provider} is deprecated — not refreshing; migrate this account to ${notice.migrateTo}`
case "gemini-cli":
// Legacy DB rows can retain this discontinued provider id. Refresh them
// with the same public OAuth client used by Gemini CLI without restoring
// gemini-cli to the routable provider or OAuth UI registries.
return await refreshGoogleToken(
credentials.refreshToken,
PROVIDERS.gemini.clientId,
PROVIDERS.gemini.clientSecret,
log,
proxyConfig
);
return {
error: "unrecoverable_refresh_error",
code: "provider_deprecated",
migrateTo: notice.migrateTo,
reason: notice.reason,
};
}
case "gemini":
case "antigravity":
@@ -435,6 +390,7 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:
export function supportsTokenRefresh(provider) {
const explicitlySupported = new Set([
"gemini",
"gemini-cli", // legacy refresh compatibility only; not a routable provider
"antigravity",
"agy",
"claude",

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