Skip to content

Commit bfaf459

Browse files
authored
Release v3.8.32 (#4418)
Release v3.8.32 — see CHANGELOG.md [3.8.32] for the full list. Merged via --admin over documented non-blocking checks: CodeQL alerts ratchet (#665 fixed by #4457/#4462, auto-closes on main rescan), Integration Tests (env-flaky batch-upstream), SonarCloud/SonarQube (advisory new-code).
1 parent d0396c2 commit bfaf459

226 files changed

Lines changed: 12627 additions & 1617 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,12 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
16251625
# OPENCODE_API_KEY=
16261626

16271627
# ─── Bifrost Go sidecar (PR-4 in #3932) ──────────────────────────────────────
1628+
# Master kill switch for the bifrost sidecar proxy. When set to 0, the
1629+
# /api/v1/relay/chat/completions/bifrost route returns 503 with the
1630+
# X-Bifrost-Killswitch header and the operator is bounced to the TS path.
1631+
# Use this to disable the sidecar without redeploying (e.g. during a
1632+
# tier-1 router incident or a key rotation). Default: 1 (sidecar active).
1633+
# BIFROST_ENABLED=1
16281634
# When BIFROST_BASE_URL is set, /api/v1/relay/chat/completions/bifrost routes
16291635
# traffic to the Go gateway instead of the TS relay handler, removing TS from
16301636
# the hot path. Auth/rate-limit/injection-guard stay in the route (security not
@@ -1668,3 +1674,30 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
16681674
# Redis image used by the 1-click Redis launcher. Default: redis:7-alpine.
16691675
# Override to redis:8-alpine or a private registry mirror as needed.
16701676
# OMNIROUTE_REDIS_IMAGE=
1677+
1678+
# ── Cluster Profile: Qdrant Vector Memory (opt-in via `docker compose --profile memory up`) ──
1679+
# Qdrant is an OPTIONAL sidecar for deployments that need cosine-distance vector
1680+
# search at >1M embeddings. The default vector store is sqlite-vec
1681+
# (src/lib/memory/vectorStore.ts:108); flip this profile on only if you hit the
1682+
# sqlite-vec ceiling or want persistent cross-replica vector state. See
1683+
# docs/architecture/cluster-decisions.md § "Qdrant (memory profile)".
1684+
# QDRANT_HOST=qdrant
1685+
# QDRANT_PORT=6333
1686+
# QDRANT_GRPC_PORT=6334
1687+
# QDRANT_API_KEY=
1688+
# QDRANT_COLLECTION=omniroute-memory
1689+
# QDRANT_EMBEDDING_MODEL=text-embedding-3-small
1690+
# QDRANT_VECTOR_SIZE=1536
1691+
# QDRANT_HNSW_EF_CONSTRUCT=128
1692+
1693+
# ── Cluster Profile: Bifrost Tier-1 Router (opt-in via `docker compose --profile bifrost up`) ──
1694+
# Bifrost is an OPTIONAL Go-based Tier-1 router that handles the upstream-provider
1695+
# multiplexing layer. Default: OmniRoute's open-sse/executors/bifrost.ts in-process
1696+
# executor handles routing directly. Flip this profile on only if you want the
1697+
# gateway as a separate sidecar (helps in 3+ replica deployments where you want
1698+
# provider rotation centralised). See docs/architecture/cluster-decisions.md §
1699+
# "Bifrost (bifrost profile)".
1700+
# BIFROST_BASE_URL=http://bifrost:8080
1701+
# BIFROST_API_KEY=
1702+
# BIFROST_STREAMING_ENABLED=true
1703+
# BIFROST_TIMEOUT_MS=30000

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ For any non-trivial change, read the matching deep-dive first:
560560
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
561561
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
562562
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
563+
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
563564

564565
---
565566

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,17 +287,17 @@ Result: 4 layers of fallback = zero downtime
287287

288288
</div>
289289

290-
> Recent highlights from **v3.8.20 → v3.8.31**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
290+
> Recent highlights from **v3.8.20 → v3.8.32**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
291291
292292
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode, Gemini CLI); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
293293
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`). → [Remote Mode](docs/guides/REMOTE-MODE.md)
294294
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), live Arena-ELO + models.dev model intelligence, and per-step account allowlists. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
295-
- **🗜️ Pluggable compression** — an async compression pipeline with Compression Studios, a stable LLMLingua-2 ONNX engine, RTK, and delegated Anthropic Context Editing. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
295+
- **🗜️ Pluggable compression** — an async compression pipeline with Compression Studios, a stable LLMLingua-2 ONNX engine, RTK, delegated Anthropic Context Editing, and a unified settings panel that is the single source for per-engine on/off + level. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
296296
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture & translate traffic from CLIs that ignore proxy env vars, with a per-SNI certificate authority and a trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
297297
- **💸 Cost telemetry everywhere**`X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
298298
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
299299
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
300-
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), a refreshed 231-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), and Vertex AI media generation (speech / transcription / music / video). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
300+
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), a refreshed 231-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech / transcription / music / video), and one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
301301
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel) and an optional Bifrost Go sidecar that offloads the hottest relay path (`BIFROST_BASE_URL`, with automatic fallback to the TypeScript path on timeout). → [Environment](docs/reference/ENVIRONMENT.md)
302302

303303
<br/>
@@ -439,6 +439,7 @@ omniroute connect 192.168.0.15 # password → scoped token, saved as
439439
omniroute models list # ← runs against the REMOTE server
440440
omniroute configure codex # ← picks a remote model, writes a local Codex profile
441441
omniroute tokens create --name ci --scope read # mint narrower tokens for other machines
442+
omniroute contexts use default # ← switch back to the local server
442443
```
443444

444445
Tokens are scoped `read` / `write` / `admin`; process-spawning routes stay loopback-only.
@@ -876,14 +877,14 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
876877

877878
### 📘 Getting Started
878879

879-
| Document | Description |
880-
| ---------------------------------------------- | ----------------------------------------------------------------------------- |
881-
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
882-
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
883-
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
884-
| [Remote Mode](docs/guides/REMOTE-MODE.md) | Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens |
880+
| Document | Description |
881+
| -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
882+
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
883+
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
884+
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
885+
| [Remote Mode](docs/guides/REMOTE-MODE.md) | Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens |
885886
| [Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md) | Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles |
886-
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
887+
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
887888

888889
### 🔧 Operations & Deployment
889890

bin/cli/commands/contexts.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ function authLabel(c) {
99
return "✗";
1010
}
1111

12-
async function confirm(msg) {
12+
export async function confirm(msg) {
13+
// Non-interactive stdin (pipe, CI, EOF) cannot answer a [y/N] prompt. Asking
14+
// anyway leaves the readline question pending forever — Node then warns about an
15+
// "unsettled top-level await" at exit. Decline cleanly instead and point at the
16+
// non-interactive escape hatch so scripted callers fail safe rather than hang.
17+
if (!process.stdin.isTTY) {
18+
process.stderr.write(`${msg} [y/N] (non-interactive stdin — declined; pass --yes to confirm)\n`);
19+
return false;
20+
}
1321
const readline = await import("node:readline");
1422
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1523
const answer = await new Promise((r) => rl.question(`${msg} [y/N] `, r));
@@ -26,6 +34,7 @@ function maskKey(k) {
2634
export function registerContexts(program) {
2735
const ctx = program
2836
.command("contexts")
37+
.alias("context") // singular alias — docs/connect output historically said `context current`
2938
.description(t("config.contexts.description") || "Manage server contexts/profiles");
3039

3140
ctx

bin/cli/commands/update.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ export async function getCurrentVersion() {
2525
}
2626
}
2727

28-
async function getLatestVersion() {
28+
// `--prefer-online` forces npm to revalidate its HTTP cache against the registry.
29+
// Without it `npm view` can return a stale cached version (e.g. report 3.8.30 as
30+
// "latest" after 3.8.31 was published), so the updater told users on an old build
31+
// they were already on the latest version (#4376). `execFn` is injectable for tests.
32+
export async function getLatestVersion(execFn = execFileAsync) {
2933
try {
30-
const { stdout } = await execFileAsync("npm", ["view", "omniroute", "version"], {
34+
const { stdout } = await execFn("npm", ["view", "omniroute", "version", "--prefer-online"], {
3135
timeout: 15000,
3236
});
3337
return stdout.trim();

bin/cli/io.mjs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,43 @@ export function createPrompt() {
66
output: process.stdout,
77
});
88

9+
// Non-interactive stdin (pipe, CI, EOF via `< /dev/null`) cannot answer an
10+
// interactive prompt. Without a guard, `rl.question` never fires its callback —
11+
// the await stays pending and Node warns about an "unsettled top-level await" at
12+
// exit. Resolving on the readline `close` event (which fires on stdin EOF)
13+
// returns the default/empty instead of hanging. A genuinely piped line still
14+
// arrives via the question callback first, so `echo value | omniroute …` keeps
15+
// working — only the no-input EOF case falls back.
916
function ask(question, defaultValue = "") {
1017
const suffix = defaultValue ? ` (${defaultValue})` : "";
1118
return new Promise((resolve) => {
19+
let settled = false;
20+
const done = (v) => {
21+
if (!settled) {
22+
settled = true;
23+
resolve(v);
24+
}
25+
};
26+
rl.once("close", () => done(defaultValue));
1227
rl.question(`${question}${suffix}: `, (answer) => {
1328
const trimmed = answer.trim();
14-
resolve(trimmed || defaultValue);
29+
done(trimmed || defaultValue);
1530
});
1631
});
1732
}
1833

1934
function askSecret(question) {
2035
return new Promise((resolve) => {
21-
let prompted = false;
36+
let settled = false;
2237
const saved = rl._writeToOutput.bind(rl);
38+
const done = (v) => {
39+
if (!settled) {
40+
settled = true;
41+
rl._writeToOutput = saved;
42+
resolve(v);
43+
}
44+
};
45+
let prompted = false;
2346
rl._writeToOutput = function (str) {
2447
if (!prompted) {
2548
rl.output.write(str);
@@ -29,9 +52,9 @@ export function createPrompt() {
2952
// Suppress character echo; allow only newlines through
3053
if (str === "\r\n" || str === "\n" || str === "\r") rl.output.write("\n");
3154
};
55+
rl.once("close", () => done("")); // non-interactive EOF → empty secret, no hang
3256
rl.question(`${question}: `, (answer) => {
33-
rl._writeToOutput = saved;
34-
resolve(answer.trim());
57+
done(answer.trim());
3558
});
3659
});
3760
}

config/quality/complexity-baseline.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
3-
"count": 1900,
3+
"count": 1905,
4+
"_rebaseline_2026_06_20_reviewprs_mine_r2": "Reconciliacao release-volatil 1900->1905 (+5) apos o lote /review-prs 'apenas minhas' r2 (17 PRs meus mergeados em release/v3.8.32). Breakdown medido com `node scripts/check/check-complexity.mjs` + diff por-arquivo eslint complexity JSON entre o tip pre-lote (9052c5a78 = 1902) e o tip pos-lote (1905): (a) +2 JA latentes no tip pre-lote (1900->1902), drift de merges concorrentes de outras sessoes ANTERIORES a este lote (nao introduzidos por mim) que o fast-path do release nao rebaselina (check:complexity so roda release->main); (b) +3 deste lote, isolados em DOIS arquivos: open-sse/translator/helpers/geminiHelper.ts +1 (convertOpenAIContentToParts passou de 80->93 linhas pela branch de audio do #4426 — max-lines-per-function, funcao de dispatch coesa por tipo de content part) e open-sse/translator/response/openai-to-gemini-sse.ts +2 (translator SSE NOVO do #4453, openAIChunkToGeminiChunk + convertOpenAIResponseToGemini em complexity 19 cada — conversores de chunk SSE inerentemente ramificados, levemente acima de 15). Crescimento de feature legitimo recem-TDD'd, nao regressao; refatorar feature recem-mergeada so para raspar +3 seria over-engineering arriscado. Reducao fica como debt.",
45
"_rebaseline_2026_06_20_reviewprs_v3831_batch": "Reconciliacao release-volatil 1896->1900 (+4): drift do lote /review-prs v3.8.31 (25 PRs A+B+C + merges concorrentes). Condicionais NOVOS legitimos — sobretudo #4381 (rotas /api/local/redis/{start,stop,status} detectRuntime + guardas + bifrost relay) e #4366 (classificacao de exhaustion de erro entre os 2 dispatchers de combo). Medido 1900 ESTAVEL em fd1391c0b E f46c69f2a com `node scripts/check/check-complexity.mjs` (commit concorrente intermediario foi complexity-neutro). O fast-path do release nao roda check:complexity (so release->main), entao o ramo acumula sem rebaselinar. Mesma familia dos rebaselines anteriores — crescimento de feature legitimo, nao regressao; valor final reconciliado no release->main.",
56
"_rebaseline_2026_06_20_postlote_concurrent_drift": "Reconciliacao release-volatil: 1895->1896 (+1). Drift de condicional NOVO de PRs mergeados pela sessao concorrente APOS o #4338 ratchetar para 1895 (#4355 pricing gpt-5.x-pro / #4364 cli active-context cred / #4363 compliance cleanup / #4358 mitm mask / #4332 injection-guard-16KB). O fast-path do release nao roda check:complexity (so release->main), entao o ramo acumula sem rebaselinar. Medido com `node scripts/check/check-complexity.mjs` no tip cdfd71c17. Mesma familia — crescimento de feature legitimo, nao regressao.",
67
"_note_2026_06_20_4371_chatcore_heap_leaf": "PR #4371 (extract checkHeapPressureGuard leaf, god-file decomposition start) e complexity-NEUTRO: handleChatCore so PERDE codigo e o novo heapPressure.ts checkHeapPressureGuard fica sob o teto — a contagem permanece 1896 (medido no tip mesclado com check-complexity.mjs).",

0 commit comments

Comments
 (0)