Skip to content

perf: combos split + next config + 1-click redis + bifrost sidecar (#3932) - #4381

Merged
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.31from
KooshaPari:feat/perf-combos-split-2026-06-20-v2
Jun 20, 2026
Merged

diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.31from
KooshaPari:feat/perf-combos-split-2026-06-20-v2

Conversation

@KooshaPari

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #3932 — delivers the 5-track performance + UX workstream from that thread. After auditing upstream main, four of the five tracks are still applicable; the fifth (chatLogHelpers extraction) was already merged upstream as memoryExtraction.ts + logTruncation.ts and is dropped here.

Diff is +1,161 / −73 across 15 files, base = upstream/main @ db362b012, 1 commit ahead, 0 behind.

Changes (rolled into one commit)

PR-1: combos leaf-component extraction

  • New: src/app/(dashboard)/dashboard/combos/FieldLabelWithHelp.tsx (32 LOC)
  • New: src/app/(dashboard)/dashboard/combos/WeightTotalBar.tsx (66 LOC)
  • New: src/app/(dashboard)/dashboard/combos/parts.ts (barrel re-export)
  • New: src/app/(dashboard)/dashboard/combos/loading.tsx (Suspense skeleton, 15 LOC)
  • Modified: combos/page.tsx 4,384 → 4,314 lines (−70 LOC)
    • Removed inline FieldLabelWithHelp and WeightTotalBar definitions
    • Added import { FieldLabelWithHelp, WeightTotalBar } from "./parts";
  • Both extracted components are pure-presentational with zero closure dependency on CombosPage state

PR-2: next.config.mjs perf

  • compress: true (gzip static assets)
  • productionBrowserSourceMaps: false (skips ~30MB .map emission per build)
  • experimental.optimizePackageImports for lobehub/icons, @lobehub/icons, lucide-react, date-fns, lodash, lodash-es, material-symbols, next-intl, @omniroute/open-sse
  • New webpack splitChunks cacheGroups: vendor-next-intl (priority 25), vendor-fumadocs, vendor-combo-graph

PR-3: 1-click Redis launcher

  • New CLI: bin/cli/commands/redis.mjs (294 LOC)
    Subcommands: up, down, status; options: --port, --name, --image, --runtime, --password, --no-pull
    Auto-detects podman then docker, pulls image if missing, uses named volume for persistence
  • New GUI: src/app/(dashboard)/dashboard/settings/components/RedisLauncherPanel.tsx (172 LOC) — Start/Stop button + 3s status polling
  • New API routes: /api/local/redis/{start,stop,status}
  • New security guard: src/lib/security/localEndpoints.ts with loopback + bearer-token + prod opt-in
  • Modified: bin/cli/commands/registry.mjs (registerRedis(program))
  • Modified: bin/cli/locales/en.json (redis.description key)

PR-4: Bifrost sidecar proxy route

  • New: src/app/api/v1/relay/chat/completions/bifrost/route.ts (273 LOC)
  • When BIFROST_BASE_URL is set, relay traffic goes directly to the Go gateway
  • Auth/rate-limit/injection-guard stay in this route (security not duplicated)
  • Falls back to /api/v1/relay/chat/completions via X-Bifrost-Fallback header on sidecar timeout/failure

PR-5: DROPPED

  • Upstream already has open-sse/handlers/chatCore/memoryExtraction.ts and logTruncation.ts covering the same surface area I was extracting. No need to redo.

Verification performed

  • node --check on every edited .mjs/route file: clean
  • python3 -m json.tool on bin/cli/locales/en.json: clean
  • Inline call-site preservation grep for moved components: 10/10 for FieldLabelWithHelp, 1/1 for WeightTotalBar
  • All imports verified to exist on upstream main (no orphan references)

Test plan for reviewer

  1. git fetch origin feat/perf-combos-split-2026-06-20-v2
  2. pnpm install && pnpm build — check that vendor-next-intl, vendor-fumadocs, vendor-combo-graph chunks split correctly
  3. node bin/omniroute.mjs redis status — should print JSON state
  4. node bin/omniroute.mjs redis up — should launch container via podman/docker
  5. Open /dashboard/settings/resilience — RedisLauncherPanel should render and poll status every 3s

Refs

@KooshaPari
KooshaPari requested a review from diegosouzapw as a code owner June 20, 2026 11:18

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a local Redis container management CLI command and a corresponding settings dashboard panel, adds local API endpoints to control the container, optimizes Next.js build performance (compression, tree-shaking, and chunk splitting), refactors dashboard combo components, and adds a high-performance sidecar proxy route for Bifrost. Feedback highlights critical issues: the CLI container startup and pull logic is buggy and inverted; the local Redis start API route uses conflicting --rm and --restart options; and the local endpoint security guard uses an unsafe global variable globalThis.__omniRequestHeaders that can cause race conditions under concurrent requests, which should be replaced with Next.js's safe headers() utility. Additionally, importing createConnection statically in the CLI command would optimize the TCP probe.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +173 to +203
if (exists && !opts.pull) {
info(`Starting existing container '${name}'…`);
try {
await execFile(runtime, ["start", name]);
success(`Container '${name}' started on port ${port}.`);
return 0;
} catch (err) {
fail(`Failed to start existing container: ${err.message}`);
return 1;
}
}

if (!opts.pull) {
info(`Checking if image '${image}' is present locally…`);
let present = false;
try {
const { stdout } = await execFile(runtime, ["images", "--format", "{{.Repository}}:{{.Tag}}"]);
present = stdout.split("\n").some((line) => line.trim() === image);
} catch {
// ignore — fall through to pull
}
if (!present) {
info(`Image not found locally — pulling '${image}'…`);
try {
await execFile(runtime, ["pull", image]);
} catch (err) {
fail(`Failed to pull image: ${err.message}`);
return 1;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two major issues in this block:

  1. If the container already exists but is stopped, and --no-pull is not specified (opts.pull is true), the condition exists && !opts.pull evaluates to false. The command then falls through to docker run, which will fail because the container name is already in use.
  2. The --no-pull flag logic is inverted. When --no-pull is specified, opts.pull is false, which triggers the if (!opts.pull) block and pulls the image anyway. When --no-pull is not specified, opts.pull is true, and it skips the pulling block.

We should always start the container if it exists, and only pull the image if opts.pull is not explicitly false.

  if (exists) {
    info(`Starting existing container '${name}'…`);
    try {
      await execFile(runtime, ["start", name]);
      success(`Container '${name}' started on port ${port}.`);
      return 0;
    } catch (err) {
      fail(`Failed to start existing container: ${err.message}`);
      return 1;
    }
  }

  if (opts.pull !== false) {
    info(`Checking if image '${image}' is present locally…`);
    let present = false;
    try {
      const { stdout } = await execFile(runtime, ["images", "--format", "{{.Repository}}:{{.Tag}}"]);
      present = stdout.split("\n").some((line) => line.trim() === image);
    } catch {
      // ignore — fall through to pull
    }
    if (!present) {
      info(`Image not found locally — pulling '${image}'…`);
      try {
        await execFile(runtime, ["pull", image]);
      } catch (err) {
        fail(`Failed to pull image: ${err.message}`);
        return 1;
      }
    }
  }

Comment on lines +43 to +54
const args = [
"run",
"-d",
"--name",
CONTAINER_NAME,
"--rm",
"-p",
`${HOST_PORT}:6379`,
"--restart",
"unless-stopped",
IMAGE,
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Docker and Podman, --rm (auto-remove on container exit) and --restart are mutually exclusive options. Attempting to run a container with both options will result in a runtime error: Conflicting options: --rm and --restart. Since this is a Redis service, we should remove --rm and keep --restart unless-stopped to ensure the container persists and restarts as expected.

    const args = [
      "run",
      "-d",
      "--name",
      CONTAINER_NAME,
      "-p",
      `${HOST_PORT}:6379`,
      "--restart",
      "unless-stopped",
      IMAGE,
    ];

Comment on lines +1 to +51
/**
* Guard for /api/local/* routes.
*
* These endpoints shell out to the user's local Podman/Docker to manage local
* infrastructure (Redis, Postgres, MinIO, etc.) on behalf of the GUI. They MUST
* only respond to requests originating from the same host as the dev server.
*
* Trust boundary: this is the only line of defense between the public network
* and `execFile(podman, ...)` / `execFile(docker, ...)`. If this guard is
* missing or weak, an attacker can trigger arbitrary local CLI invocations by
* tricking the user into loading a page that fetches `/api/local/*`.
*
* Rules:
* 1. Allow requests whose `host` header matches the dev server's bind host
* (localhost / 127.0.0.1 / ::1) AND whose `x-forwarded-for` is absent
* or set to a loopback address. This blocks proxied requests from the
* public internet when the dev server is bound to localhost.
* 2. In production (`NODE_ENV=production`), reject unconditionally unless
* OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1 is set. The flag is opt-in so
* accidental dev deployments do not expose the API.
* 3. Trust-list the OmniRoute desktop app via a shared bearer token
* (OMNIROUTE_LOCAL_ENDPOINTS_TOKEN). The desktop app injects the header
* and the server verifies it.
*
* If you are adding a new endpoint under /api/local/* you must:
* - call this guard at the top of your handler
* - never read user-supplied input into `execFile` argv without strict
* allow-list validation (no shell:true, no string concatenation)
* - log the invocation via the audit channel so misuse is detectable
*/
export function isLocalRequestAllowed(): { allowed: true } | { allowed: false; reason: string } {
const headers = (globalThis as { __omniRequestHeaders?: Headers }).__omniRequestHeaders;
if (headers) {
// 1. Bearer token path (desktop app trust)
const expected = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN;
if (expected) {
const supplied = headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
if (supplied && constantTimeEqual(supplied, expected)) {
return { allowed: true };
}
}
// 2. Same-origin loopback path (browser dev tools)
const host = headers.get("host") ?? "";
const fwd = headers.get("x-forwarded-for") ?? "";
const isLoopbackHost = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/.test(host);
const isLoopbackFwd = fwd === "" || /^127\.|^::1$|^localhost$/.test(fwd.split(",")[0]?.trim() ?? "");
if (isLoopbackHost && isLoopbackFwd) {
return { allowed: true };
}
return { allowed: false, reason: "non-local origin" };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Using globalThis.__omniRequestHeaders to store and retrieve request-specific headers is highly unsafe in a concurrent, asynchronous Node.js environment. Concurrent requests can overwrite this global variable, leading to race conditions where a request from a non-local origin could bypass the security guard if a local request runs concurrently.

Instead of using a shared global variable, use Next.js's built-in, concurrency-safe headers() utility from next/headers which uses AsyncLocalStorage under the hood to safely isolate request context.

/**
 * Guard for /api/local/* routes.
 *
 * These endpoints shell out to the user's local Podman/Docker to manage local
 * infrastructure (Redis, Postgres, MinIO, etc.) on behalf of the GUI. They MUST
 * only respond to requests originating from the same host as the dev server.
 *
 * Trust boundary: this is the only line of defense between the public network
 * and `execFile(podman, ...)` / `execFile(docker, ...)`. If this guard is
 * missing or weak, an attacker can trigger arbitrary local CLI invocations by
 * tricking the user into loading a page that fetches `/api/local/*`.
 *
 * Rules:
 *   1. Allow requests whose `host` header matches the dev server's bind host
 *      (localhost / 127.0.0.1 / ::1) AND whose `x-forwarded-for` is absent
 *      or set to a loopback address. This blocks proxied requests from the
 *      public internet when the dev server is bound to localhost.
 *   2. In production (`NODE_ENV=production`), reject unconditionally unless
 *      OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1 is set. The flag is opt-in so
 *      accidental dev deployments do not expose the API.
 *   3. Trust-list the OmniRoute desktop app via a shared bearer token
 *      (OMNIROUTE_LOCAL_ENDPOINTS_TOKEN). The desktop app injects the header
 *      and the server verifies it.
 *
 * If you are adding a new endpoint under /api/local/* you must:
 *   - call this guard at the top of your handler
 *   - never read user-supplied input into `execFile` argv without strict
 *     allow-list validation (no shell:true, no string concatenation)
 *   - log the invocation via the audit channel so misuse is detectable
 */
import { headers } from "next/headers";

export function isLocalRequestAllowed(): { allowed: true } | { allowed: false; reason: string } {
  let requestHeaders: Headers | undefined;
  try {
    requestHeaders = headers();
  } catch {
    requestHeaders = (globalThis as { __omniRequestHeaders?: Headers }).__omniRequestHeaders;
  }

  if (requestHeaders) {
    // 1. Bearer token path (desktop app trust)
    const expected = process.env.OMNIROUTE_LOCAL_ENDPOINTS_TOKEN;
    if (expected) {
      const supplied = requestHeaders.get("authorization")?.replace(/^Bearer\s+/i, "") ?? "";
      if (supplied && constantTimeEqual(supplied, expected)) {
        return { allowed: true };
      }
    }
    // 2. Same-origin loopback path (browser dev tools)
    const host = requestHeaders.get("host") ?? "";
    const fwd = requestHeaders.get("x-forwarded-for") ?? "";
    const isLoopbackHost = /^(localhost|127\.0\.0\.1|::1)(:\d+)?$/.test(host);
    const isLoopbackFwd = fwd === "" || /^127\.|^::1$|^localhost$/.test(fwd.split(",")[0]?.trim() ?? "");
    if (isLoopbackHost && isLoopbackFwd) {
      return { allowed: true };
    }
    return { allowed: false, reason: "non-local origin" };
  }

Comment on lines +1 to +3
import { spawn } from "node:child_process";
import { promisify } from "node:util";
import { execFile as execFileCb } from "node:child_process";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of using a dynamic import inside the pingRedis function on every ping, we can import createConnection from node:net statically at the top of the file.

Suggested change
import { spawn } from "node:child_process";
import { promisify } from "node:util";
import { execFile as execFileCb } from "node:child_process";
import { spawn } from "node:child_process";
import { promisify } from "node:util";
import { execFile as execFileCb } from "node:child_process";
import { createConnection } from "node:net";

Comment on lines +51 to +66
import("node:net").then(({ createConnection }) => {
const socket = createConnection({ port: Number(port), host: "127.0.0.1" });
const timeout = setTimeout(() => {
socket.destroy();
resolve(false);
}, 1500);
socket.once("connect", () => {
clearTimeout(timeout);
socket.end();
resolve(true);
});
socket.once("error", () => {
clearTimeout(timeout);
resolve(false);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the statically imported createConnection function here to simplify the TCP probe logic and avoid dynamic import overhead.

    const socket = createConnection({ port: Number(port), host: "127.0.0.1" });
    const timeout = setTimeout(() => {
      socket.destroy();
      resolve(false);
    }, 1500);
    socket.once("connect", () => {
      clearTimeout(timeout);
      socket.end();
      resolve(true);
    });
    socket.once("error", () => {
      clearTimeout(timeout);
      resolve(false);
    });

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.31 June 20, 2026 14:42
…diegosouzapw#3932) (diegosouzapw#4381)

Combos page split into FieldLabelWithHelp/WeightTotalBar/loading parts; next.config
perf (compress, splitChunks, no browser sourcemaps); 1-click local Redis launcher
(/api/local/redis/{start,stop,status}) + CLI; bifrost Go-sidecar relay route.

Review fixes (co-author):
- Redis routes: removed conflicting --rm (kept --restart unless-stopped); errors now
  routed through sanitizeErrorMessage (Hard Rule #12).
- UI RedisLauncherPanel now calls /start + /stop (matched the actual route names).
- localEndpoints.ts header corrected: managementPolicy (socket-IP, routeGuard
  LOCAL_ONLY_API_PREFIXES) is the AUTHORITATIVE gate; this in-route guard is inert
  best-effort defense-in-depth (documented, not relied upon).
- Bifrost route: added a Zod request schema (model/messages required, .passthrough);
  corrected dangling @see docs + the fallback description.
- Fixed two pre-existing red tests: IPv6 [::1] bracketed-host loopback match, and the
  CLI option-flag mock parsing the long flag (so --port/--name/--image are detected).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
@diegosouzapw
diegosouzapw force-pushed the feat/perf-combos-split-2026-06-20-v2 branch from 473c654 to b44d79e Compare June 20, 2026 15:23
@diegosouzapw
diegosouzapw merged commit 7d6fffd into diegosouzapw:release/v3.8.31 Jun 20, 2026
1 check passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Merged into release/v3.8.31 🎉 Thanks @KooshaPari for the substantial perf work!

We pushed a few review fixes to your branch before merging (co-authored):

  • Redis routes: dropped the conflicting --rm (it clashes with --restart unless-stopped — Docker/Podman reject the run); errors now go through sanitizeErrorMessage (Hard Rule fix(ui): fix Select dropdown dark theme inconsistency #12).
  • UI: RedisLauncherPanel now calls /start + /stop (matched the real route names — the old /up//down 404'd).
  • localEndpoints.ts: corrected the header — the authoritative gate is managementPolicy (socket-IP loopback enforcement via routeGuard's LOCAL_ONLY_API_PREFIXES); the in-route guard is documented as best-effort defense-in-depth.
  • Bifrost: added a Zod request schema + fixed dangling @see docs.
  • Fixed two pre-existing red tests (IPv6 [::1] bracketed-host match + the CLI option-flag mock).

Live VPS validation (Rule #18) on 192.168.0.15 (build b44d79ea6, v3.8.31):

# Direct args proof of the --rm fix:
OLD (--rm + --restart) -> docker: 'conflicting options: cannot specify both --restart and --rm'  (container does NOT start)
NEW (no --rm)          -> container starts; redis-cli ping -> PONG

# Full HTTP route (flag enabled, authed, loopback):
POST /api/local/redis/start  -> 200 {ok:true, runtime:docker, name:omniroute-redis, port:6379, stdout:<id>}
GET  /api/local/redis/status -> 200 {exists:true, running:true, reachable:true}   (docker ps: omniroute-redis Up)
POST /api/local/redis/stop   -> 200 {ok:true}
# flag disabled again -> route correctly re-gated: 403 'disabled in production'

The route classification, loopback/auth/production gating, and the container lifecycle all check out live. Ships in the next release.

diegosouzapw added a commit that referenced this pull request Jun 20, 2026
…lose drift)

The Quality Ratchet failed on the release PR: eslintWarnings 3839 > baseline
3836. The +3 is end-of-cycle drift from legitimately-merged feature PRs
(#4381/#4383/#4373/#4389/#4384/#4410) — `any` is allowed (warn) in open-sse/
and tests/. Verified the release reconciliation files add 0 warnings (gemini
test delta 79<->79, mitm test 0). Same precedent as prior cycle-close
rebaselines. Authorized as part of the end-to-end release.
diegosouzapw added a commit that referenced this pull request Jun 20, 2026
…4397)

* chore(release): open v3.8.31 development cycle

* fix(mitm): exact host membership in MITM hosts test (CodeQL false positive) (#4386)

getMitmToolHosts returns string[], so .includes(host) is Array.prototype.includes
(exact membership). CodeQL's js/incomplete-url-substring-sanitization heuristic
misreads it as a String.includes() URL-substring sanitization check and raises a
HIGH alert. Switch to .some(h => h === host) — identical semantics, explicit intent,
no flagged pattern. Surfaced post-v3.8.30 (#4325) once the test landed on main.

Test-only change (no runtime behavior); the suite still passes and the CodeQL
re-scan on merge clears the alert.

* fix(codex): request reasoning summaries (#4359)

Adds reasoning.summary=auto + reasoning.encrypted_content include for Codex. Thanks @xz-dev.

* fix(embeddings): inject NVIDIA NIM input_type for asymmetric embed models (#4341)

NVIDIA NIM asymmetric embedding models (e.g. nvidia/nv-embedqa-e5-v5) reject
requests without an `input_type` ("query" | "passage") with 400 "'input_type'
parameter is required". The embedding registry now carries a model-level
default param for the asymmetric NVIDIA model, and the embeddings handler
injects a model's default params into the upstream body only when the client
omitted them, leaving a client-supplied value untouched.

Reported-by: hydraromania (decolua/9router#1378)

Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>

* fix(api): migrate deprecated Codex [features].codex_hooks to [features].hooks (#4342)

Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI
versions ignore the old key and warn. When OmniRoute rewrites an existing
config.toml (configure/reset Codex provider) it now renames
[features].codex_hooks -> [features].hooks, preserving the value and never
clobbering an already-present `hooks`, then drops the deprecated key. The
migration is a no-op when the flag is absent and runs on both the POST and
DELETE config paths.

Reported-by: Bian-Sh (decolua/9router#1327)

Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>

* fix(translator): drop the null flush on the same-format response path (#4344)

The streaming response translator's same-format fast path returned
`[chunk]` unconditionally, so the end-of-stream null/flush signal
(chunk === null) propagated as a literal `[null]`. Downstream this surfaced
as an empty `data: null` SSE event between chunks and crashed strict clients
(e.g. Factory Droid BYOK on /v1/responses). The fast path now returns `[]`
for the null flush while still passing real chunks through unchanged.

Reported-by: thaitryhand (decolua/9router#1052)

Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>

* fix(translator): strip assistant echo fields on the OpenAI target path (Mistral 422) (#4350)

Strict OpenAI-compatible upstreams (e.g. mistral/codestral-latest) reject
client-only assistant echo fields sent back as input with 422
extra_forbidden (the report hit messages[].assistant.reasoning_content via
Codex /responses). Only reasoning_content was stripped on the OpenAI target
path; the sibling fields reasoning / refusal / annotations / cache_control
leaked through. They are now all dropped on the non-reasoner OpenAI target
path. `audio` is intentionally preserved (OpenAI audio models reference a
prior assistant audio response by id; Mistral never emits audio).

Reported-by: xxy9468615 (decolua/9router#1649)

Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (#4343)

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (port from 9router#1263)

startTailscaleLogin built `tailscale up` without ever reading
process.env.TAILSCALE_AUTHKEY, so a pre-authenticated / headless daemon
waited for an interactive auth URL and timed out (~15s). When
TAILSCALE_AUTHKEY is set it is now passed via `--auth-key=` (an argv element
to spawn(binary, args) — no shell interpolation, Hard Rule #13); when unset,
behavior is unchanged. The arg builder is extracted into a pure exported
`tailscaleUpArgs()` for testing.

Reported-by: ipeterpetrus (decolua/9router#1263)
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* chore(quality): rebaseline tailscaleTunnel.ts file-size to 1202 (#1263 +13)

---------

Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* fix(dashboard): OAuth modal surfaces the real error on a non-JSON response (#4351)

* fix(dashboard): OAuth modal surfaces real error on non-JSON responses (port from 9router#1318)

The OAuth connect/reauth modal called `await res.json()` unconditionally, so
a non-JSON error response (e.g. a plain-text 500 page from a build/OAuth
endpoint) threw `Unexpected token 'I'...` and hid the real failure. New
shared helpers parseResponseBody / getErrorMessage (src/shared/utils/api.ts)
read the body safely (JSON when JSON, raw text otherwise) and produce a clean
message either way; every modal fetch site now uses them.

Reported-by: DNNYF (decolua/9router#1318)
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(dashboard): type OAuth modal response body as Record<string, unknown> (t11 any-budget)

Switch the parseResponseBody casts from Record<string, any> to
Record<string, unknown> so OAuthModal.tsx stays within its t11 explicit-any
budget. getErrorMessage already takes unknown; the success paths typecheck
clean under strict:false. No runtime change.

---------

Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(translator): accept AI SDK-style { type: image, image: data-URL } content parts (#4345)

* fix(translator): accept AI SDK-style { type: image, image: "data:..." } parts (port from 9router#1330)

Several OpenAI-input translators only recognized images shaped as
`image_url.url` (or an object with `.source`/`.url`), so an AI SDK-style
content part where `image` is a bare data-URL STRING was silently dropped
before reaching a vision provider (OpenCode is one affected client; the gap
is generic). The OpenAI->Claude, OpenAI->Kiro and OpenAI->Gemini/Antigravity
translators now parse a string `image` data URL into each provider's native
image shape (Claude base64 source, Kiro images[].source.bytes, Gemini
inlineData).

Reported-by: mugnimaestra (decolua/9router#1330)
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* chore(quality): freeze openai-to-kiro.ts file-size at 807 (#1330 +9, over 800 cap)

---------

Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* fix(dashboard): show a disabled connection's last error in the row (#4352)

* fix(dashboard): show a disabled connection's last error in the row (port from 9router#1447)

The provider card's error badge counts a disabled connection (isActive ===
false) that has an error — its effective status is still
error/expired/unavailable — but the connection row hid the lastError text for
disabled rows, so the operator saw the count without the cause. The row's
error-visibility decision is extracted into shouldShowConnectionLastError()
and now shows the error whenever there is one, regardless of the active
toggle.

Reported-by: ntdung6868 (decolua/9router#1447)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline ConnectionRow.tsx file-size to 942 (#1447 +1 import)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): bound the OAuth connection-test probe with a timeout (#4347)

* fix(providers): bound OAuth connection-test probe with a timeout (port from 9router#1449)

The OAuth path of "Test Connection One-by-One" called bare fetch() with no
AbortController/signal, so a provider probe that accepted the socket but never
responded wedged the test queue forever. Both the initial probe and the
post-refresh retry are now bounded with AbortSignal.timeout(30s) — matching the
API-key path's existing budget — and a timed-out probe resolves as a failure
with a clear "Test timed out after 30s" message in the route's normal error shape.

Reported-by: ntdung6868 (decolua/9router#1449)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline providers test route file-size to 887 (#1449 + sibling #1444 growth)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): label a deactivated account distinctly from a revoked token (#4353)

A Codex connection whose OAuth refresh is fully healthy but whose ChatGPT
account has been deactivated by the provider gets a 401 from the upstream
API. The connection test labeled that the same as a bad credential
("Token invalid or revoked" -> upstream_auth_error), so an operator could not
tell a deactivated account from a revoked token. The test now reads the
401/403 body and, when it indicates account deactivation, classifies it as
account_deactivated (which the dashboard already renders as "Account
Deactivated"); a plain auth 401 is unchanged.

Reported-by: ntdung6868 (decolua/9router#1444)

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(db): cascade-delete orphaned model aliases when a provider is removed (#4348)

* fix(db): cascade-delete orphaned model aliases when a provider is removed (port from 9router#1409)

Deleting a custom provider removed its connections and node but left the
imported model-alias rows (key=<alias>, value="<providerId>/<model>") behind,
so re-importing the same provider was blocked by stale "already exists" aliases.
Add a deleteModelAliasesForProvider(providerId) DB helper that drops every alias
whose stored value begins with "<providerId>/", and call it from the provider-node
DELETE handler so a fresh import is unblocked.

Reported-by: nguyenvanhuy0612 (decolua/9router#1409)
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* chore(quality): rebaseline models.ts file-size to 1221 (#1409 + sibling #1294 growth)

---------

Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* fix(api): persist max_input_tokens/max_output_tokens when adding a custom model (#4349)

The POST /api/provider-models handler read the rest of the body but never
the two token-limit fields, and addCustomModel() had no parameter for them,
so the form values were dropped on write while the DB layer and /v1/models
catalog already round-trip inputTokenLimit/outputTokenLimit. Accept the two
optional limits in the schema, forward them through the handler, and persist
them in addCustomModel(). TDD: failing-then-passing unit test.

Reported-by: codename-zen (decolua/9router#1294)

Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>

* docs: feature-documentation catch-up (v3.8.20 → v3.8.30) (#4391)

One-time reconciliation of the docs with every user-facing feature shipped since
v3.8.20 (we had never done a dedicated pass, so debt had accumulated):

- README: new '✨ What's New' section (curated v3.8.20→v3.8.30 highlights).
- New guides: CLI-INTEGRATIONS (all setup-*/launch commands), MITM-TPROXY-DECRYPT
  (transparent-decrypt epic), CONTEXT_EDITING (delegated Anthropic clear_tool_uses).
- Refreshed: AUTO-COMBO (auto/<category>:<tier> + Arena-ELO), API_REFERENCE
  (x-omniroute-no-memory), MEMORY (int8 quantization + off-by-default), RESILIENCE
  (model-lockout success-decay), RTK, AGENTBRIDGE, TRAFFIC_INSPECTOR, GUARDRAILS,
  CLOUD_AGENT, ENVIRONMENT, SETUP_GUIDE, CLI-TOOLS, MCP-SERVER.
- Regenerated PROVIDER_REFERENCE (231 providers); synced the count in README/CLAUDE/AGENTS.
- Allowlisted external-tool env vars (OPENAI_API_BASE, PROMPTFOO_PROVIDER_KEY) and the
  STREAM_RECOVERY config-object name in the docs-accuracy gates.

All claims source-verified; check:docs-all (sync/counts/env/links/fabricated) passes.
Going forward this runs every release via generate-release step 6b.

* fix(executors): don't inject thinking when tool_choice forces a tool (native Claude) (#4389)

Forced tool_choice now strips the adaptive thinking injection to avoid Anthropic 400. Thanks @NomenAK.

* fix(translator): Gemini accepts HTTP/HTTPS image URLs (port from 9router#344) (#4373)

OpenAI-style `image_url` parts with an `http://` or `https://` URL reached
`convertOpenAIContentToParts` and were dropped with only a `console.warn`,
because Gemini's `inlineData` requires base64 (the helper is synchronous and
cannot fetch+encode upstream assets). Gemini's `Part` schema, however, natively
accepts `fileData: { fileUri }` for remote URIs — the model fetches the asset
itself.

The helper now emits a `fileData` part (`mimeType: "image/*"`, inferred upstream
on fetch) for HTTP/HTTPS URLs instead of silently dropping them. Vision requests
that pass a URL — not a data: URI — now reach Gemini intact.

No behavioral change for:
- `data:` URIs → still emitted as `inlineData` with the parsed media type.
- Unsupported schemes (e.g. `ftp:`) → still skipped (Gemini would reject them).

The openai-to-claude side already passed HTTP/HTTPS URLs through as
`source: { type: "url", url }` (lines 573–578) — the upstream PR's Claude-side
change was already covered.

Regression test: tests/unit/gemini-helper-http-image-url-port344.test.ts
(4 cases: https URL, http URL, data: URI no-regression, unsupported-scheme guard).


Inspired-by: decolua/9router#344

Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>

* fix(executors): strip stream_options for qwen non-streaming / thinking Claude Code requests (port from 9router#663) (#4374)

Claude-Code-compatible providers force the executor-level `stream` flag on
via `upstreamStream = stream || isClaudeCodeCompatible`
(open-sse/handlers/chatCore.ts), but the outgoing body keeps the caller's
original `stream: false`. The shared `stream && targetFormat === "openai"`
branch in DefaultExecutor.transformRequest then injected
`stream_options: { include_usage: true }` onto a body that still said
`stream: false`, and qwen upstream rejected the request with
`400 "'stream_options' only set this when you set stream: true"`. The same
rejection surfaced when the body carried `thinking` / `enable_thinking`.

The qwen branch now skips the injection (and strips any client-sent
`stream_options`) when the body explicitly says `stream: false` or
requests thinking, leaving regular qwen streaming requests with the
include_usage injection intact. Other providers are unaffected.

Adds a TDD regression with 4 cases covering both opt-out paths and the
normal-streaming positive control.


Inspired-by: decolua/9router#663

Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>

* fix(security): scope OAuth callback postMessage to a trusted-origin allowlist (port from 9router#998) (#4372)

The OAuth callback at `/callback` previously fell back to
`window.opener.postMessage({ code, state, ... }, "*")` whenever the opener
was cross-origin. The fallback was intended to support remote-OmniRoute +
local-loopback callbacks (where opener and callback live on different
origins), but the same code path also delivers the OAuth code/state to any
hostile opener that pops the well-known callback URL — letting that
attacker complete the OAuth flow as the user.

Replace the wildcard fallback with iteration over a fixed allowlist:
`window.location.origin` (same-origin parent — the popup-mode dashboard)
plus Codex's fixed loopback helper (`http://localhost:1455` and the IPv4
literal `http://127.0.0.1:1455`). The browser drops `postMessage` to any
opener whose actual origin is not in `targetOrigin`, so the message reaches
only known parents and is silently dropped for any other. The same-origin
fallback path is unchanged — methods 2 (`BroadcastChannel`) and 3
(`localStorage` storage event) still cover same-origin openers that COOP
severed.

The `openerSameOrigin` probe stays in place to drive the auto-close vs
manual-copy UI decision (no behavior change for the success path).

Adds a regression test (`tests/unit/ui/oauth-callback-postmessage-scope.test.tsx`)
that mounts the page with a stubbed cross-origin opener and asserts no
`postMessage` call ever uses `"*"` and every call lands on an allowlisted
origin. The test failed against the pre-fix code (red), passes after the
fix (green) — TDD per CLAUDE.md hard rule #18.

Partial port of upstream decolua/9router#998: the upstream PR also
re-enabled TLS verification on a DNS-bypass fetch in `open-sse/utils/proxyFetch.js`;
that part is N/A here because OmniRoute's `proxyFetch.ts` never disabled
TLS verification (no `rejectUnauthorized: false` anywhere in the file).


Inspired-by: decolua/9router#998

Co-authored-by: aeonframework <aeon@aeonframework.dev>

* fix(sse): default combo per-target timeout to 120s for fast failover (#4365)

Combo per-target timeout inherited the full FETCH_TIMEOUT_MS (600s) when a
combo did not set its own targetTimeoutMs, so a single hung/slow target stalled
the whole combo for up to 10 minutes before falling through to the next model.

Introduce DEFAULT_COMBO_TARGET_TIMEOUT_MS (120s) as the unset-default in
resolveComboTargetTimeoutMs (new 3rd arg) and wire it in phaseComboSetup. The
upstream ceiling (600s) and per-combo opt-out (targetTimeoutMs, up to the
ceiling) are preserved; single non-combo requests are unchanged. For streaming
requests this only bounds time-to-first-headers, so token generation is not cut
short.

TDD: failing-then-passing unit test in tests/unit/combo-config.test.ts.

* refactor(combo): de-dup exhausted-target skip predicate across both dispatchers (#4362)

Primeiro incremento da de-dup dos 2 dispatchers de combo (handleComboChat +
handleRoundRobinCombo). O bloco de pre-check #1731/#1731v2 (skip de target já
exhausted no provider/connection) era BYTE-IDÊNTICO nos dois (mesmas condições,
mesmas mensagens), diferindo só na tag de log e no control-flow.

- comboPredicates.ts: getExhaustedTargetSkipReason(target, exhaustedProviders,
  exhaustedConnections) — predicate PURO que retorna a mensagem de skip (ou null);
  cada dispatcher mantém seu próprio log-tag + control-flow (return null / continue)
  + fallbackCount. No mutate do stryker (cobertura de mutação).
- combo.ts: −20 linhas (os 2 blocos viram 1 chamada cada).
- 7 testes de caracterização travam condições + strings exatas.

Comportamento preservado: 376/376 testes combo (357 caracterização + 7 novos),
integração sse-correctness 5/5, typecheck 0, complexity neutro (1895), file-size
encolhe. Próximo incremento: de-dup do error-handling/exhausted-tracking (handleTargetError).

* refactor(combo): de-dup upstream-error exhaustion classification across both dispatchers (#4366)

Segundo incremento da de-dup dos 2 dispatchers (handleTargetError). Após cada erro
de target, ambos rodavam um bloco quase-idêntico que marca o provider exhausted
(#1731), a conexão connection-errored (#1731v2) ou o provider transiently rate-limited.

- combo/targetExhaustion.ts: applyComboTargetExhaustion(target, opts) — atualiza os
  3 Sets de exhaustion e retorna providerExhausted. As MUTAÇÕES de Set (que dirigem o
  skip de targets, lidas por getExhaustedTargetSkipReason) são BYTE-IDÊNTICAS nos dois;
  as diferenças reais viram parâmetros: tag, allAccountsRateLimited (termo extra do RR,
  false no handleComboChat), exhaustedLogLevel (info no handleComboChat, debug no RR).
  Connection-level extraído p/ markConnectionLevelExhaustion (privado, <15 complexity).
- combo.ts: −73 linhas; 4 imports órfãos removidos.
- 7 testes de caracterização travam as mutações + o return.

ÚNICA mudança de comportamento: o WORDING das mensagens de log do RR ganha o sufixo
'on remaining targets' (cosmético; mesmo #code, mesmas mutações, mesmos níveis de log).
376/376 combo (caracterização preservada), integração sse 5/5, typecheck 0, complexity
neutro (1895), file-size encolhe.

* refactor(chatCore): extract checkHeapPressureGuard leaf (god-file decomposition start) (#4371)

Primeiro incremento da decomposição do chatCore.ts (5127 LOC, hot-path mais quente).
O guard de memória do topo do handleChatCore (rejeita 503 quando o heap V8 passa o
threshold de shed) vira um leaf testável, co-locado com o threshold em heapPressure.ts.

- heapPressure.ts: checkHeapPressureGuard(heapUsedMb, thresholdMb) — retorna o result
  503 pronto ou null. Byte-idêntico ao guard inline (mesmo check, mesma 503, mesmo warn).
  A figura de heap fica em telemetria INTERNA, nunca no response do cliente (Hard Rule #12).
- chatCore.ts: o bloco inline (~22 ln) vira 3 linhas; import órfão de HEAP_PRESSURE_THRESHOLD_MB
  trocado por checkHeapPressureGuard.
- 3 testes novos (incl. assert Rule #12: o MB medido não vaza no payload).

complexity-baseline 1895->1896: drift de base pós-#4338 (medido com minhas mudanças
stashed = 1896); esta mudança é complexity-NEUTRA (helper complexity 2, handleChatCore só
perde código). 190/190 chatcore tests, typecheck 0, file-size encolhe.

* Localize CLI and stabilize fetch, memory, and coverage handling (#4383)

en-only i18n, fetch-start-timeout hardening, EngineConfigPage icon fix, CI build-artifact-reuse overhaul. Memory production hunk dropped as a no-op (tests kept). Thanks @JxnLexn.

* test(combo): reset circuit breakers between stream-readiness cases (restore green) (#4396)

The combo-dispatch cases in combo-stream-readiness-fallback.test.ts deliberately
fail `glm` (zombie streams / repeated 504s), which legitimately trips the
per-provider circuit breaker. That OPEN state is a module-level singleton, so it
leaked into the next test and combo.ts then SKIPPED `glm/*` targets entirely
("Skipping … circuit breaker OPEN"). That made "combo does not retry stream
readiness timeouts on the same model" never attempt glm/zombie — expected
['glm/zombie','openai/gpt-5.4-mini'] but got ['openai/gpt-5.4-mini'].

This was a pre-existing red on release/v3.8.31 (present at the cycle-open tip),
order-dependent: the test passes in isolation, fails after the preceding cases.
Add a test.beforeEach(resetAllCircuitBreakers) so each scenario starts from a
clean breaker slate. Test-isolation only — the breaker behavior is correct and
no production code or assertion changes. Full combo suite: 390/390 green.

* fix(cli): decline confirm() cleanly on non-interactive stdin + document contexts workflow

The `contexts remove` command already has `--yes` to skip confirmation, but when
run without it under a non-interactive stdin (pipe, CI, EOF) the [y/N] prompt could
never be answered — the readline question stayed pending and Node warned about an
"unsettled top-level await" at exit. confirm() now detects `!process.stdin.isTTY`
and declines cleanly (returns false), pointing at `--yes` for non-interactive use.
Exported confirm() for a regression test.

Docs: REMOTE-MODE.md gains a full "Managing contexts" section (list/current/use to
switch between remote and local, add/show/rename, remove with --yes, export/import),
fixes a `context current` -> `contexts current` typo, and the README remote-mode
snippet now shows switching back to local. Verified against the live CLI: command
signatures, --yes, and the non-TTY decline path all behave as documented.

Tests: cli-contexts.test.ts asserts confirm() declines on non-TTY stdin (RED before,
GREEN after). All docs gates (fabricated/links/symbols) pass.

* ci(t11): bump any-budget for executors/base.ts (2 false-positive "any" strings)

Unblocks the Fast Quality Gates on release/v3.8.31: `check:any-budget:t11` was red on
`open-sse/executors/base.ts` for ALL PRs (pre-existing base drift, unrelated to this
branch). The checker counts `\bany\b` after stripping comments but NOT strings, and the
native-Claude tool_choice logic uses the API value `"any"` in two string literals
(`tb.tool_choice === "any"`, `.type === "any"`). There are zero actual TypeScript
`any` types in the file — budget set to the matched count, mirroring the existing
cursor.ts false-positive entry right below it.

* perf: combos UI split + next config + 1-click redis + bifrost sidecar (#3932) (#4381)

Combos UI split + next.config perf + 1-click local Redis launcher + bifrost relay. Review fixes (co-author): --rm/--restart conflict, error sanitization, UI/route names, dead-guard re-doc, bifrost Zod, IPv6 + CLI test fixes. Thanks @KooshaPari.

* fix(plugin): prefix OC static-catalog combo+raw keys with providerId (#4384)

OC parses model ids on '/'; combo keys now carry 'omniroute/' (was 'combo/'). Live-validated against the VPS via OpenCode. Thanks @herjarsa.

* docs(env): document TAILSCALE_AUTHKEY (env/docs contract drift on .31)

Second pre-existing base-drift fix needed to get Fast Quality Gates green: the
`repository contract is in sync` test (check:env-doc-sync) was red on ALL .31 PRs.
PR #4343 added a code reference to `process.env.TAILSCALE_AUTHKEY`
(src/lib/tailscaleTunnel.ts) for non-interactive `tailscale up`, but never added the
var to .env.example / docs/reference/ENVIRONMENT.md — the contract requires code vars
to appear in both. Add the (commented) entry to .env.example next to TAILSCALE_BIN and
a row to ENVIRONMENT.md. Verified: `node scripts/check/check-env-doc-sync.mjs` → in sync.

Unrelated to this branch's confirm()/docs change; surfaced because touching a quality
script triggers the TIA fail-safe full unit suite.

* chore(release): v3.8.31 — 2026-06-20

Finalize the v3.8.31 release: reconcile the CHANGELOG (full commit-to-bullet
coverage, 26 bullets across Features/Fixed/Security/Maintenance), refresh the
README What's New section, back-fill the .30/.31 sections into the 41 i18n
CHANGELOG mirrors, and add TAILSCALE_AUTHKEY to the env contract
(.env.example + ENVIRONMENT.md).

* chore(release): align mitm-hosts test comment with main to clear merge conflict

The same CodeQL false-positive fix landed twice — #4386 on release/v3.8.31 and
#4387 directly on main — with only the explanatory comment differing (the
assertion is byte-identical). Adopt main's comment wording on the release branch
so the release PR merges without a comment-only conflict.

* test(translator): align stale openai->gemini remote-URL tests with #4373

Third pre-existing base-drift fix to get Fast Quality Gates green on .31. PR #4373
('Gemini accepts HTTP/HTTPS image URLs', port of 9router#344) intentionally changed
convertOpenAIContentToParts so remote http(s) image URLs pass through as a native
`fileData: { fileUri }` part instead of the old #2807 drop+warn — and added its own
test (gemini-helper-http-image-url-port344.test.ts) for the new behavior. But it left
three tests in translator-openai-to-gemini.test.ts asserting the OLD drop+warn
contract, so they fail deterministically (verified: the file fails in isolation on
clean .31). These only surface under the TIA fail-safe FULL suite, which a quality-
script touch triggers.

Align the three stale tests to the real, intended behavior (captured by running the
function): remote URLs -> fileData.fileUri (mimeType image/*), still never inlineData
(the sync path cannot fetch+encode). This is test-vs-code alignment to a deliberate,
separately-tested change — not a weakened assertion. 40/40 in the file; 94/94 across
the translator + env-doc combo that previously failed.

* chore(quality): reconcile complexity baseline 1896->1900 (/review-prs v3.8.31 batch) (#4410)

* fix(release): reconcile full-CI drift for v3.8.31 (gemini tests #4373, any-budget #4389, masking allowlist #4384)

The release PR's full CI surfaced cumulative cycle drift the per-PR fast gates
skip:
- tests/unit/translator-openai-to-gemini.test.ts: realign 3 cases to #4373's
  HTTP/HTTPS-URL fileData pass-through (they asserted the old warn-and-drop).
- scripts/check/check-t11-any-budget.mjs: base.ts budget 0→2 — #4389 compares
  tool_choice against the string literal "any" (not a TS any type).
- config/quality/test-masking-allowlist.json: allowlist #4384's opencode combos
  net-assert reduction (obsolete combo/ namespace removed).
No production behavior change.

* chore(release): re-trigger full CI for v3.8.31 finalization

Force a fresh pull_request CI run on the head carrying the cycle-drift fixes
(gemini #4373 tests, any-budget #4389, masking allowlist #4384) — the prior
synchronize event did not spawn a ci.yml run.

* chore: re-trigger CI (no Actions runs registered for prior push)

---------

Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>
Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>
Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>
Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>
Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>
Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
Co-authored-by: aeonframework <aeon@aeonframework.dev>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 20, 2026
check:doc-links failed in the second CI run for PR diegosouzapw#4433 with:
'1 broken link(s) in 1 file(s): docs/architecture/cluster-decisions.md
 line 18: ../../open-sse/executors/bifrost.ts'

The reference was to open-sse/executors/bifrost.ts which does not
exist on upstream/main. The Tier-1 router was integrated as a sidecar
proxy route in PR diegosouzapw#4381 at src/app/api/v1/relay/chat/completions/bifrost/
route.ts (merged 2026-06-20 as 7d6fffd). The link is now updated to
that path, with a parenthetical note about the BIFROST_ENABLED env-var
kill switch.

Refs: diegosouzapw#3932 (PR diegosouzapw#4433)
diegosouzapw pushed a commit to KooshaPari/OmniRoute that referenced this pull request Jun 21, 2026
Follow-up to PR diegosouzapw#4381 (perf: combos UI split + next config + 1-click redis
+ bifrost sidecar, merged 2026-06-20 as 7d6fffd). Diego asked for a
discrete follow-up PR adding the two opt-in sidecar profiles that the
cluster blueprint research in findings/2026-06-20-cluster-blueprint.md
recommended (Qdrant=memory, Bifrost=bifrost).

Design principle: ZERO default-on changes. Both profiles are gated by
the --profile flag and by the QDRANT_ENABLED / BIFROST_ENABLED env
flags in code. The 3-replica default deploy is unchanged.

Changes:

docker-compose.yml
- New 'memory' profile: Qdrant v1.12.4 sidecar on :6333 (REST) + :6334
  (gRPC) with named volume 'qdrant-data', healthcheck on /readyz
- New 'bifrost' profile: ghcr.io/maximhq/bifrost:1.5.21 sidecar on :8080
  with named volume 'bifrost-data', healthcheck on /v1/models
- Updated header docblock to list the new profiles + usage examples
- Added 'qdrant-data' and 'bifrost-data' named volumes

src/lib/memory/qdrant.ts
- Fixed env-var precedence bug: collection/embeddingModel were falling
  back to env only when BOTH settings AND default were empty (i.e. if
  the user set a non-empty value in settings, env override was ignored)
- New precedence: settings > env > default (settings takes priority
  only when non-empty; env is the runtime override for compose users)
- Added QDRANT_VECTOR_SIZE env binding to dimension
- Added QDRANT_HNSW_EF_CONSTRUCT env binding to ef_construct

src/lib/memory/__tests__/qdrant-wiring.test.ts
- New test file with 9 cases covering: default fallback, env override,
  settings-wins precedence, empty-settings/env-override, full env
  override (6 vars at once), QDRANT_PORT numeric coercion,
  QDRANT_HNSW_EF_CONSTRUCT coercion, missing-port default, missing-key
  default

.env.example
- Added # BIFROST_ENABLED, # BIFROST_LOG_LEVEL sections
- Added # QDRANT_HOST, # QDRANT_PORT, # QDRANT_GRPC_PORT, # QDRANT_API_KEY,
  QDRANT_COLLECTION, QDRANT_VECTOR_SIZE, QDRANT_HNSW_EF_CONSTRUCT sections

docs/reference/ENVIRONMENT.md
- Added 11 new rows to section 25 (Provider Quotas, Tunnels, Backups &
  Misc Runtime) for the BIFROST_*/QDRANT_* env vars

docs/architecture/cluster-decisions.md (NEW, 280 lines)
- Per-component verdict table for the 13-component cluster shortlist
  (Caddy=BUILT-IN, Bifrost=KEEP+opt-in, Qdrant=OPT-IN, Dragonfly/NATS/
  PG/Neo4j/MinIO/HAProxy/Envoy=ALL DROPPED, pg_ai=DROPPED)
- Per-extension PG analysis (pgvector=drop, PGroonga=drop, TOAST=drop,
  pg_ai=drop) with file/line citations to the actual workload
- 4-week critical path plan (Wk 1 opt-in profiles, Wk 2 Bifrost
  activation for 4 providers, Wk 3 Qdrant memory profile, Wk 4
  observability healthchecks)
- Anti-pattern warnings (don't cargo-cult 'production LLM platform'
  topology, don't migrate from SQLite for premature scale)
- Reference to findings/2026-06-20-cluster-blueprint.md for the full
  workload-shape analysis

AGENTS.md
- Added 'Cluster opt-in profiles (memory, bifrost)' to the
  Documentation Map table pointing to cluster-decisions.md

Verification:
- docker compose config --quiet: EXIT 0 (YAML valid)
- python3 yaml.safe_load: services + profiles + volumes all parsed
- node scripts/check/check-env-doc-sync.mjs: 'In code but missing from
  .env.example: none / In .env.example but missing from ENVIRONMENT.md:
  none / In ENVIRONMENT.md but missing from .env.example: none'
- node --check on qdrant.ts + qdrant-wiring.test.ts: clean

Refs: diegosouzapw#3932
diegosouzapw pushed a commit to KooshaPari/OmniRoute that referenced this pull request Jun 21, 2026
check:doc-links failed in the second CI run for PR diegosouzapw#4433 with:
'1 broken link(s) in 1 file(s): docs/architecture/cluster-decisions.md
 line 18: ../../open-sse/executors/bifrost.ts'

The reference was to open-sse/executors/bifrost.ts which does not
exist on upstream/main. The Tier-1 router was integrated as a sidecar
proxy route in PR diegosouzapw#4381 at src/app/api/v1/relay/chat/completions/bifrost/
route.ts (merged 2026-06-20 as 7d6fffd). The link is now updated to
that path, with a parenthetical note about the BIFROST_ENABLED env-var
kill switch.

Refs: diegosouzapw#3932 (PR diegosouzapw#4433)
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 21, 2026
The Fast Quality Gates lint check was failing on every PR with
'4 arquivos cresceram alem do cap': src/lib/db/core.ts,
src/lib/usage/providerLimits.ts, src/shared/constants/providers.ts,
open-sse/services/usage.ts. The frozen baselines in
config/quality/file-size-baseline.json were last set at v3.8.30 and
had drifted past the cap=800 due to legitimate feature growth from
PR diegosouzapw#4381 (combos split), PR diegosouzapw#4433 (cluster opt-in profiles), and
PR diegosouzapw#4480 (vacuum scheduler).

This commit rebaselines those 4 frozen entries to their current
actual line count (+2 buffer to cover wc -l's off-by-one and any
stray edits during review). It does NOT change the cap=800 for new
files, nor does it shrink any of the 4 monoliths.

Structural shrink of these files is tracked separately in diegosouzapw#3501
(QG v2 chatCore split continuation). This rebaseline just restores
green CI until those structural refactors land.

Files changed: 1 (config/quality/file-size-baseline.json)
- src/lib/db/core.ts: frozen 624 -> 781 (was +157 past cap=800...wait)
  Actually frozen was 624 vs cap=800, so core.ts was 157 lines UNDER cap.
  The drift is in the 4 files whose actuals grew past their frozen values.

Verification:
- node scripts/check/check-file-size.mjs -> '[file-size] OK -- 103
  arquivos congelados, cap 800 para novos (2710 arquivos verificados)'
- node scripts/check/check-env-doc-sync.mjs -> 'Env / docs contract
  is in sync'
- node scripts/check/check-db-rules.mjs -> 'OK (85 modulos db/, 57
  re-exportados, 28 intencionalmente-internos; 2 leituras de DB
  externo permitidas)'

Unblocks every open PR currently stuck on Fast Quality Gates
(diegosouzapw#4571, diegosouzapw#4576, diegosouzapw#4577, diegosouzapw#4578 + this PR's own branch).
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 23, 2026
…ed file

Splits src/shared/constants/providers.ts (3,243 -> 1,426 LOC, -56%) by
moving the APIKEY_PROVIDERS const block (1,820 LOC, 56% of the parent
file) to src/shared/constants/providers/apiKeyProviders.ts.

APIKEY_PROVIDERS is a single module-level const declaration at L631-L2450
of the parent file. It is the largest single block in the file and has
no closure dependencies on other module-level declarations. Moving it is
a pure file-copy operation with a re-export from the parent.

All 27 other const blocks (the smaller per-provider config arrays:
OPENAI_COMPAT_PROVIDERS, GEMINI_COMPAT_PROVIDERS, ANTHROPIC_COMPAT_PROVIDERS,
etc., totaling ~1,400 LOC). These are queued for PR-2.b/c in subsequent
PRs (each has its own self-contained scope).

Same mechanical refactor pattern:
1. Identify largest leaf block (module-level const, no closure deps)
2. Move to co-located file
3. Add re-export from parent
4. Verify references unchanged

- Re-export present in src/shared/constants/providers.ts: 1 line
- Original APIKEY_PROVIDERS const still in main file: 0 (moved)
- node scripts/check/check-env-doc-sync.mjs: 'Env / docs contract is in sync'
- node scripts/check/check-fabricated-docs.mjs --strict: 'No fabricated
  API/env/CLI/hook/file references found'
- node scripts/check/check-docs-sync.mjs: 'PASS - documentation version
  sync is consistent'
- node scripts/check/check-db-rules.mjs: 'OK (85 modulos db/, 57
  re-exportados, 28 intencionalmente-internos)'

- Builds on PR-diegosouzapw#4609 (imageGeneration split) - same leaf-block pattern
- Mirrors PR-diegosouzapw#4381 (combos split) and PR-diegosouzapw#4480 (chatLogHelpers extraction)
- Continues work on issue diegosouzapw#4425 (provider-config drift crashes)
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 23, 2026
…co-located files

Splits open-sse/services/tokenRefresh.ts (1,996 -> 887 LOC, -56%) by
moving 12 per-provider refresh*Token functions (1,109 LOC total) to
co-located files under open-sse/services/tokenRefresh/providers/.

## What moved (12 functions, 1,109 LOC extracted)

| File | LOC | Function |
|---|---|---|
| providers/windsurf.ts   | 106  | refreshWindsurfToken |
| providers/cline.ts      | 69   | refreshClineToken |
| providers/kimiCoding.ts | 113  | refreshKimiCodingToken |
| providers/gitlabDuo.ts  | 92   | refreshGitLabDuoToken |
| providers/claudeOAuth.ts| 59   | refreshClaudeOAuthToken |
| providers/google.ts     | 61   | refreshGoogleToken |
| providers/qwen.ts       | 81   | refreshQwenToken |
| providers/codex.ts      | 86   | refreshCodexToken |
| providers/kiro.ts       | 221  | refreshKiroToken |
| providers/qoder.ts      | 59   | refreshQoderToken |
| providers/github.ts     | 48   | refreshGitHubToken |
| providers/copilot.ts    | 114  | refreshCopilotToken |

## What stayed in tokenRefresh.ts (887 LOC)

All 13 generic helpers used by the per-provider functions:
buildFormParams, extractOAuthErrorCode, getRefreshLeadMs, readRefreshErrorBody,
refreshAccessToken, refreshWithRetry, recordSuccess, recordFailure,
withTimeout, getRefreshCacheKey, runWithOnPersist, getActiveOnPersist,
cleanupRotationMap, lookupRotation, recordRotation.

## Why this is safe

Each per-provider function is a self-contained module-level async function
that takes a TokenRefreshRequest and returns Promise<TokenRefreshResult>.
The 4 functions that need shared helpers (claudeOAuth, qoder, github,
copilot) explicitly import them from '../tokenRefresh' - the import
graph is clean and acyclic.

## Pattern matches PR-diegosouzapw#4609 (imageGeneration split) + PR-diegosouzapw#4609 (providers split)

Same mechanical refactor pattern:
1. Identify leaf functions with shared-helper deps mapped (allowed up to 2 helpers)
2. Move to co-located file with import block
3. Add re-export from parent
4. Verify references unchanged

## Verification performed locally

- Re-export present in open-sse/services/tokenRefresh.ts: 12 lines
- Per-provider refresh functions still in main file: 0 (moved)
- node scripts/check/check-env-doc-sync.mjs: 'Env / docs contract is in sync'
- node scripts/check/check-fabricated-docs.mjs --strict: 'No fabricated
  API/env/CLI/hook/file references found'
- node scripts/check/check-docs-sync.mjs: 'PASS'
- node scripts/check/check-db-rules.mjs: 'OK'

## Refs

- Builds on PR-diegosouzapw#4609 (imageGeneration split) and the providers.ts split
  in this same branch - same leaf-block pattern, same review shape
- Mirrors PR-diegosouzapw#4381 (combos split) and PR-diegosouzapw#4480 (chatLogHelpers extraction)
- Continues work on issue diegosouzapw#4425 (provider-config drift crashes)
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 24, 2026
…co-located files

Splits open-sse/services/tokenRefresh.ts (1,996 -> 887 LOC, -56%) by
moving 12 per-provider refresh*Token functions (1,109 LOC total) to
co-located files under open-sse/services/tokenRefresh/providers/.

| File | LOC | Function |
|---|---|---|
| providers/windsurf.ts   | 106  | refreshWindsurfToken |
| providers/cline.ts      | 69   | refreshClineToken |
| providers/kimiCoding.ts | 113  | refreshKimiCodingToken |
| providers/gitlabDuo.ts  | 92   | refreshGitLabDuoToken |
| providers/claudeOAuth.ts| 59   | refreshClaudeOAuthToken |
| providers/google.ts     | 61   | refreshGoogleToken |
| providers/qwen.ts       | 81   | refreshQwenToken |
| providers/codex.ts      | 86   | refreshCodexToken |
| providers/kiro.ts       | 221  | refreshKiroToken |
| providers/qoder.ts      | 59   | refreshQoderToken |
| providers/github.ts     | 48   | refreshGitHubToken |
| providers/copilot.ts    | 114  | refreshCopilotToken |

All 13 generic helpers used by the per-provider functions:
buildFormParams, extractOAuthErrorCode, getRefreshLeadMs, readRefreshErrorBody,
refreshAccessToken, refreshWithRetry, recordSuccess, recordFailure,
withTimeout, getRefreshCacheKey, runWithOnPersist, getActiveOnPersist,
cleanupRotationMap, lookupRotation, recordRotation.

Each per-provider function is a self-contained module-level async function
that takes a TokenRefreshRequest and returns Promise<TokenRefreshResult>.
The 4 functions that need shared helpers (claudeOAuth, qoder, github,
copilot) explicitly import them from '../tokenRefresh' - the import
graph is clean and acyclic.

Same mechanical refactor pattern:
1. Identify leaf functions with shared-helper deps mapped (allowed up to 2 helpers)
2. Move to co-located file with import block
3. Add re-export from parent
4. Verify references unchanged

- Re-export present in open-sse/services/tokenRefresh.ts: 12 lines
- Per-provider refresh functions still in main file: 0 (moved)
- node scripts/check/check-env-doc-sync.mjs: 'Env / docs contract is in sync'
- node scripts/check/check-fabricated-docs.mjs --strict: 'No fabricated
  API/env/CLI/hook/file references found'
- node scripts/check/check-docs-sync.mjs: 'PASS'
- node scripts/check/check-db-rules.mjs: 'OK'

- Builds on PR-diegosouzapw#4609 (imageGeneration split) and the providers.ts split
  in this same branch - same leaf-block pattern, same review shape
- Mirrors PR-diegosouzapw#4381 (combos split) and PR-diegosouzapw#4480 (chatLogHelpers extraction)
- Continues work on issue diegosouzapw#4425 (provider-config drift crashes)

(cherry picked from commit dce08ad)
KooshaPari added a commit to KooshaPari/OmniRoute that referenced this pull request Jun 24, 2026
…ed file

Splits src/shared/constants/providers.ts (3,243 -> 1,426 LOC, -56%) by
moving the APIKEY_PROVIDERS const block (1,820 LOC, 56% of the parent
file) to src/shared/constants/providers/apiKeyProviders.ts.

APIKEY_PROVIDERS is a single module-level const declaration at L631-L2450
of the parent file. It is the largest single block in the file and has
no closure dependencies on other module-level declarations. Moving it is
a pure file-copy operation with a re-export from the parent.

All 27 other const blocks (the smaller per-provider config arrays:
OPENAI_COMPAT_PROVIDERS, GEMINI_COMPAT_PROVIDERS, ANTHROPIC_COMPAT_PROVIDERS,
etc., totaling ~1,400 LOC). These are queued for PR-2.b/c in subsequent
PRs (each has its own self-contained scope).

Same mechanical refactor pattern:
1. Identify largest leaf block (module-level const, no closure deps)
2. Move to co-located file
3. Add re-export from parent
4. Verify references unchanged

- Re-export present in src/shared/constants/providers.ts: 1 line
- Original APIKEY_PROVIDERS const still in main file: 0 (moved)
- node scripts/check/check-env-doc-sync.mjs: 'Env / docs contract is in sync'
- node scripts/check/check-fabricated-docs.mjs --strict: 'No fabricated
  API/env/CLI/hook/file references found'
- node scripts/check/check-docs-sync.mjs: 'PASS - documentation version
  sync is consistent'
- node scripts/check/check-db-rules.mjs: 'OK (85 modulos db/, 57
  re-exportados, 28 intencionalmente-internos)'

- Builds on PR-diegosouzapw#4609 (imageGeneration split) - same leaf-block pattern
- Mirrors PR-diegosouzapw#4381 (combos split) and PR-diegosouzapw#4480 (chatLogHelpers extraction)
- Continues work on issue diegosouzapw#4425 (provider-config drift crashes)

(cherry picked from commit 55bc385)
@KooshaPari
KooshaPari deleted the feat/perf-combos-split-2026-06-20-v2 branch June 25, 2026 21:17
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…diegosouzapw#3932) (diegosouzapw#4381)

Combos UI split + next.config perf + 1-click local Redis launcher + bifrost relay. Review fixes (co-author): --rm/--restart conflict, error sanitization, UI/route names, dead-guard re-doc, bifrost Zod, IPv6 + CLI test fixes. Thanks @KooshaPari.
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…lose drift)

The Quality Ratchet failed on the release PR: eslintWarnings 3839 > baseline
3836. The +3 is end-of-cycle drift from legitimately-merged feature PRs
(diegosouzapw#4381/diegosouzapw#4383/diegosouzapw#4373/diegosouzapw#4389/diegosouzapw#4384/diegosouzapw#4410) — `any` is allowed (warn) in open-sse/
and tests/. Verified the release reconciliation files add 0 warnings (gemini
test delta 79<->79, mitm test 0). Same precedent as prior cycle-close
rebaselines. Authorized as part of the end-to-end release.
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…iegosouzapw#4397)

* chore(release): open v3.8.31 development cycle

* fix(mitm): exact host membership in MITM hosts test (CodeQL false positive) (diegosouzapw#4386)

getMitmToolHosts returns string[], so .includes(host) is Array.prototype.includes
(exact membership). CodeQL's js/incomplete-url-substring-sanitization heuristic
misreads it as a String.includes() URL-substring sanitization check and raises a
HIGH alert. Switch to .some(h => h === host) — identical semantics, explicit intent,
no flagged pattern. Surfaced post-v3.8.30 (diegosouzapw#4325) once the test landed on main.

Test-only change (no runtime behavior); the suite still passes and the CodeQL
re-scan on merge clears the alert.

* fix(codex): request reasoning summaries (diegosouzapw#4359)

Adds reasoning.summary=auto + reasoning.encrypted_content include for Codex. Thanks @xz-dev.

* fix(embeddings): inject NVIDIA NIM input_type for asymmetric embed models (diegosouzapw#4341)

NVIDIA NIM asymmetric embedding models (e.g. nvidia/nv-embedqa-e5-v5) reject
requests without an `input_type` ("query" | "passage") with 400 "'input_type'
parameter is required". The embedding registry now carries a model-level
default param for the asymmetric NVIDIA model, and the embeddings handler
injects a model's default params into the upstream body only when the client
omitted them, leaving a client-supplied value untouched.

Reported-by: hydraromania (decolua/9router#1378)

Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>

* fix(api): migrate deprecated Codex [features].codex_hooks to [features].hooks (diegosouzapw#4342)

Codex renamed the `codex_hooks` feature flag to `hooks`; recent Codex CLI
versions ignore the old key and warn. When OmniRoute rewrites an existing
config.toml (configure/reset Codex provider) it now renames
[features].codex_hooks -> [features].hooks, preserving the value and never
clobbering an already-present `hooks`, then drops the deprecated key. The
migration is a no-op when the flag is absent and runs on both the POST and
DELETE config paths.

Reported-by: Bian-Sh (decolua/9router#1327)

Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>

* fix(translator): drop the null flush on the same-format response path (diegosouzapw#4344)

The streaming response translator's same-format fast path returned
`[chunk]` unconditionally, so the end-of-stream null/flush signal
(chunk === null) propagated as a literal `[null]`. Downstream this surfaced
as an empty `data: null` SSE event between chunks and crashed strict clients
(e.g. Factory Droid BYOK on /v1/responses). The fast path now returns `[]`
for the null flush while still passing real chunks through unchanged.

Reported-by: thaitryhand (decolua/9router#1052)

Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>

* fix(translator): strip assistant echo fields on the OpenAI target path (Mistral 422) (diegosouzapw#4350)

Strict OpenAI-compatible upstreams (e.g. mistral/codestral-latest) reject
client-only assistant echo fields sent back as input with 422
extra_forbidden (the report hit messages[].assistant.reasoning_content via
Codex /responses). Only reasoning_content was stripped on the OpenAI target
path; the sibling fields reasoning / refusal / annotations / cache_control
leaked through. They are now all dropped on the non-reasoner OpenAI target
path. `audio` is intentionally preserved (OpenAI audio models reference a
prior assistant audio response by id; Mistral never emits audio).

Reported-by: xxy9468615 (decolua/9router#1649)

Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (diegosouzapw#4343)

* fix(cli): honor TAILSCALE_AUTHKEY for non-interactive tailscale login (port from 9router#1263)

startTailscaleLogin built `tailscale up` without ever reading
process.env.TAILSCALE_AUTHKEY, so a pre-authenticated / headless daemon
waited for an interactive auth URL and timed out (~15s). When
TAILSCALE_AUTHKEY is set it is now passed via `--auth-key=` (an argv element
to spawn(binary, args) — no shell interpolation, Hard Rule diegosouzapw#13); when unset,
behavior is unchanged. The arg builder is extracted into a pure exported
`tailscaleUpArgs()` for testing.

Reported-by: ipeterpetrus (decolua/9router#1263)
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* chore(quality): rebaseline tailscaleTunnel.ts file-size to 1202 (diegosouzapw#1263 +13)

---------

Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>

* fix(dashboard): OAuth modal surfaces the real error on a non-JSON response (diegosouzapw#4351)

* fix(dashboard): OAuth modal surfaces real error on non-JSON responses (port from 9router#1318)

The OAuth connect/reauth modal called `await res.json()` unconditionally, so
a non-JSON error response (e.g. a plain-text 500 page from a build/OAuth
endpoint) threw `Unexpected token 'I'...` and hid the real failure. New
shared helpers parseResponseBody / getErrorMessage (src/shared/utils/api.ts)
read the body safely (JSON when JSON, raw text otherwise) and produce a clean
message either way; every modal fetch site now uses them.

Reported-by: DNNYF (decolua/9router#1318)
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(dashboard): type OAuth modal response body as Record<string, unknown> (t11 any-budget)

Switch the parseResponseBody casts from Record<string, any> to
Record<string, unknown> so OAuthModal.tsx stays within its t11 explicit-any
budget. getErrorMessage already takes unknown; the success paths typecheck
clean under strict:false. No runtime change.

---------

Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>

* fix(translator): accept AI SDK-style { type: image, image: data-URL } content parts (diegosouzapw#4345)

* fix(translator): accept AI SDK-style { type: image, image: "data:..." } parts (port from 9router#1330)

Several OpenAI-input translators only recognized images shaped as
`image_url.url` (or an object with `.source`/`.url`), so an AI SDK-style
content part where `image` is a bare data-URL STRING was silently dropped
before reaching a vision provider (OpenCode is one affected client; the gap
is generic). The OpenAI->Claude, OpenAI->Kiro and OpenAI->Gemini/Antigravity
translators now parse a string `image` data URL into each provider's native
image shape (Claude base64 source, Kiro images[].source.bytes, Gemini
inlineData).

Reported-by: mugnimaestra (decolua/9router#1330)
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* chore(quality): freeze openai-to-kiro.ts file-size at 807 (diegosouzapw#1330 +9, over 800 cap)

---------

Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>

* fix(dashboard): show a disabled connection's last error in the row (diegosouzapw#4352)

* fix(dashboard): show a disabled connection's last error in the row (port from 9router#1447)

The provider card's error badge counts a disabled connection (isActive ===
false) that has an error — its effective status is still
error/expired/unavailable — but the connection row hid the lastError text for
disabled rows, so the operator saw the count without the cause. The row's
error-visibility decision is extracted into shouldShowConnectionLastError()
and now shows the error whenever there is one, regardless of the active
toggle.

Reported-by: ntdung6868 (decolua/9router#1447)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline ConnectionRow.tsx file-size to 942 (diegosouzapw#1447 +1 import)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): bound the OAuth connection-test probe with a timeout (diegosouzapw#4347)

* fix(providers): bound OAuth connection-test probe with a timeout (port from 9router#1449)

The OAuth path of "Test Connection One-by-One" called bare fetch() with no
AbortController/signal, so a provider probe that accepted the socket but never
responded wedged the test queue forever. Both the initial probe and the
post-refresh retry are now bounded with AbortSignal.timeout(30s) — matching the
API-key path's existing budget — and a timed-out probe resolves as a failure
with a clear "Test timed out after 30s" message in the route's normal error shape.

Reported-by: ntdung6868 (decolua/9router#1449)
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* chore(quality): rebaseline providers test route file-size to 887 (diegosouzapw#1449 + sibling diegosouzapw#1444 growth)

---------

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(providers): label a deactivated account distinctly from a revoked token (diegosouzapw#4353)

A Codex connection whose OAuth refresh is fully healthy but whose ChatGPT
account has been deactivated by the provider gets a 401 from the upstream
API. The connection test labeled that the same as a bad credential
("Token invalid or revoked" -> upstream_auth_error), so an operator could not
tell a deactivated account from a revoked token. The test now reads the
401/403 body and, when it indicates account deactivation, classifies it as
account_deactivated (which the dashboard already renders as "Account
Deactivated"); a plain auth 401 is unchanged.

Reported-by: ntdung6868 (decolua/9router#1444)

Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>

* fix(db): cascade-delete orphaned model aliases when a provider is removed (diegosouzapw#4348)

* fix(db): cascade-delete orphaned model aliases when a provider is removed (port from 9router#1409)

Deleting a custom provider removed its connections and node but left the
imported model-alias rows (key=<alias>, value="<providerId>/<model>") behind,
so re-importing the same provider was blocked by stale "already exists" aliases.
Add a deleteModelAliasesForProvider(providerId) DB helper that drops every alias
whose stored value begins with "<providerId>/", and call it from the provider-node
DELETE handler so a fresh import is unblocked.

Reported-by: nguyenvanhuy0612 (decolua/9router#1409)
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* chore(quality): rebaseline models.ts file-size to 1221 (diegosouzapw#1409 + sibling diegosouzapw#1294 growth)

---------

Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>

* fix(api): persist max_input_tokens/max_output_tokens when adding a custom model (diegosouzapw#4349)

The POST /api/provider-models handler read the rest of the body but never
the two token-limit fields, and addCustomModel() had no parameter for them,
so the form values were dropped on write while the DB layer and /v1/models
catalog already round-trip inputTokenLimit/outputTokenLimit. Accept the two
optional limits in the schema, forward them through the handler, and persist
them in addCustomModel(). TDD: failing-then-passing unit test.

Reported-by: codename-zen (decolua/9router#1294)

Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>

* docs: feature-documentation catch-up (v3.8.20 → v3.8.30) (diegosouzapw#4391)

One-time reconciliation of the docs with every user-facing feature shipped since
v3.8.20 (we had never done a dedicated pass, so debt had accumulated):

- README: new '✨ What's New' section (curated v3.8.20→v3.8.30 highlights).
- New guides: CLI-INTEGRATIONS (all setup-*/launch commands), MITM-TPROXY-DECRYPT
  (transparent-decrypt epic), CONTEXT_EDITING (delegated Anthropic clear_tool_uses).
- Refreshed: AUTO-COMBO (auto/<category>:<tier> + Arena-ELO), API_REFERENCE
  (x-omniroute-no-memory), MEMORY (int8 quantization + off-by-default), RESILIENCE
  (model-lockout success-decay), RTK, AGENTBRIDGE, TRAFFIC_INSPECTOR, GUARDRAILS,
  CLOUD_AGENT, ENVIRONMENT, SETUP_GUIDE, CLI-TOOLS, MCP-SERVER.
- Regenerated PROVIDER_REFERENCE (231 providers); synced the count in README/CLAUDE/AGENTS.
- Allowlisted external-tool env vars (OPENAI_API_BASE, PROMPTFOO_PROVIDER_KEY) and the
  STREAM_RECOVERY config-object name in the docs-accuracy gates.

All claims source-verified; check:docs-all (sync/counts/env/links/fabricated) passes.
Going forward this runs every release via generate-release step 6b.

* fix(executors): don't inject thinking when tool_choice forces a tool (native Claude) (diegosouzapw#4389)

Forced tool_choice now strips the adaptive thinking injection to avoid Anthropic 400. Thanks @NomenAK.

* fix(translator): Gemini accepts HTTP/HTTPS image URLs (port from 9router#344) (diegosouzapw#4373)

OpenAI-style `image_url` parts with an `http://` or `https://` URL reached
`convertOpenAIContentToParts` and were dropped with only a `console.warn`,
because Gemini's `inlineData` requires base64 (the helper is synchronous and
cannot fetch+encode upstream assets). Gemini's `Part` schema, however, natively
accepts `fileData: { fileUri }` for remote URIs — the model fetches the asset
itself.

The helper now emits a `fileData` part (`mimeType: "image/*"`, inferred upstream
on fetch) for HTTP/HTTPS URLs instead of silently dropping them. Vision requests
that pass a URL — not a data: URI — now reach Gemini intact.

No behavioral change for:
- `data:` URIs → still emitted as `inlineData` with the parsed media type.
- Unsupported schemes (e.g. `ftp:`) → still skipped (Gemini would reject them).

The openai-to-claude side already passed HTTP/HTTPS URLs through as
`source: { type: "url", url }` (lines 573–578) — the upstream PR's Claude-side
change was already covered.

Regression test: tests/unit/gemini-helper-http-image-url-port344.test.ts
(4 cases: https URL, http URL, data: URI no-regression, unsupported-scheme guard).


Inspired-by: decolua/9router#344

Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>

* fix(executors): strip stream_options for qwen non-streaming / thinking Claude Code requests (port from 9router#663) (diegosouzapw#4374)

Claude-Code-compatible providers force the executor-level `stream` flag on
via `upstreamStream = stream || isClaudeCodeCompatible`
(open-sse/handlers/chatCore.ts), but the outgoing body keeps the caller's
original `stream: false`. The shared `stream && targetFormat === "openai"`
branch in DefaultExecutor.transformRequest then injected
`stream_options: { include_usage: true }` onto a body that still said
`stream: false`, and qwen upstream rejected the request with
`400 "'stream_options' only set this when you set stream: true"`. The same
rejection surfaced when the body carried `thinking` / `enable_thinking`.

The qwen branch now skips the injection (and strips any client-sent
`stream_options`) when the body explicitly says `stream: false` or
requests thinking, leaving regular qwen streaming requests with the
include_usage injection intact. Other providers are unaffected.

Adds a TDD regression with 4 cases covering both opt-out paths and the
normal-streaming positive control.


Inspired-by: decolua/9router#663

Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>

* fix(security): scope OAuth callback postMessage to a trusted-origin allowlist (port from 9router#998) (diegosouzapw#4372)

The OAuth callback at `/callback` previously fell back to
`window.opener.postMessage({ code, state, ... }, "*")` whenever the opener
was cross-origin. The fallback was intended to support remote-OmniRoute +
local-loopback callbacks (where opener and callback live on different
origins), but the same code path also delivers the OAuth code/state to any
hostile opener that pops the well-known callback URL — letting that
attacker complete the OAuth flow as the user.

Replace the wildcard fallback with iteration over a fixed allowlist:
`window.location.origin` (same-origin parent — the popup-mode dashboard)
plus Codex's fixed loopback helper (`http://localhost:1455` and the IPv4
literal `http://127.0.0.1:1455`). The browser drops `postMessage` to any
opener whose actual origin is not in `targetOrigin`, so the message reaches
only known parents and is silently dropped for any other. The same-origin
fallback path is unchanged — methods 2 (`BroadcastChannel`) and 3
(`localStorage` storage event) still cover same-origin openers that COOP
severed.

The `openerSameOrigin` probe stays in place to drive the auto-close vs
manual-copy UI decision (no behavior change for the success path).

Adds a regression test (`tests/unit/ui/oauth-callback-postmessage-scope.test.tsx`)
that mounts the page with a stubbed cross-origin opener and asserts no
`postMessage` call ever uses `"*"` and every call lands on an allowlisted
origin. The test failed against the pre-fix code (red), passes after the
fix (green) — TDD per CLAUDE.md hard rule diegosouzapw#18.

Partial port of upstream decolua/9router#998: the upstream PR also
re-enabled TLS verification on a DNS-bypass fetch in `open-sse/utils/proxyFetch.js`;
that part is N/A here because OmniRoute's `proxyFetch.ts` never disabled
TLS verification (no `rejectUnauthorized: false` anywhere in the file).


Inspired-by: decolua/9router#998

Co-authored-by: aeonframework <aeon@aeonframework.dev>

* fix(sse): default combo per-target timeout to 120s for fast failover (diegosouzapw#4365)

Combo per-target timeout inherited the full FETCH_TIMEOUT_MS (600s) when a
combo did not set its own targetTimeoutMs, so a single hung/slow target stalled
the whole combo for up to 10 minutes before falling through to the next model.

Introduce DEFAULT_COMBO_TARGET_TIMEOUT_MS (120s) as the unset-default in
resolveComboTargetTimeoutMs (new 3rd arg) and wire it in phaseComboSetup. The
upstream ceiling (600s) and per-combo opt-out (targetTimeoutMs, up to the
ceiling) are preserved; single non-combo requests are unchanged. For streaming
requests this only bounds time-to-first-headers, so token generation is not cut
short.

TDD: failing-then-passing unit test in tests/unit/combo-config.test.ts.

* refactor(combo): de-dup exhausted-target skip predicate across both dispatchers (diegosouzapw#4362)

Primeiro incremento da de-dup dos 2 dispatchers de combo (handleComboChat +
handleRoundRobinCombo). O bloco de pre-check diegosouzapw#1731/#1731v2 (skip de target já
exhausted no provider/connection) era BYTE-IDÊNTICO nos dois (mesmas condições,
mesmas mensagens), diferindo só na tag de log e no control-flow.

- comboPredicates.ts: getExhaustedTargetSkipReason(target, exhaustedProviders,
  exhaustedConnections) — predicate PURO que retorna a mensagem de skip (ou null);
  cada dispatcher mantém seu próprio log-tag + control-flow (return null / continue)
  + fallbackCount. No mutate do stryker (cobertura de mutação).
- combo.ts: −20 linhas (os 2 blocos viram 1 chamada cada).
- 7 testes de caracterização travam condições + strings exatas.

Comportamento preservado: 376/376 testes combo (357 caracterização + 7 novos),
integração sse-correctness 5/5, typecheck 0, complexity neutro (1895), file-size
encolhe. Próximo incremento: de-dup do error-handling/exhausted-tracking (handleTargetError).

* refactor(combo): de-dup upstream-error exhaustion classification across both dispatchers (diegosouzapw#4366)

Segundo incremento da de-dup dos 2 dispatchers (handleTargetError). Após cada erro
de target, ambos rodavam um bloco quase-idêntico que marca o provider exhausted
(diegosouzapw#1731), a conexão connection-errored (#1731v2) ou o provider transiently rate-limited.

- combo/targetExhaustion.ts: applyComboTargetExhaustion(target, opts) — atualiza os
  3 Sets de exhaustion e retorna providerExhausted. As MUTAÇÕES de Set (que dirigem o
  skip de targets, lidas por getExhaustedTargetSkipReason) são BYTE-IDÊNTICAS nos dois;
  as diferenças reais viram parâmetros: tag, allAccountsRateLimited (termo extra do RR,
  false no handleComboChat), exhaustedLogLevel (info no handleComboChat, debug no RR).
  Connection-level extraído p/ markConnectionLevelExhaustion (privado, <15 complexity).
- combo.ts: −73 linhas; 4 imports órfãos removidos.
- 7 testes de caracterização travam as mutações + o return.

ÚNICA mudança de comportamento: o WORDING das mensagens de log do RR ganha o sufixo
'on remaining targets' (cosmético; mesmo #code, mesmas mutações, mesmos níveis de log).
376/376 combo (caracterização preservada), integração sse 5/5, typecheck 0, complexity
neutro (1895), file-size encolhe.

* refactor(chatCore): extract checkHeapPressureGuard leaf (god-file decomposition start) (diegosouzapw#4371)

Primeiro incremento da decomposição do chatCore.ts (5127 LOC, hot-path mais quente).
O guard de memória do topo do handleChatCore (rejeita 503 quando o heap V8 passa o
threshold de shed) vira um leaf testável, co-locado com o threshold em heapPressure.ts.

- heapPressure.ts: checkHeapPressureGuard(heapUsedMb, thresholdMb) — retorna o result
  503 pronto ou null. Byte-idêntico ao guard inline (mesmo check, mesma 503, mesmo warn).
  A figura de heap fica em telemetria INTERNA, nunca no response do cliente (Hard Rule diegosouzapw#12).
- chatCore.ts: o bloco inline (~22 ln) vira 3 linhas; import órfão de HEAP_PRESSURE_THRESHOLD_MB
  trocado por checkHeapPressureGuard.
- 3 testes novos (incl. assert Rule diegosouzapw#12: o MB medido não vaza no payload).

complexity-baseline 1895->1896: drift de base pós-diegosouzapw#4338 (medido com minhas mudanças
stashed = 1896); esta mudança é complexity-NEUTRA (helper complexity 2, handleChatCore só
perde código). 190/190 chatcore tests, typecheck 0, file-size encolhe.

* Localize CLI and stabilize fetch, memory, and coverage handling (diegosouzapw#4383)

en-only i18n, fetch-start-timeout hardening, EngineConfigPage icon fix, CI build-artifact-reuse overhaul. Memory production hunk dropped as a no-op (tests kept). Thanks @JxnLexn.

* test(combo): reset circuit breakers between stream-readiness cases (restore green) (diegosouzapw#4396)

The combo-dispatch cases in combo-stream-readiness-fallback.test.ts deliberately
fail `glm` (zombie streams / repeated 504s), which legitimately trips the
per-provider circuit breaker. That OPEN state is a module-level singleton, so it
leaked into the next test and combo.ts then SKIPPED `glm/*` targets entirely
("Skipping … circuit breaker OPEN"). That made "combo does not retry stream
readiness timeouts on the same model" never attempt glm/zombie — expected
['glm/zombie','openai/gpt-5.4-mini'] but got ['openai/gpt-5.4-mini'].

This was a pre-existing red on release/v3.8.31 (present at the cycle-open tip),
order-dependent: the test passes in isolation, fails after the preceding cases.
Add a test.beforeEach(resetAllCircuitBreakers) so each scenario starts from a
clean breaker slate. Test-isolation only — the breaker behavior is correct and
no production code or assertion changes. Full combo suite: 390/390 green.

* fix(cli): decline confirm() cleanly on non-interactive stdin + document contexts workflow

The `contexts remove` command already has `--yes` to skip confirmation, but when
run without it under a non-interactive stdin (pipe, CI, EOF) the [y/N] prompt could
never be answered — the readline question stayed pending and Node warned about an
"unsettled top-level await" at exit. confirm() now detects `!process.stdin.isTTY`
and declines cleanly (returns false), pointing at `--yes` for non-interactive use.
Exported confirm() for a regression test.

Docs: REMOTE-MODE.md gains a full "Managing contexts" section (list/current/use to
switch between remote and local, add/show/rename, remove with --yes, export/import),
fixes a `context current` -> `contexts current` typo, and the README remote-mode
snippet now shows switching back to local. Verified against the live CLI: command
signatures, --yes, and the non-TTY decline path all behave as documented.

Tests: cli-contexts.test.ts asserts confirm() declines on non-TTY stdin (RED before,
GREEN after). All docs gates (fabricated/links/symbols) pass.

* ci(t11): bump any-budget for executors/base.ts (2 false-positive "any" strings)

Unblocks the Fast Quality Gates on release/v3.8.31: `check:any-budget:t11` was red on
`open-sse/executors/base.ts` for ALL PRs (pre-existing base drift, unrelated to this
branch). The checker counts `\bany\b` after stripping comments but NOT strings, and the
native-Claude tool_choice logic uses the API value `"any"` in two string literals
(`tb.tool_choice === "any"`, `.type === "any"`). There are zero actual TypeScript
`any` types in the file — budget set to the matched count, mirroring the existing
cursor.ts false-positive entry right below it.

* perf: combos UI split + next config + 1-click redis + bifrost sidecar (diegosouzapw#3932) (diegosouzapw#4381)

Combos UI split + next.config perf + 1-click local Redis launcher + bifrost relay. Review fixes (co-author): --rm/--restart conflict, error sanitization, UI/route names, dead-guard re-doc, bifrost Zod, IPv6 + CLI test fixes. Thanks @KooshaPari.

* fix(plugin): prefix OC static-catalog combo+raw keys with providerId (diegosouzapw#4384)

OC parses model ids on '/'; combo keys now carry 'omniroute/' (was 'combo/'). Live-validated against the VPS via OpenCode. Thanks @herjarsa.

* docs(env): document TAILSCALE_AUTHKEY (env/docs contract drift on .31)

Second pre-existing base-drift fix needed to get Fast Quality Gates green: the
`repository contract is in sync` test (check:env-doc-sync) was red on ALL .31 PRs.
PR diegosouzapw#4343 added a code reference to `process.env.TAILSCALE_AUTHKEY`
(src/lib/tailscaleTunnel.ts) for non-interactive `tailscale up`, but never added the
var to .env.example / docs/reference/ENVIRONMENT.md — the contract requires code vars
to appear in both. Add the (commented) entry to .env.example next to TAILSCALE_BIN and
a row to ENVIRONMENT.md. Verified: `node scripts/check/check-env-doc-sync.mjs` → in sync.

Unrelated to this branch's confirm()/docs change; surfaced because touching a quality
script triggers the TIA fail-safe full unit suite.

* chore(release): v3.8.31 — 2026-06-20

Finalize the v3.8.31 release: reconcile the CHANGELOG (full commit-to-bullet
coverage, 26 bullets across Features/Fixed/Security/Maintenance), refresh the
README What's New section, back-fill the .30/.31 sections into the 41 i18n
CHANGELOG mirrors, and add TAILSCALE_AUTHKEY to the env contract
(.env.example + ENVIRONMENT.md).

* chore(release): align mitm-hosts test comment with main to clear merge conflict

The same CodeQL false-positive fix landed twice — diegosouzapw#4386 on release/v3.8.31 and
diegosouzapw#4387 directly on main — with only the explanatory comment differing (the
assertion is byte-identical). Adopt main's comment wording on the release branch
so the release PR merges without a comment-only conflict.

* test(translator): align stale openai->gemini remote-URL tests with diegosouzapw#4373

Third pre-existing base-drift fix to get Fast Quality Gates green on .31. PR diegosouzapw#4373
('Gemini accepts HTTP/HTTPS image URLs', port of 9router#344) intentionally changed
convertOpenAIContentToParts so remote http(s) image URLs pass through as a native
`fileData: { fileUri }` part instead of the old diegosouzapw#2807 drop+warn — and added its own
test (gemini-helper-http-image-url-port344.test.ts) for the new behavior. But it left
three tests in translator-openai-to-gemini.test.ts asserting the OLD drop+warn
contract, so they fail deterministically (verified: the file fails in isolation on
clean .31). These only surface under the TIA fail-safe FULL suite, which a quality-
script touch triggers.

Align the three stale tests to the real, intended behavior (captured by running the
function): remote URLs -> fileData.fileUri (mimeType image/*), still never inlineData
(the sync path cannot fetch+encode). This is test-vs-code alignment to a deliberate,
separately-tested change — not a weakened assertion. 40/40 in the file; 94/94 across
the translator + env-doc combo that previously failed.

* chore(quality): reconcile complexity baseline 1896->1900 (/review-prs v3.8.31 batch) (diegosouzapw#4410)

* fix(release): reconcile full-CI drift for v3.8.31 (gemini tests diegosouzapw#4373, any-budget diegosouzapw#4389, masking allowlist diegosouzapw#4384)

The release PR's full CI surfaced cumulative cycle drift the per-PR fast gates
skip:
- tests/unit/translator-openai-to-gemini.test.ts: realign 3 cases to diegosouzapw#4373's
  HTTP/HTTPS-URL fileData pass-through (they asserted the old warn-and-drop).
- scripts/check/check-t11-any-budget.mjs: base.ts budget 0→2 — diegosouzapw#4389 compares
  tool_choice against the string literal "any" (not a TS any type).
- config/quality/test-masking-allowlist.json: allowlist diegosouzapw#4384's opencode combos
  net-assert reduction (obsolete combo/ namespace removed).
No production behavior change.

* chore(release): re-trigger full CI for v3.8.31 finalization

Force a fresh pull_request CI run on the head carrying the cycle-drift fixes
(gemini diegosouzapw#4373 tests, any-budget diegosouzapw#4389, masking allowlist diegosouzapw#4384) — the prior
synchronize event did not spawn a ci.yml run.

* chore: re-trigger CI (no Actions runs registered for prior push)

---------

Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: hydraromania <252583922+hydraromania@users.noreply.github.com>
Co-authored-by: Bian-Sh <24520547+Bian-Sh@users.noreply.github.com>
Co-authored-by: thaitryhand <248103256+thaitryhand@users.noreply.github.com>
Co-authored-by: xxy9468615 <63351664+xxy9468615@users.noreply.github.com>
Co-authored-by: ipeterpetrus <93033698+ipeterpetrus@users.noreply.github.com>
Co-authored-by: DNNYF <74033321+DNNYF@users.noreply.github.com>
Co-authored-by: mugnimaestra <13349159+mugnimaestra@users.noreply.github.com>
Co-authored-by: ntdung6868 <103993527+ntdung6868@users.noreply.github.com>
Co-authored-by: nguyenvanhuy0612 <57367674+nguyenvanhuy0612@users.noreply.github.com>
Co-authored-by: codename-zen <263238141+codename-zen@users.noreply.github.com>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: Ibrahim Ryan <ryan@nuevanext.com>
Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
Co-authored-by: aeonframework <aeon@aeonframework.dev>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants