Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1619,3 +1619,48 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# OpenCode-style API key (sk-...) for the regenerated opencode.json. Used by:
# scripts/ad-hoc/regen-opencode-config.ts. Falls back to OMNIROUTE_KEY.
# OPENCODE_API_KEY=

# ─── Bifrost Go sidecar (PR-4 in #3932) ──────────────────────────────────────
# When BIFROST_BASE_URL is set, /api/v1/relay/chat/completions/bifrost routes
# traffic to the Go gateway instead of the TS relay handler, removing TS from
# the hot path. Auth/rate-limit/injection-guard stay in the route (security not
# duplicated). Falls back to TS path via X-Bifrost-Fallback header on
# timeout/failure. See bin/omniroute for the local-redis companion.
# BIFROST_BASE_URL=
# API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
# unset, the route expects the request to carry a valid OmniRoute API key;
# this key is for gateway-side auth only.
# BIFROST_API_KEY=
# When true, the Bifrost sidecar route streams responses back via SSE through
# the gateway rather than the TS streaming executor. Default: true (when
# BIFROST_BASE_URL is set).
# BIFROST_STREAMING_ENABLED=
# Per-request timeout when proxying to the Bifrost gateway. Default: 30000 (30s).
# BIFROST_TIMEOUT_MS=
# Alias for BIFROST_API_KEY (used by scripts that read the env via
# OMNIROUTE_*). Falls back to BIFROST_API_KEY when unset.
# OMNIROUTE_BIFROST_KEY=

# ─── 1-click local service launchers (PR-3 in #3932) ────────────────────────
# Master switch for /api/local/* routes. When unset or "0", all /api/local/*
# routes return 503 in production. Default: 0. Must be "1" in non-loopback
# deploys to enable the Redis launcher and similar 1-click local service
# starters. Belt-and-suspenders with the isLocalOnlyPath() route-guard
# classification (LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts).
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=
# Bearer token for /api/local/* callers that aren't on loopback (e.g. the
# desktop app). When set, requests from non-loopback IPs must carry
# Authorization: Bearer <token>. Required when
# OMNIROUTE_LOCAL_ENDPOINTS_ENABLED=1 in non-loopback deployments. Default:
# unset (loopback-only).
# OMNIROUTE_LOCAL_ENDPOINTS_TOKEN=
# Container name for the 1-click Redis launcher (`omniroute redis up`).
# Default: omniroute-redis. Used by bin/cli/commands/redis.mjs and the
# RedisLauncherPanel.
# OMNIROUTE_REDIS_CONTAINER_NAME=
# Host port for the 1-click Redis launcher. Default: 6379. Bump if the host
# already binds 6379. The container's internal port stays 6379.
# OMNIROUTE_REDIS_HOST_PORT=
# Redis image used by the 1-click Redis launcher. Default: redis:7-alpine.
# Override to redis:8-alpine or a private registry mirror as needed.
# OMNIROUTE_REDIS_IMAGE=
294 changes: 294 additions & 0 deletions bin/cli/commands/redis.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
import { spawn } from "node:child_process";
import { promisify } from "node:util";
import { execFile as execFileCb } from "node:child_process";
Comment on lines +1 to +3

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";


import { t } from "../i18n.mjs";

const execFile = promisify(execFileCb);

const DEFAULT_IMAGE = "docker.io/redis:7-alpine";
const DEFAULT_NAME = "omniroute-redis";
const DEFAULT_PORT = "6379";
const DEFAULT_VOLUME = "omniroute-redis-data";

const RUNTIME_PREFERENCE = ["podman", "docker"];

async function detectRuntime() {
for (const candidate of RUNTIME_PREFERENCE) {
try {
await execFile(candidate, ["--version"], { timeout: 3000 });
return candidate;
} catch {
// try next candidate
}
}
return null;
}

async function containerExists(runtime, name) {
try {
const { stdout } = await execFile(runtime, ["ps", "-a", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
}
}

async function containerRunning(runtime, name) {
try {
const { stdout } = await execFile(runtime, ["ps", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
}
}

async function pingRedis(port) {
// Minimal TCP probe via /dev/tcp — works in bash/zsh but Node has no
// native equivalent, so spawn a short-lived `redis-cli` if available,
// otherwise fall back to a raw socket connect.
return new Promise((resolve) => {
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);
});
});
Comment on lines +51 to +66

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);
    });

});
}

function colorize(text, code) {
if (process.stdout.isTTY === false) return text;
return `\x1b[${code}m${text}\x1b[0m`;
}

function info(msg) {
console.log(colorize("•", "36") + " " + msg);
}

function success(msg) {
console.log(colorize("✓", "32") + " " + msg);
}

function warn(msg) {
console.error(colorize("!", "33") + " " + msg);
}

function fail(msg) {
console.error(colorize("✗", "31") + " " + msg);
}

export function registerRedis(program) {
const redis = program
.command("redis")
.description(
t("redis.description") ||
"Launch a 1-click local Redis container (Podman or Docker) for OmniRoute caching and quota tracking"
);

redis
.command("up")
.description("Start the local Redis container")
.option("-p, --port <port>", "Host port to expose", DEFAULT_PORT)
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("-i, --image <image>", "Container image", DEFAULT_IMAGE)
.option("--no-pull", "Skip pulling the image if it is missing")
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.option("--password <password>", "Set a Redis password (AUTH)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisUpCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});

redis
.command("down")
.description("Stop and remove the local Redis container")
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("--keep-data", "Keep the named volume for next start")
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisDownCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});

redis
.command("status")
.description("Show status of the local Redis container")
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("-p, --port <port>", "Host port", DEFAULT_PORT)
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}

async function pickRuntime(forced) {
if (forced) {
try {
await execFile(forced, ["--version"], { timeout: 3000 });
return forced;
} catch (err) {
fail(`Forced runtime '${forced}' not available: ${err.message}`);
return null;
}
}
const detected = await detectRuntime();
if (!detected) {
fail("Neither podman nor docker found on PATH. Install one or pass --runtime.");
return null;
}
return detected;
}

export async function runRedisUpCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;

const name = opts.name || DEFAULT_NAME;
const port = opts.port || DEFAULT_PORT;
const image = opts.image || DEFAULT_IMAGE;

const exists = await containerExists(runtime, name);
const running = exists && (await containerRunning(runtime, name));

if (running) {
success(`Container '${name}' is already running on port ${port}.`);
return 0;
}

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;
}
}
}
Comment on lines +173 to +203

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;
      }
    }
  }


const args = [
"run",
"-d",
"--name", name,
"--restart", "unless-stopped",
"-p", `${port}:6379`,
"-v", `${DEFAULT_VOLUME}:/data`,
];
if (opts.password) {
args.push("-e", `REDIS_PASSWORD=${opts.password}`);
}
args.push(image, "redis-server", "--appendonly", "yes");
if (opts.password) args.push("--requirepass", opts.password);

info(`Launching ${runtime} run ${args.join(" ")}`);
try {
await execFile(runtime, args);
success(`Container '${name}' is now running on redis://127.0.0.1:${port}`);
info(`Set OMNIROUTE_REDIS_URL=redis://127.0.0.1:${port} in your .env to wire OmniRoute to it.`);
return 0;
} catch (err) {
fail(`Failed to launch container: ${err.message}`);
return 1;
}
}

export async function runRedisDownCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;

const name = opts.name || DEFAULT_NAME;

if (!(await containerExists(runtime, name))) {
info(`Container '${name}' does not exist — nothing to do.`);
return 0;
}

try {
await execFile(runtime, ["rm", "-f", name]);
success(`Removed container '${name}'.`);
} catch (err) {
fail(`Failed to remove container: ${err.message}`);
return 1;
}

if (!opts.keepData) {
try {
await execFile(runtime, ["volume", "rm", DEFAULT_VOLUME]);
success(`Removed volume '${DEFAULT_VOLUME}'.`);
} catch (err) {
warn(`Could not remove volume '${DEFAULT_VOLUME}': ${err.message}`);
}
}
return 0;
}

export async function runRedisStatusCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;

const name = opts.name || DEFAULT_NAME;
const port = opts.port || DEFAULT_PORT;

const exists = await containerExists(runtime, name);
if (!exists) {
console.log(JSON.stringify({ runtime, name, port, exists: false, running: false, reachable: false }, null, 2));
return 0;
}

const running = await containerRunning(runtime, name);
const reachable = running ? await pingRedis(port) : false;

if (opts.json || opts.output === "json") {
console.log(JSON.stringify({ runtime, name, port, exists, running, reachable }, null, 2));
return 0;
}

console.log(`\n\x1b[1m\x1b[36mRedis (${runtime})\x1b[0m\n`);
console.log(` Container: ${name}`);
console.log(` Exists: ${exists ? "yes" : "no"}`);
console.log(` Running: ${running ? "yes" : "no"}`);
console.log(` Reachable: ${reachable ? "yes" : "no"} (port ${port})`);
if (running && !reachable) {
warn("Container is running but the port is not reachable. Is REDIS_PASSWORD set or another process bound?");
}
if (!running) {
info(`Run 'omniroute redis up' to launch it.`);
}
return 0;
}
2 changes: 2 additions & 0 deletions bin/cli/commands/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { registerBackup, registerRestore } from "./backup.mjs";
import { registerHealth } from "./health.mjs";
import { registerQuota } from "./quota.mjs";
import { registerCache } from "./cache.mjs";
import { registerRedis } from "./redis.mjs";
import { registerMcp } from "./mcp.mjs";
import { registerA2a } from "./a2a.mjs";
import { registerTunnel } from "./tunnel.mjs";
Expand Down Expand Up @@ -126,6 +127,7 @@ export function registerCommands(program) {
registerHealth(program);
registerQuota(program);
registerCache(program);
registerRedis(program);
registerMcp(program);
registerA2a(program);
registerTunnel(program);
Expand Down
3 changes: 3 additions & 0 deletions bin/cli/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"base_url": "عنوان URL الأساسي لخادم OmniRoute",
"context": "سياق/ملف تعريف الخادم المستخدم في هذا الأمر",
"lang": "تعيين لغة عرض CLI (يتجاوز OMNIROUTE_LANG)"
},
"redis": {
"description": "تشغيل حاوية Redis محلية بضغطة واحدة (Podman أو Docker) لتخزين التخزين المؤقت وتتبع الحصة في OmniRoute"
}
}
3 changes: 3 additions & 0 deletions bin/cli/locales/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"base_url": "OmniRoute server baza URL-i",
"context": "Bu əmr üçün server konteksti/profili",
"lang": "CLI ekran dilini təyin edin (OMNIROUTE_LANG-ı keçir)"
},
"redis": {
"description": "OmniRoute keşfi və kvota izləmə üçün 1 kliklə yerli Redis konteynerini (Podman və ya Docker) işə salın"
}
}
3 changes: 3 additions & 0 deletions bin/cli/locales/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"base_url": "Базов URL на сървъра OmniRoute",
"context": "Контекст/профил на сървъра за тази команда",
"lang": "Задай език на CLI (замества OMNIROUTE_LANG)"
},
"redis": {
"description": "Стартиране на локален Redis контейнер с едно щракване (Podman или Docker) за кеширане и проследяване на квота в OmniRoute"
}
}
6 changes: 5 additions & 1 deletion bin/cli/locales/bn.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"redis": {
"description": "OmniRoute ক্যাশিং এবং কোটা ট্র্যাকিংয়ের জন্য এক ক্লিকে স্থানীয় Redis কন্টেইনার (Podman বা Docker) চালু করুন"
}
}
Loading
Loading