diff --git a/.env.example b/.env.example index 0ed1391ba2..4abc1cbd02 100644 --- a/.env.example +++ b/.env.example @@ -26,9 +26,12 @@ SQLITE_CLEAN_LEGACY_FILES=true # Canonical/base port (keeps backward compatibility) PORT=20128 # Optional split ports: -# API_PORT=20128 +# API_PORT=20129 # API_HOST=0.0.0.0 # DASHBOARD_PORT=20128 +# Optional Docker production host publish ports: +# PROD_DASHBOARD_PORT=20130 +# PROD_API_PORT=20131 NODE_ENV=production INSTANCE_NAME=omniroute diff --git a/Dockerfile b/Dockerfile index ffb9c07364..6b4472cfe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,7 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs +COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs EXPOSE 20128 diff --git a/bin/omniroute.mjs b/bin/omniroute.mjs index a7e74a4dbd..7bf3ad527c 100755 --- a/bin/omniroute.mjs +++ b/bin/omniroute.mjs @@ -67,12 +67,12 @@ function parsePort(value, fallback) { let port = parsePort(process.env.PORT || "20128", 20128); const portIdx = args.indexOf("--port"); if (portIdx !== -1 && args[portIdx + 1]) { - const parsed = parseInt(args[portIdx + 1], 10); - if (isNaN(parsed)) { + const cliPort = parsePort(args[portIdx + 1], null); + if (cliPort === null) { console.error("\x1b[31m✖ Invalid port number\x1b[0m"); process.exit(1); } - port = parsed; + port = cliPort; } const apiPort = parsePort(process.env.API_PORT || String(port), port); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index f129888275..0b1a11fa4c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -22,11 +22,15 @@ services: env_file: .env environment: - NODE_ENV=production - - PORT=20128 + - PORT=${PORT:-20128} + - DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}} + - API_PORT=${API_PORT:-20129} + - API_HOST=${API_HOST:-0.0.0.0} - HOSTNAME=0.0.0.0 - DATA_DIR=/app/data ports: - - "20130:20128" + - "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}" + - "${PROD_API_PORT:-20131}:${API_PORT:-20129}" volumes: - omniroute-prod-data:/app/data healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index 40441ace65..d13768bc82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,10 @@ x-common: &common env_file: .env environment: - DATA_DIR=/app/data # Must match the volume mount below + - PORT=${PORT:-20128} + - DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}} + - API_PORT=${API_PORT:-20129} + - API_HOST=${API_HOST:-0.0.0.0} volumes: - omniroute-data:/app/data healthcheck: @@ -45,7 +49,8 @@ services: target: runner-base image: omniroute:base ports: - - "${PORT:-20128}:20128" + - "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}" + - "${API_PORT:-20129}:${API_PORT:-20129}" profiles: - base @@ -58,7 +63,8 @@ services: target: runner-cli image: omniroute:cli ports: - - "${PORT:-20128}:20128" + - "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}" + - "${API_PORT:-20129}:${API_PORT:-20129}" profiles: - cli @@ -71,8 +77,14 @@ services: target: runner-base image: omniroute:base ports: - - "${PORT:-20128}:20128" + - "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}" + - "${API_PORT:-20129}:${API_PORT:-20129}" environment: + - DATA_DIR=/app/data + - PORT=${PORT:-20128} + - DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}} + - API_PORT=${API_PORT:-20129} + - API_HOST=${API_HOST:-0.0.0.0} - CLI_MODE=host - CLI_EXTRA_PATHS=/host-local/bin:/host-node/bin - CLI_CONFIG_HOME=/host-home diff --git a/scripts/run-next.mjs b/scripts/run-next.mjs index 0214bed3b3..c56c8455f0 100644 --- a/scripts/run-next.mjs +++ b/scripts/run-next.mjs @@ -1,41 +1,22 @@ #!/usr/bin/env node -import { spawn } from "node:child_process"; - -function parsePort(value, fallback) { - const parsed = Number.parseInt(String(value), 10); - return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback; -} +import { + resolveRuntimePorts, + withRuntimePortEnv, + spawnWithForwardedSignals, +} from "./runtime-env.mjs"; const mode = process.argv[2] === "start" ? "start" : "dev"; -const basePort = parsePort(process.env.PORT || "20128", 20128); -const apiPort = parsePort(process.env.API_PORT || String(basePort), basePort); -const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(basePort), basePort); +const runtimePorts = resolveRuntimePorts(); +const { dashboardPort } = runtimePorts; const args = ["./node_modules/next/dist/bin/next", mode, "--port", String(dashboardPort)]; if (mode === "dev") { args.splice(2, 0, "--webpack"); } -const child = spawn(process.execPath, args, { +spawnWithForwardedSignals(process.execPath, args, { stdio: "inherit", - env: { - ...process.env, - OMNIROUTE_PORT: String(basePort), - PORT: String(dashboardPort), - DASHBOARD_PORT: String(dashboardPort), - API_PORT: String(apiPort), - }, + env: withRuntimePortEnv(process.env, runtimePorts), }); - -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - return; - } - process.exit(code ?? 0); -}); - -process.on("SIGINT", () => child.kill("SIGINT")); -process.on("SIGTERM", () => child.kill("SIGTERM")); diff --git a/scripts/run-standalone.mjs b/scripts/run-standalone.mjs index b48ccf3eb2..1510cdc83a 100644 --- a/scripts/run-standalone.mjs +++ b/scripts/run-standalone.mjs @@ -1,34 +1,14 @@ #!/usr/bin/env node -import { spawn } from "node:child_process"; +import { + resolveRuntimePorts, + withRuntimePortEnv, + spawnWithForwardedSignals, +} from "./runtime-env.mjs"; -function parsePort(value, fallback) { - const parsed = Number.parseInt(String(value), 10); - return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback; -} +const runtimePorts = resolveRuntimePorts(); -const basePort = parsePort(process.env.PORT || "20128", 20128); -const apiPort = parsePort(process.env.API_PORT || String(basePort), basePort); -const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(basePort), basePort); - -const child = spawn("node", ["server.js"], { +spawnWithForwardedSignals("node", ["server.js"], { stdio: "inherit", - env: { - ...process.env, - OMNIROUTE_PORT: String(basePort), - PORT: String(dashboardPort), - DASHBOARD_PORT: String(dashboardPort), - API_PORT: String(apiPort), - }, + env: withRuntimePortEnv(process.env, runtimePorts), }); - -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - return; - } - process.exit(code ?? 0); -}); - -process.on("SIGINT", () => child.kill("SIGINT")); -process.on("SIGTERM", () => child.kill("SIGTERM")); diff --git a/scripts/runtime-env.mjs b/scripts/runtime-env.mjs new file mode 100644 index 0000000000..489685d830 --- /dev/null +++ b/scripts/runtime-env.mjs @@ -0,0 +1,43 @@ +import { spawn } from "node:child_process"; + +export function parsePort(value, fallback) { + const parsed = Number.parseInt(String(value), 10); + return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback; +} + +export function resolveRuntimePorts() { + const basePort = parsePort(process.env.PORT || "20128", 20128); + const apiPort = parsePort(process.env.API_PORT || String(basePort), basePort); + const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(basePort), basePort); + + return { basePort, apiPort, dashboardPort }; +} + +export function withRuntimePortEnv(env, runtimePorts) { + const { basePort, apiPort, dashboardPort } = runtimePorts; + + return { + ...env, + OMNIROUTE_PORT: String(basePort), + PORT: String(dashboardPort), + DASHBOARD_PORT: String(dashboardPort), + API_PORT: String(apiPort), + }; +} + +export function spawnWithForwardedSignals(command, args, options = {}) { + const child = spawn(command, args, options); + + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); + }); + + process.on("SIGINT", () => child.kill("SIGINT")); + process.on("SIGTERM", () => child.kill("SIGTERM")); + + return child; +} diff --git a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts index fd52913c94..8b65f7b6a6 100644 --- a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts +++ b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import fs from "fs/promises"; import path from "path"; import os from "os"; +import { getRuntimePorts } from "@/lib/runtime/ports"; /** * POST /api/cli-tools/guide-settings/:toolId @@ -37,6 +38,7 @@ export async function POST(request, { params }) { * Merges with existing config if present. */ async function saveContinueConfig({ baseUrl, apiKey, model }) { + const { apiPort } = getRuntimePorts(); const configPath = path.join(os.homedir(), ".continue", "config.json"); const configDir = path.dirname(configPath); @@ -82,8 +84,8 @@ async function saveContinueConfig({ baseUrl, apiKey, model }) { (m.omnirouteManaged === true || normalizeApiBase(m.apiBase) === normalizedBaseUrl.toLowerCase() || normalizeApiBase(m.apiBase).includes("omniroute") || - normalizeApiBase(m.apiBase).includes("localhost:20128") || - normalizeApiBase(m.apiBase).includes("127.0.0.1:20128") || + normalizeApiBase(m.apiBase).includes(`localhost:${apiPort}`) || + normalizeApiBase(m.apiBase).includes(`127.0.0.1:${apiPort}`) || String(m.apiKey || "") .toLowerCase() .includes("sk_omniroute")) diff --git a/src/lib/apiBridgeServer.ts b/src/lib/apiBridgeServer.ts index 1271481b9c..61566a84ec 100644 --- a/src/lib/apiBridgeServer.ts +++ b/src/lib/apiBridgeServer.ts @@ -36,15 +36,21 @@ function proxyRequest(req: IncomingMessage, res: ServerResponse, dashboardPort: if (res.headersSent) return; res.writeHead(502, { "content-type": "application/json" }); res.end( - JSON.stringify({ error: "api_bridge_unavailable", detail: String(error.message || error) }) + JSON.stringify({ + error: "api_bridge_unavailable", + detail: String(error.message || error), + }) ); }); + req.on("aborted", () => { + targetReq.destroy(); + }); + req.pipe(targetReq); } declare global { - var __omnirouteApiBridgeStarted: boolean | undefined; } @@ -54,7 +60,7 @@ export function initApiBridgeServer(): void { const { apiPort, dashboardPort } = getRuntimePorts(); if (apiPort === dashboardPort) return; - const host = process.env.API_HOST || "0.0.0.0"; + const host = process.env.API_HOST || "127.0.0.1"; const server = http.createServer((req, res) => { const rawUrl = req.url || "/"; diff --git a/src/lib/oauth/config/index.ts b/src/lib/oauth/config/index.ts index d6ae85cc4b..7484e1baf4 100644 --- a/src/lib/oauth/config/index.ts +++ b/src/lib/oauth/config/index.ts @@ -5,6 +5,8 @@ * with the running OmniRoute server when saving tokens. */ +import { getRuntimePorts } from "@/lib/runtime/ports"; + interface ServerCredentials { server: string; token: string; @@ -12,11 +14,8 @@ interface ServerCredentials { } function getDefaultApiServer() { - const basePort = Number.parseInt(process.env.OMNIROUTE_PORT || process.env.PORT || "20128", 10); - const fallbackPort = Number.isFinite(basePort) ? basePort : 20128; - const apiPort = Number.parseInt(process.env.API_PORT || String(fallbackPort), 10); - const effectivePort = Number.isFinite(apiPort) ? apiPort : fallbackPort; - return `http://localhost:${effectivePort}`; + const { dashboardPort } = getRuntimePorts(); + return `http://localhost:${dashboardPort}`; } /**