The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.
Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.
Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:
- RequestLoggerDetail crashed on a structured error object. #7920 gave the component
formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown
checks went to the raw field and called .toLowerCase() on it. Both paths now use
the helper.
- The logs detail modal reopened on first close again. #6830 fixed that by reading the
deep-link id ONCE; the #8354 page rewrite regressed it by reading the live
searchParams every render, so the prop flips mid-session and re-fires the child's
deep-link effect exactly as the modal closes. Frozen at mount again.
Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.
Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.
Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.
Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.
i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.
PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the #7866 Qwen OAuth provider removal and the
#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.
Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.
Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.
Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.
SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.
Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:
- CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the
GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008):
the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback}
had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873)
were missing from the API reference
README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861).
check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).
Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (2c62333b0..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.
- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for #7904, which shipped
through the conflict-resolved #8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors
Coverage after: 0 commits uncovered.
Second aggregation pass, for the PRs merged after the main reconciliation:
#8860, #8861, #8862, #8863, #8865, #8866.
The `### 📝 Maintenance` heading added to the living section in 26a7783521 held —
[3.8.47] stays at its 234 bullets instead of absorbing this cycle's maintenance
entries. Credits attached where the fragment carried the number only in its
filename: @HouMinXi (#8860, #8865), @artickc (#8863), @rafaeldrincon (#8866).
#8862 already carried its own link and credit.
[3.8.49]: 1172 -> 1178 bullets · contributors 155 -> 156 · 42 i18n mirrors synced.
Aggregates every pending changelog.d fragment into the [3.8.49] section and
regenerates the contributors table from the reconciled bullets.
Three fixes this surfaced:
- The [3.8.49] section had no `### 📝 Maintenance` heading, so the aggregator's
findIndex matched the first one in the file — inside [3.8.47] — and would have
filed 92 maintenance bullets under the wrong release. Added the heading to the
living section; [3.8.47] stays at its original 234 bullets.
- 46 bullets carried no PR/issue reference. Fragments may keep the number only in
the filename (`<N>-slug.md`), which the aggregator does not copy into the bullet,
so the link and the credit were dropped on aggregation. Restored, scoped strictly
to the [3.8.49] range.
- 9 external contributors lost their attribution that way and are credited again:
@MisileLab (#8566), @MumuTW (#8619), @epsilonode (#8724), @hppsc1215 (#8835),
@sumanxg (#8837, #8856), @TitoTFP (#8838), @HouMinXi (#8842, #8845).
Contributors table: 84 → 155 entries, no one removed. 42 i18n mirrors synced.
check:changelog-integrity green — no base bullet lost.
* docs: restore the Polish API_REFERENCE removed by #8823
`docs/reference/API_REFERENCE.md` lists Polish in its language index, but the
target file no longer exists: #8823 ("replace outdated Polish docs with
translation from latest English") deleted
`docs/i18n/pl/docs/reference/API_REFERENCE.md` without writing a replacement.
Every other one of the 31 linked languages still has the file.
That leaves `check:doc-links` red — and because the Docs Gates job only runs on
pull requests, the branch itself reports green while carrying the break. The
first PR opened afterwards is what surfaces it, and it blocks every PR until
fixed.
Restored from `71c5a7592^`. The content is the pre-#8823 translation, which is
by definition outdated relative to the English source — but an outdated
translation behind a working link is strictly better than a 404, and the next
translation pass will overwrite it. `check:doc-links` now passes (813 internal
links, 0 broken).
The same commit also removed four other Polish files
(`cloudflare-zero-trust-guide.md`, `features/context-relay.md`,
`reference/CLI-TOOLS.md`, `reference/ENVIRONMENT.md`). None of them is linked
from a scanned index, so none breaks the gate; they are noted here so a later
translation pass can decide whether they should come back too.
* chore(quality): freeze the pre-existing exhaustive-deps error in ProviderAccountRoutingCard
Second half of unbreaking the base. `lint:json --max-warnings 0` fails on
`ProviderAccountRoutingCard.tsx:87` — `save` calls `load()` but declares only
`[providerKey]`. Same root cause as the broken doc link in the previous commit:
the Lint job is skipped on pushes to `release/**`, so the branch reports green
while carrying the violation, and every PR inherits it.
Frozen rather than fixed: adding `load` to the dependency array changes when
the callback is recreated in a settings component, and that belongs in a change
that can verify the card's behaviour. The entry keeps the gate meaningful for
genuinely new warnings instead of leaving it red for everyone.
With this and the restored Polish file, both gates pass on the base again.
Strict contextFilterMode excluded every target whose context limit was missing
from the capability catalog, so otherwise-executable combos returned 404
no_executable_targets. Restore unknown-context targets when no known-good
survivor remains, surface context_requirements_exhausted from targetResolution,
and keep the empty-pool payload in pinRecovery after the #8592 split.
* fix(sse): compact Responses multi-turn images before context hard-reject (#8560)
Codex Desktop sessions near the 372k input cap were rejected on the second
inline image because compressContext no-op'd on Responses input[] and never
pruned older vision turns. Adapt via bodyAdapter, prune older images while
keeping the latest, and run last-resort compaction before the budget check.
* docs(env): document CONTEXT_KEEP_LATEST_IMAGES for #8560
Keep check:env-doc-sync green after the context image-pruning override.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(docker): honor OMNIROUTE_BASE_PATH behind reverse-proxy subpaths
Next.js basePath is compile-time state; Docker now records the baked value,
forwards the env var as a build-arg, patches root-path images at container
start when needed, and probes health under the active subpath.
Hard Rule #13: scripts/docker/patch-basepath.sh and the entrypoint invoke Node
with a fixed argv; OMNIROUTE_BASE_PATH is read from process.env only — never
interpolated into sed/awk.
Closes#8600
* fix(docs): unblock CI for Docker basePath guide
Describe the build-time basePath marker as a sentinel file instead of a
fabricated env var, and replace the unsupported ```env fence with bash so
fumadocs/Shiki can compile DOCKER_GUIDE.md during DAST smoke.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(docker): add changelog fragment for #8615 basePath bundle patch
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(ci): automate ratchet shrink-banking on the release branch (#8584)
The quality ratchet is only half automatic, and it is the wrong half. Raising a
cap is a manual JSON edit that takes ten seconds and is the fastest way to unblock
a red PR. Lowering one requires someone to run `--update` and commit the result —
and no workflow does: grepping `.github/workflows/` for `--update` finds only
wiki-sync.yml (unrelated) and ci.yml's check-quality-ratchet.mjs --require-tighten
(a different script against a different metric).
Measured on release/v3.8.49 at 4053e2314: 18 frozen files already at or under the
800-line new-file cap, the worst at 132x (src/shared/validation/schemas.ts, 19
lines carrying a 2,523 cap); the complexity ceiling walked 1794 -> 2169 across ~37
rebaseline notes with exactly one decrease (-1); "tighten via --update next cycle"
written 31 times and honoured once in six weeks. A cap that outlives the code that
earned it converts every completed decomposition into a growth allowance for
whoever edits that file next.
New job `bank-ratchet-shrinks` in nightly-release-green.yml measures the active
release branch, runs the two shrink-only `--update` paths, and opens ONE
always-current PR with the result. Schedule/dispatch only, deliberately not on
push: banking has no latency requirement, while a per-merge run would rebuild the
PR branch during merge campaigns and pay for a full ESLint walk each time.
Detection stays on push (release-green); only banking is batched. The job never
pushes to release/* — a human merges, so a bad measurement cannot land unreviewed.
scripts/quality/verify-ratchet-bank.mjs is the hard guarantee that the automation
can only ever write downward. It diffs the post-`--update` tree against HEAD and
aborts the job before a commit exists — opening no PR — unless every change is a
frozen/testFrozen entry lowered or removed, `count` lowered, or
cognitiveComplexity.value lowered. Raising a number, adding an entry, changing
cap/testCap, or deleting/rewriting a `_rebaseline_*` note all fail. A bot that
could raise a cap would be strictly worse than the status quo.
Verified both directions against the real baselines: --update + verifier reports
77 lowered / 14 removed / nothing raised, exit 0; hand-raising chatCore.ts to 9999
and cap to 1200 is rejected with exit 1. 22 unit tests cover each way the
automation could go wrong.
No product code changes.
* chore(skills): sync cli-backup-sync SKILL.md after rebase onto tip
* fix(cli): ensure `~/.cache` is created and `XDG_CACHE_HOME` is set before Next.js loads on Android/Termux to prevent silent HTTP 500 errors due to instrumentation hook failures (#8519)
* chore(quality): ignore XDG_CACHE_HOME in the env/docs contract scanner
XDG_CACHE_HOME is an XDG Base Directory spec variable set by the OS or the
operator, never OmniRoute product config — the same reason XDG_CONFIG_HOME is
already ignored. The Android/Termux cache-dir preparation added here reads it
to honor an operator-set cache location, which made check-env-doc-sync demand
an .env.example/ENVIRONMENT.md entry for a variable we do not own.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* build(pack): require bin/cli/utils/ensureAndroidCacheDir.mjs in the tarball
bin/omniroute.mjs imports this module at startup to prepare the Next.js cache
dir before serve on Android/Termux. bin/cli/ is only an allowlist PREFIX, so a
file missing from the tarball would not fail the unexpected-paths check — it
would ship a CLI that throws ERR_MODULE_NOT_FOUND on the very platform this
change targets. Registering it makes the absence loud, same guard class as
storageKeyProvision.mjs and versionFastPath.mjs.
Caught by tests/unit/pack-artifact-entrypoint-closures.test.ts in the v3.8.49
merge-train.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Closes the gap that let #8463 ship. `oauthModal.googleOAuthWarning`'s English value
was rewritten when the Antigravity login helper landed (#5203); 39 of 43 locales kept a
translation of the PREVIOUS English, which told operators to "copy the full URL and
paste it below" — a flow that cannot complete for that provider family. Non-English
users read confident, wrong instructions for months and no gate noticed.
None of the three existing gates can see this class:
- `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE;
- `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as
fully covered (all 43 locales sat at 99.6% throughout);
- `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation
mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is
also absent, so it self-skips, but bootstrapping it would not have helped: wrong
surface.)
New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than
baseline-backed: it compares `en.json` at the merge base against the working tree, and
for every key whose English value changed, reports any locale still holding an untouched
translation.
That choice deliberately freezes pre-existing debt — a diff cannot reveal which old
English a long-standing translation came from, so the gate judges only what the current
change touches, and unrelated PRs never pay for historical drift. The alternative, a
per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the
largest existing baseline) churning on every i18n PR.
Two ways to satisfy it: refresh the translations, or set them to
`__MISSING__:<new english>` so the runtime serves the corrected English (#7258) while
the key queues for translation. When the string's MEANING changes, renaming the key is
better still — a new key cannot inherit a stale translation, which is what #8463 did.
Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is
`continue-on-error: true`, so a gate there could not block anything). That job gains
`fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with
`base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:`
and reaches git only through `execFileSync` argv — never a shell string.
Verified against the real defect: rewriting an English value with translations left
behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable
base exits 0 with `SKIP reason=base-unresolved`.
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
* docs(codex): document session affinity and stream idle for long tasks
Operators running multi-hour Codex sessions need both knobs spelled out:
sessionAffinityTtlMs (default off) and STREAM_IDLE_TIMEOUT_MS (10 min),
with a concrete recipe and an explicit keep-defaults decision (#7287).
* fix(ci): keep #7287 docs-only so base-red gates stay skipped
Drop the unit content-guard that classified the PR as code (triggering
i18n/unit/eslint/dast on a red release tip). Sync agent-skills so
check:agent-skills-sync passes (config-codex-cli blank line + stale
cli-backup-sync catalog drift).
The runner reads exactly one directory and records the bare numeric prefix as the
version, so the numeric slots are a single global namespace. Any distribution that
ships its own migrations next to the upstream set has to draw from that same range
while upstream keeps appending to it — and when both sides claim a number, the
runner records one name for it and treats the other as already applied. That
migration then never runs, silently, on every already-provisioned database.
OMNIROUTE_EXTRA_MIGRATIONS_DIRS registers additional directories as
`namespace=dir` entries separated by the platform path delimiter. Files found
there are recorded as `<namespace>-<number>` (e.g. `ee-134`), a version space that
cannot collide with the upstream numeric one, and they are applied after the core
set. Unset — the default, and the only case for a plain install — nothing changes:
no filesystem access, identical behaviour.
Misconfiguration throws instead of being skipped. A malformed entry, a namespace
outside [a-z][a-z0-9]*, a duplicate namespace, or a directory that does not exist
aborts startup, because silently missing schema is the exact failure this exists to
prevent. Two files sharing a number inside the SAME namespace still collide and
throw, mirroring the runner's own guard.
Also fixes an inconsistency the tests surfaced: a missing core directory returned
early and took the extra directories down with it. They are an independent set.
The version-namespaced strings need no further plumbing — the applied set, the gap
reconciliation and the name-mismatch check all key on the version string, and the
numeric-only paths (`Number.parseInt`, the "032"/"041"/"042" special cases,
`isSchemaAlreadyApplied`) ignore them by construction.
11 new tests; the 7 neighbouring migration suites stay green (62 tests).
The discovery-alias info button explains the gate and links to the flag, but the
operator still had to assemble the Claude Code config by hand from the guide.
Render it instead, on the same Claude tool card, from the base URL the card has
already resolved (custom override included, normalized — no /v1, no trailing
slash), with a copy button.
The key slot holds a placeholder, never the real key: this card renders before a
key is necessarily selected, and a real key in copyable text is an easy way to
leak one into a screenshot or a pasted snippet.
buildClaudeDiscoverySettingsSnippet is a pure builder in claudeCliConfig so the
shape is unit-tested (7 cases, including that no `sk-` ever reaches the output
and that an invalid CLAUDE_CODE_AUTO_COMPACT_WINDOW is dropped rather than
emitted). Guide gains the matching section; the five new strings are translated
for pt-BR and vi (the locales with strict parity/marker tests) and marked in the
rest, per the repo's convention.
* feat(db): cc discovery alias gate storage + EXPOSE_CC_DISCOVERY_ALIASES flag
Adds the gate for claude/<provider>/<model> discovery-alias mirror ids on
the /v1/models catalog: a new runtime feature flag (env forces on and wins
over the dashboard DB override), per-provider and per-model "on"/"off"/null
overrides stored in key_value under the ccDiscoveryAliases namespace, and a
pure precedence resolver (model > provider > global). Catalog wiring is a
separate follow-up task; this only lands the gate + storage.
* feat(sse): synthesize claude/ discovery aliases for the model catalog
* feat(api): advertise cc discovery aliases on /v1/models behind the 3-level gate
* feat(sse): resolve claude/ discovery aliases on the request path
* fix(sse): import getComboByName from db/combos, not the localDb barrel
* fix(sse): cover custom-node prefixes and the Codex WS bridge in cc discovery alias resolution
* feat(dashboard): cc discovery alias toggles + flag-screen env warning
Adds the operator-facing UI/API layer for the Claude Code discovery-alias
gate (claude/<provider>/<model> mirror ids on /v1/models): REST endpoint
for provider/model overrides, a provider-detail card with 3-state
(inherit/on/off) toggles, an info button on the Claude Code tool card
linking to Feature Flags, and an env-source warning on the
EXPOSE_CC_DISCOVERY_ALIASES flag card when it's forced on via env.
* feat(api): cc discovery usage metrics
* fix(api): record cc alias metric in the production wrapper + atomic counter upsert
* docs: document cc discovery aliases (Claude Code guide + feature flag catalog)
* fix(sse): don't mirror built-in auto/* combos as discovery aliases (advertised-but-unroutable)
* i18n(vi): translate the discovery-alias strings instead of shipping placeholders
vi is the one locale with a strict "no internal missing markers" test, so the 17
__MISSING__ entries this branch added (the provider ccAlias panel, the info
button, the feature-flag description and the env warning) would have turned that
test red the moment the base itself was repaired. Translated, keeping every ICU
placeholder ({modelId}, {error}) and the literal claude/<provider>/<model> id
shape intact.
* chore(quality): raise the frozen caps this feature legitimately grows
catalog.ts 1615 -> 1639: the alias synthesis is wired into the catalog builder,
which is where the per-key-filtered list is assembled — the only place the mirror
entries can be appended after model hiding has been applied.
localDb.ts 808 -> 810: two re-export lines for the new ccDiscoveryAliases db
module, which is exactly what the "Adding a New DB Module" recipe prescribes.
* refactor(dashboard,api): keep the complexity ratchets flat
The feature added four cyclomatic violations and one cognitive one, which the
ratchets reject — the baseline only moves when a metric improves. Split the new
code instead:
- appendCcDiscoveryAliases: the four skip-guards become isMirrorableId().
- resolveCcDiscoveryAliasStripWith: alias parsing and gate resolution become
parseCcAliasTarget() and resolveGateFor(), replacing a chain of ternaries that
each re-tested isComboAlias.
- FeatureFlagCard: the env-precedence warning becomes its own component instead
of a conditional branch inside an already-large render.
- ProviderCcAliasSection: the loader moves to useCcAliasData(), and the override
list and add-row become ModelOverrideList / AddOverrideRow, bringing both
oversized functions back under the 80-line rule.
Behavior unchanged — the 74 discovery-alias tests pass untouched. Both ratchets
now sit exactly at baseline (2188 / 971).
Both vars were introduced in code without a .env.example / ENVIRONMENT.md
entry, so `check:env-doc-sync` (docs-sync-strict / docs-gates) went red on
release/v3.8.49 with "In code but missing from .env.example: 2":
- NEXT_PUBLIC_OMNIROUTE_BASE_PATH — src/shared/hooks/useDisplayBaseUrl.ts (#8514)
- OMNIROUTE_BACKUP_SCHEDULE_JOB_INTERVAL_MS — src/lib/jobs/backupScheduleJob.ts (#8517)
Documents both in .env.example and docs/reference/ENVIRONMENT.md rather than
adding allowlist entries: both are real operator-tunable knobs, so the
allowlist would hide a genuine gap.
Refs #8540
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: maxmad64bis <maxmad64bis@users.noreply.github.com>
* fix(sse): label HTTP 499 disconnects as client_disconnected
Preserve caller-supplied error type/code in buildErrorBody so stream
abort classification is not overwritten by the status-code table.
* docs(security): document buildErrorBody classification arg
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(oauth): explain the LAN-IP loopback mismatch with an actionable panel
#8046 already stops the doomed login when a PKCE_CALLBACK_SERVER_PROVIDERS
provider (codex / xai-oauth / grok-cli) is connected from a LAN IP, but it
explained itself as one long English sentence rendered in the generic red
"Connection failed" step. Two concrete problems with that:
- the operator had to parse prose to work out WHICH ports to forward, and the
command shipped with `<port>` / `<omniroute-host>` placeholders to resolve
by hand;
- it forwarded a single port. Both are required: the dashboard port is what
makes the origin true-localhost (a LAN origin never reaches the
callback-server branch at all), and the provider's fixed callback port is
where the browser is actually sent back to. Forwarding either one alone
still fails.
buildPkceLoopbackMismatchHint() now returns the diagnosis as structured data
with the detected host and both ports already filled in, and a dedicated
OAuthLoopbackMismatchPanel renders it as: what happened -> how to fix, in
three numbered steps with copy-to-clipboard fields. No "Try again" button —
retrying the same origin fails identically. The panel yields to the
paste-token tab so grok-cli (which is in both provider sets) never stacks the
two views.
The flat warning string stays exported for non-UI callers.
docs: REMOTE-MODE.md gains a "Connecting Codex / Grok on a remote install"
section with the fixed-callback table and the two-port tunnel, mirroring the
existing Antigravity section.
i18n: 9 new oauthModal keys, hand-written for en + pt-BR and propagated to the
remaining 40 locales as `__MISSING__:` sentinels (runtime falls back to the
clean English value per #7258).
* fix(oauth): correct the Antigravity remote-login guidance and drop the stale i18n copy
Same LAN-origin family as the codex fix in this branch, different mechanism and a
worse failure mode.
Google providers (antigravity / agy) have no fixed foreign port: OAuthModal builds
`http://127.0.0.1:<dashboardPort>/callback`. On a LAN origin that 127.0.0.1 is the
BROWSER's machine, and Google's firstparty/nativeapp consent only releases the code
once the loopback is reachable from the approving browser. When it is not, the consent
never redirects at all — it hangs. So unlike an ordinary provider there is no error
page and no callback URL in the address bar.
That made the existing copy actively wrong. `googleOAuthWarning` was corrected when
the login helper shipped (#5203), but a changed English value does not invalidate
existing translations and `i18n:sync-ui` only fills keys that are ABSENT, never ones
that are STALE — so 39 of 43 locales (pt-BR, pt, es, de, fr, ja, zh-CN, …) kept the
original "wait for the redirect, copy the full URL and paste it below", instructing a
flow that cannot complete. The drift gate that should have caught this is a no-op:
`check-translation-drift.mjs` needs `.i18n-state.json`, which is not in the repo, and
it runs `--warn`.
Because the key's MEANING changed, it is renamed rather than edited — a new key cannot
inherit a stale translation. `googleOAuthWarning` is removed from all 43 locales and
replaced by 7 `googleLoopback*` keys, hand-written for en + pt-BR and marked
`__MISSING__:` elsewhere so the runtime falls back to correct English (#7258).
UI: `OAuthGoogleLoopbackNotice` states what is happening and surfaces both real
remedies with the detected host and port filled in — the local login helper
(recommended; its blob is what the Step 2 field accepts) and a single-port SSH forward.
It also REPLACES `remoteAccessInfo` for this family instead of stacking on top of it:
that notice promises an error page whose URL you copy, true for ordinary providers and
false here.
`agy` deliberately gets no helper command. bin/cli/commands/login.mjs pins
PROVIDER = "antigravity" and parsePastedCredentials() rejects a blob whose embedded
provider does not match the route provider, so advertising the helper there would send
the operator to a blob guaranteed to be refused. It keeps the tunnel path.
Refactor: the shared `resolveDashboardPort` / `buildSshLocalForward` helpers move to
`loopbackTunnel.ts`, used by both hint builders. The codex builder's behaviour is
unchanged (its 11 tests still pass untouched).
docs: REMOTE-MODE.md notes that the dashboard now surfaces the remedies, states that
one forward is enough for Antigravity (contrasting the two-port codex case), and aligns
Option B's command on 127.0.0.1 to match what the UI generates.
---------
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
POST /api/combos and PUT /api/combos/[id] had zero validation or
observability when a combo name collided with a real model id, and
sseModelService.getComboForModel() always resolves the combo first. That
combo-first precedence is not a bug: #6940 documents a combo named after a
bare model id (e.g. `gpt-5.5`) as the supported mechanism for per-model
provider fallback, reusing the #3227/#3233 machinery and covered by
tests/unit/responses-combo-resolution-3227.test.ts and
tests/unit/combo-name-codex-responses-rewrite.test.ts. Hard-rejecting a
colliding name (as #8530's literal acceptance criteria requested) would
regress that documented workflow.
Instead, both routes now attach a non-blocking `warning` field
(`COMBO_NAME_SHADOWS_MODEL`) to the create/rename response when the name
collides with a real model id, and a new boot-time scan
(scanComboModelNameCollisionsAtBoot in src/instrumentation-node.ts) logs a
startup warning enumerating existing collisions — so an operator who hits
this by accident has a signal, while the #6940-sanctioned pattern keeps
working exactly as before.
New tests/unit/combo-model-name-collision-8530.test.ts proves both: the
sanctioned shapes (create/rename to a colliding name) still return
201/200 with the warning attached, and non-colliding names get no warning
field at all.
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
The zh-TW catalog was machine-translated with mainland-habit vocabulary and
simplified->traditional conversions that picked the wrong homophone, and its
README still advertised the v3.7-era figures.
Terminology (docs/i18n/zh-TW/ + src/i18n/messages/zh-TW.json):
- wrong-character conversions: 上遊->上游, 後臺->後台, 儀錶板->儀表板
- mainland habits: 默認->預設, 緩存->快取, 模塊->模組, 調用->呼叫,
字符串->字串, 全局->全域, 文檔->文件, 響應->回應
- consistency: 供應商/提供商->提供者 (提供者 was already 71% dominant),
型別->類型 for UI labels, 不活躍->未啟用
README figures synced to the English source: 231->290 providers,
17->19 routing strategies, 1.6B->1.53B free tokens, 50+->90+ free tiers,
11->40+ free forever, 87->104 MCP tools, 30->31 scopes.
Root cause — the generator's post-translation pass was a hardcoded list that
duplicated the glossary and was wired only into the deprecated
generate-multilang.mjs, so the active run-translation.mjs pipeline applied
nothing. Worse, its blanket /代碼/g -> 程式碼 rule would corrupt 控制代碼
(handle), 語系代碼 (locale code) and 錯誤代碼 (error code) on the next
regeneration.
Both scripts and the drift gate now share scripts/i18n/glossary-normalize.mjs,
driven by scripts/i18n/glossary/<locale>.json as the single source of truth.
Ambiguous terms carry blockedPrefixes so 型別->類型 can stay enforced without
mangling 模型別名 (model alias) or 基本型別 (a programming data type); terms
whose synonym is also a legitimate rendering elsewhere (代碼, 項目) are seeded
with no synonyms and documented instead of blanket-rewritten.
CI now runs the glossary gate for zh-TW alongside zh-CN.
* fix(resilience): honor comboCooldownWait for every combo strategy
The cooldown-wait path claimed all strategies, but isComboCooldownWaitEligible still gated on quota-share/auto. Widen the gate, raise the per-target timeout floor with it, and consult the allow-list from earliestRetryAfter so a later 403 cannot skip the decision. Fixes#8541.
* test(combo): fold cooldown-wait strategy assertions into a loop
Keep combo-config.test.ts under the file-size ceiling while covering every
routing strategy (including internal quota-share) for the #8541 gate fix.
* chore(combo): trim cooldown-wait comment under file-size ceiling
After rebase onto tip the net +2 in combo.ts sat one line over the frozen
check:file-size count (split-based); fold the SECURITY note into the block.
Claude Code always sends bare (unprefixed) model IDs such as
claude-opus-4-8. When both the Claude Code (cc) and Claude (claude)
providers are connected, that bare id resolves to two routes and the
gateway returns a 400 'Ambiguous model' error, which the guide never
mentioned.
Add a Troubleshooting entry covering both fixes: pinning a prefixed
ANTHROPIC_MODEL, or enabling the 'Prefer Claude Code for unprefixed
Claude models' setting (dashboard toggle /
OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS), linking to
the environment reference where the flag is documented.
Closes#8311
Mirror the search/route.ts pattern for /v1/web/fetch: skip rate-limited
stubs instead of letting them short-circuit auto-select, walk the
fixed-priority pool (fill-first) with a request-time fallback on
retryable/quota upstream statuses (429 always; 402/403 for
Firecrawl/Tavily/TinyFish quota-style tiers), and return a proper 429
(with Retry-After) when the whole pool is exhausted instead of a
generic 400. Explicit-provider requests never silently fall back.
Contributor guidance contradicted itself in four places (found in the #8084 review):
- pull_request_template.md + CONTRIBUTING.md asked contributors to run the FULL
unit suite + coverage gate locally, while the maintainer's stated golden path
(#8273/#8329) is: focused tests for the change locally; full suite, coverage,
and build are CI's job. On 16GB hosts the full local chain has saturated
machines (#8084 incident report).
- GEMINI.md demanded coverage >= 75/75/75/70 while the official CI gate is
60/60/60/60 (quality-baseline ratchet on top).
- AGENTS.md fork workflow said to branch from upstream/main; the default branch
is the active release/vX.Y.Z line (main only receives release squash-merges).
Also makes the #8084 CI direction explicit in the public ROADMAP: lane
consolidation (3.8.51), one CI policy for release/** and main (3.8.52),
full-regression authority -> merge queue after TIA shadow evidence (3.8.54),
preview-artifact + build-once rehearsal inside the 3.8.58 dry-run.
Refs #8329
Refs #8084
- Created a comparison table SVG illustrating the capabilities of OmniRoute versus competitors (9router, OpenRouter, CLIProxyAPI, LiteLLM) across 13 features.
- Added a zero-config installation SVG demonstrating the ease of setting up OmniRoute with three simple steps: installation, pointing to the tool, and receiving instant replies.
- Added Sweden flag (se.svg)
- Added Slovakia flag (sk.svg)
- Added Thailand flag (th.svg)
- Added Turkey flag (tr.svg)
- Added Taiwan flag (tw.svg)
- Added Tanzania flag (tz.svg)
- Added Ukraine flag (ua.svg)
- Added United States flag (us.svg)
- Added Vietnam flag (vn.svg)
#8232 correctly targeted OAuth auto-refresh for legacy stored Gemini CLI connections, but exceeded that compatibility goal by recreating a complete routable and UI-visible provider with an Antigravity model catalog.
Preserve legacy refresh by mapping gemini-cli rows to the existing Gemini OAuth credentials. Remove the public provider registry, OAuth preset, model routing snapshot, and canonical-provider tests while keeping Gemini API and Antigravity unchanged.
Regenerated the [3.8.49] CHANGELOG from the full cycle range (bump 2c62333b0..tip):
added 289 previously-uncovered merged PRs as categorized bullets with per-PR author
attribution, then re-injected the 🙌 Contributors hall (83 external contributors).
Closed-PR credit audit: clean — the 3 closed-not-merged authors not in the hall
(#8178 AI Council 'opened by mistake, stays on fork', #8076 Rust fetch 'closing at my
request', dependabot bot) had no landed work, so no missing credit. Synced 41 i18n
CHANGELOG mirrors.
Sync the hand-authored readme-hero.svg / promise-pillars.svg provider count
(number + accessibility desc) to 290, matching the catalog + the README/AGENTS/CLAUDE
text updated in the prior commit.
* docs(i18n): full Russian README rewrite for native readers
Rewrite docs/i18n/ru/README.md from an outdated English dump into a proper Russian manual aligned with the modern product README (v3.8.x structure, free-tier budget, combos, compression, CLI/MCP). Fix relative screenshot/doc links for the i18n path.
* fix(8217): changelog fragment bullet prefix
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add Typhoon and Inception Mercury API-key providers
Two OpenAI-compatible API-key providers, each verified against a live
endpoint smoke test with a negative control before registration: an
unknown route answers 404 while /v1/chat/completions answers 401, which
rules out gateways that reply identically to every path.
- typhoon (SCB 10X, Thailand): first Thai-first provider in the catalog.
/v1/models answers 200 unauthenticated and serves exactly one chat
model, typhoon-v2.5-30b-a3b-instruct (128K ctx). The docs also list
typhoon-v2.1-12b-instruct, but the live endpoint no longer serves it,
so it is deliberately not registered. The typhoon-ocr* and typhoon-asr*
entries are OCR and speech models, not chat, and are omitted.
- inception (Inception Labs): first diffusion LLM (dLLM) in the catalog.
mercury-2 has a 128K context, 50K max output, and supports tools,
json_mode and structured outputs. The mercury-coder models advertised
on the vendor blog are no longer served by the live endpoint and are
therefore not registered.
Both expose a working /v1/models catalog, so they are added to
NAMED_OPENAI_STYLE_PROVIDERS for discovery and key validation.
Free-tier metadata is claimed only where it is documented and durable:
Typhoon issues a free API key rate-limited to 5 req/s and 200 req/m, and
Inception grants 10M tokens on signup with no card, so both are
registered with hasFree: true.
Provider count moves from 280 to 282; README/AGENTS/CLAUDE counters were
stale at 278 and are resynced against docs/reference/PROVIDER_REFERENCE.md.
* fix(8170): union frontier-labs Inception+Writer, regen ref+golden
* fix(8170): close inception object + regen ref/golden
---------
Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(providers): add Sarvam AI, Writer Palmyra and PLaMo API-key providers
Three OpenAI-compatible API-key providers, each verified against a live
endpoint smoke test before registration:
- sarvam (India): /v1/models answers 200 unauthenticated and lists
sarvam-105b (128K ctx) and sarvam-30b (64K ctx). The older sarvam-m is
discontinued upstream and is deliberately not registered.
- writer (Palmyra): api.writer.com exposes the OpenAI alias
/v1/chat/completions alongside its native /v1/chat — confirmed with a
negative control, since an unknown route answers 404 'endpoint not
available via API gateway' while /v1/chat/completions answers 401.
Registers palmyra-x5 (1M ctx) and palmyra-x4 (128K ctx); the
medical/financial/creative/vision variants are deprecated upstream and
are omitted.
- plamo (Preferred Networks, Japan): only plamo-3.0-prime (262K ctx) is
registered. plamo-3.0-prime-beta is discontinued on 2026-07-31 and
plamo-2.2-prime on 2026-09-30, so neither is worth wiring up.
Free-tier metadata is claimed only where it is documented and durable:
Sarvam ships a permanent signup credit, while PLaMo's 10M-token grant is
a campaign that expires on 2026-07-31 and Writer documents no free tier,
so both are registered with hasFree: false.
* regen golden+ref
---------
Co-authored-by: Álvaro Ángel Molina <alvaretto@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>