mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 16:12:23 +03:00
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
374 lines
15 KiB
YAML
374 lines
15 KiB
YAML
# ──────────────────────────────────────────────────────────────────────
|
|
# OmniRoute — Docker Compose
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
#
|
|
# Profiles:
|
|
# base → minimal image, no CLI tools
|
|
# web → runner-web (+Chromium/Playwright) for web-cookie providers
|
|
# cli → CLIs installed inside the container (portable)
|
|
# host → runner-base + host-mounted CLI binaries (Linux-first)
|
|
# cliproxyapi → CLIProxyAPI sidecar on port 8317
|
|
# memory → Qdrant sidecar on port 6333 (semantic memory offload)
|
|
# bifrost → Bifrost Go sidecar on port 8080 (Tier-1 LLM router)
|
|
#
|
|
# Usage:
|
|
# docker compose --profile base up -d
|
|
# docker compose --profile web up -d # gemini-web / claude-web / claude-turnstile
|
|
# docker compose --profile cli up -d
|
|
# docker compose --profile host up -d
|
|
# docker compose --profile cliproxyapi up -d
|
|
# docker compose --profile cli --profile cliproxyapi up -d
|
|
# docker compose --profile base --profile memory up -d # adds Qdrant sidecar
|
|
# docker compose --profile base --profile bifrost up -d # adds Bifrost sidecar
|
|
#
|
|
# See docs/architecture/cluster-decisions.md for the per-component rationale
|
|
# (Qdrant=opt-in, Bifrost=opt-in, Caddy=upstream LB, no Dragonfly/NATS/PG/Neo4j/MinIO).
|
|
#
|
|
# Before first run, copy .env.example → .env and edit your secrets.
|
|
# ──────────────────────────────────────────────────────────────────────
|
|
|
|
x-common: &common
|
|
restart: unless-stopped
|
|
stop_grace_period: 40s
|
|
env_file: .env
|
|
environment:
|
|
- DATA_DIR=/app/data # Must match the volume mount below
|
|
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
|
- PORT=${PORT:-20128}
|
|
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
|
- API_PORT=${API_PORT:-20129}
|
|
- API_HOST=${API_HOST:-0.0.0.0}
|
|
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
|
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
|
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
|
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
- NODE_OPTIONS=--max-old-space-size=2048
|
|
# Codex App-Server transport (provider: codex-app-server). Inert unless the
|
|
# `codex-app-server` compose profile is up (the sidecar below). Points the app
|
|
# at the internal sidecar; the capability token is shared via the mounted file.
|
|
- OMNIROUTE_CODEX_APPSERVER_WS=${OMNIROUTE_CODEX_APPSERVER_WS:-ws://codex-app-server:1456}
|
|
- OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE=${OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE:-/run/codex-appserver/token}
|
|
volumes:
|
|
- ./data:/app/data
|
|
# Shared capability token + codex auth for the app-server WS. Only meaningful
|
|
# when the codex-app-server profile is active. The token dir carries the WS
|
|
# capability token; the codex home is where the dashboard "Apply auth" writes
|
|
# ~/.codex/auth.json (getCliConfigPaths("codex") = <home>/.codex; the base
|
|
# image runs as `node`, so /home/node/.codex) and the SAME volume is mounted
|
|
# into the sidecar so its `codex app-server` reads the same auth.
|
|
- codex-appserver-token:/run/codex-appserver
|
|
- codex-appserver-home:/home/node/.codex
|
|
healthcheck:
|
|
test: ["CMD", "node", "healthcheck.mjs"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
|
|
services:
|
|
# ── Redis (Rate Limiter Backend) ──────────────────────────────────
|
|
redis:
|
|
image: docker.io/library/redis:8.6.5-alpine
|
|
container_name: omniroute-redis
|
|
restart: unless-stopped
|
|
ports:
|
|
# Loopback-only by default: this Redis has no `requirepass`, and the app
|
|
# containers reach it over the compose network (redis:6379), so the
|
|
# published port exists purely for host-side tooling (redis-cli, a local
|
|
# `npm run dev`). A bare "6379:6379" binds 0.0.0.0 — that puts an
|
|
# unauthenticated Redis on every LAN interface. Override REDIS_BIND_HOST
|
|
# only together with a password (`--requirepass`).
|
|
- "${REDIS_BIND_HOST:-127.0.0.1}:${REDIS_PORT:-6379}:6379"
|
|
volumes:
|
|
- redis-data:/data
|
|
command: redis-server --save 60 1 --loglevel warning
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
|
|
# ── Profile: base (minimal, no CLI tools) ──────────────────────────
|
|
omniroute-base:
|
|
<<: *common
|
|
container_name: omniroute
|
|
build:
|
|
context: .
|
|
target: runner-base
|
|
args:
|
|
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
|
image: omniroute:base
|
|
ports:
|
|
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
|
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
|
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
|
profiles:
|
|
- base
|
|
|
|
# ── Profile: web (runner-web + Chromium/Playwright) ────────────────
|
|
# Required for web-cookie providers (gemini-web, claude-web, claude-turnstile).
|
|
# The default `base` image ships without Chromium, so those providers fail
|
|
# with "Executable doesn't exist at .../ms-playwright/chromium..." (#2832).
|
|
omniroute-web:
|
|
<<: *common
|
|
container_name: omniroute
|
|
build:
|
|
context: .
|
|
target: runner-web
|
|
args:
|
|
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
|
image: omniroute:web
|
|
depends_on:
|
|
chatgpt-web-codex-browser:
|
|
condition: service_started
|
|
environment:
|
|
- DATA_DIR=/app/data
|
|
- PORT=${PORT:-20128}
|
|
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
|
- API_PORT=${API_PORT:-20129}
|
|
- API_HOST=${API_HOST:-0.0.0.0}
|
|
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
|
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
|
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
|
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
|
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
|
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
|
ports:
|
|
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
|
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
|
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
|
profiles:
|
|
- web
|
|
|
|
# Internal-only Chromium runtime for ChatGPT Web (Codex). No CDP or browser
|
|
# UI port is published to the host.
|
|
chatgpt-web-codex-browser:
|
|
build:
|
|
context: .
|
|
dockerfile: docker/chatgpt-web-codex-browser/Dockerfile
|
|
image: omniroute:chatgpt-web-codex-browser
|
|
restart: unless-stopped
|
|
shm_size: "2gb"
|
|
volumes:
|
|
- chatgpt-web-codex-browser-data:/browser-profile
|
|
profiles:
|
|
- web
|
|
|
|
# ── Profile: cli (CLIs installed inside container) ─────────────────
|
|
omniroute-cli:
|
|
<<: *common
|
|
container_name: omniroute
|
|
build:
|
|
context: .
|
|
target: runner-cli
|
|
args:
|
|
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
|
image: omniroute:cli
|
|
ports:
|
|
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
|
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
|
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
|
volumes:
|
|
- ./data:/app/data
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
- /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro
|
|
- ${AUTO_UPDATE_HOST_REPO_DIR:-.}:/workspace/omniroute:rw
|
|
profiles:
|
|
- cli
|
|
|
|
# ── Profile: host (host-mounted CLI binaries, Linux-first) ────────
|
|
omniroute-host:
|
|
<<: *common
|
|
container_name: omniroute
|
|
build:
|
|
context: .
|
|
target: runner-base
|
|
args:
|
|
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
|
image: omniroute:base
|
|
ports:
|
|
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
|
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
|
- "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}"
|
|
environment:
|
|
- DATA_DIR=/app/data
|
|
- PORT=${PORT:-20128}
|
|
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
|
- API_PORT=${API_PORT:-20129}
|
|
- API_HOST=${API_HOST:-0.0.0.0}
|
|
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
|
|
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
|
|
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
|
|
- CLI_MODE=host
|
|
- CLI_EXTRA_PATHS=/host-local/bin:/host-node/bin
|
|
- CLI_CONFIG_HOME=/host-home
|
|
- CLI_ALLOW_CONFIG_WRITES=true
|
|
# Uncomment per-tool overrides as needed:
|
|
# - CLI_CURSOR_BIN=agent
|
|
# - CLI_CLINE_BIN=cline
|
|
# - CLI_CONTINUE_BIN=cn
|
|
volumes:
|
|
- ./data:/app/data
|
|
# ── Host binary mounts (read-only) ──
|
|
# Adjust paths below to match YOUR host system.
|
|
- ~/.local/bin:/host-local/bin:ro
|
|
# Node global binaries (adjust node version path)
|
|
# - ~/.nvm/versions/node/v24.14.1/bin:/host-node/bin:ro
|
|
# ── Host config mounts (read-write) ──
|
|
- ~/.codex:/host-home/.codex:rw
|
|
- ~/.claude:/host-home/.claude:rw
|
|
- ~/.factory:/host-home/.factory:rw
|
|
- ~/.openclaw:/host-home/.openclaw:rw
|
|
- ~/.cursor:/host-home/.cursor:rw
|
|
- ~/.config/cursor:/host-home/.config/cursor:rw
|
|
profiles:
|
|
- host
|
|
|
|
# ── Profile: memory (Qdrant semantic-memory sidecar) ─────────────
|
|
# Off by default. SQLite + sqlite-vec + FTS5 (RRF) is the primary vector
|
|
# store (see src/lib/memory/vectorStore.ts). Enable only when you need
|
|
# cross-instance memory sharing or >1M points — at which point the
|
|
# QDRANT_ENABLED=true flag activates the dual-write path in
|
|
# src/lib/memory/qdrant.ts:37. See docs/architecture/cluster-decisions.md
|
|
# for the workload analysis behind this profile.
|
|
qdrant:
|
|
image: docker.io/qdrant/qdrant:v1.12.4
|
|
container_name: omniroute-qdrant
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${QDRANT_PORT:-6333}:6333"
|
|
- "${QDRANT_GRPC_PORT:-6334}:6334"
|
|
volumes:
|
|
- qdrant-data:/qdrant/storage
|
|
environment:
|
|
- QDRANT__SERVICE__GRPC_PORT=6334
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:6333/readyz"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
profiles:
|
|
- memory
|
|
|
|
# ── Profile: bifrost (Bifrost Go LLM-router sidecar) ─────────────
|
|
# Off by default. Bifrost is the Tier-1 LLM router (per ADR-031 and
|
|
# open-sse/executors/bifrost.ts). When BIFROST_ENABLED=true is set in
|
|
# .env, OmniRoute delegates /v1/chat/completions, /v1/responses, and
|
|
# /v1/embeddings to this sidecar instead of handling them in chatCore.
|
|
# The kill switch is the BIFROST_ENABLED env var — flip to false to
|
|
# fall back to the chatCore path with zero code changes. See
|
|
# docs/architecture/cluster-decisions.md for the activation plan.
|
|
bifrost:
|
|
image: ghcr.io/maximhq/bifrost:v1.6.11
|
|
container_name: omniroute-bifrost
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${BIFROST_PORT:-8080}:8080"
|
|
volumes:
|
|
- bifrost-data:/data
|
|
environment:
|
|
- BIFROST_LOG_LEVEL=${BIFROST_LOG_LEVEL:-info}
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8080/v1/models"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 15s
|
|
profiles:
|
|
- bifrost
|
|
|
|
# ── Profile: cliproxyapi (CLIProxyAPI as sidecar) ─────────────────
|
|
# Official pre-built image lives on Docker Hub (eceasy/cli-proxy-api);
|
|
# ghcr.io/router-for-me/* is not publicly pullable. v6.9.7 is the pinned
|
|
# version the sidecar integration (port 8317, /v1/models healthcheck) targets.
|
|
cliproxyapi:
|
|
container_name: cliproxyapi
|
|
image: docker.io/eceasy/cli-proxy-api:v6.9.7
|
|
restart: unless-stopped
|
|
ports:
|
|
- "${CLIPROXYAPI_PORT:-8317}:${CLIPROXYAPI_PORT:-8317}"
|
|
volumes:
|
|
- cliproxyapi-data:/root/.cli-proxy-api
|
|
environment:
|
|
- PORT=${CLIPROXYAPI_PORT:-8317}
|
|
- HOST=0.0.0.0
|
|
healthcheck:
|
|
test:
|
|
["CMD", "wget", "--spider", "-q", "http://127.0.0.1:${CLIPROXYAPI_PORT:-8317}/v1/models"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
profiles:
|
|
- cliproxyapi
|
|
|
|
# ── Profile: codex-app-server (Codex CLI app-server sidecar) ──────────
|
|
# A PLAIN Codex app-server for the `codex-app-server` provider: OmniRoute drives
|
|
# the Codex CLI's own `codex app-server` over JSON-RPC/WebSocket instead of
|
|
# replaying a session token to the API. It listens ONLY on the internal compose
|
|
# network (ws://codex-app-server:1456), guarded by a capability token — it is
|
|
# NEVER published to the host / internet. The Codex CLI (baked into
|
|
# omniroute:base) self-manages its OpenAI OAuth via the shared ~/.codex volume,
|
|
# which the dashboard "Apply auth" (device-OAuth) writes and this sidecar reads.
|
|
#
|
|
# NOTE: this is the GENERIC public sidecar. An operator wanting residential /
|
|
# UDP egress (via a TUN sidecar) runs that separately as an override; it is
|
|
# intentionally not shipped here.
|
|
codex-app-server:
|
|
image: omniroute:base
|
|
container_name: omniroute-codex-app-server
|
|
restart: unless-stopped
|
|
# Generate the WS capability token on first boot if absent, then run the
|
|
# app-server. entrypoint is overridden because the base image's default is the
|
|
# Next.js server.
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command:
|
|
- |
|
|
set -e
|
|
TOKEN_FILE=/run/codex-appserver/token
|
|
mkdir -p /run/codex-appserver
|
|
if [ ! -s "$$TOKEN_FILE" ]; then
|
|
# 32-byte hex capability token; shared with the app via the token volume.
|
|
TF="$$TOKEN_FILE" node -e 'require("fs").writeFileSync(process.env.TF, require("crypto").randomBytes(32).toString("hex"))' 2>/dev/null || \
|
|
{ head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "$$TOKEN_FILE"; }
|
|
chmod 600 "$$TOKEN_FILE"
|
|
fi
|
|
exec codex app-server \
|
|
--listen ws://0.0.0.0:1456 \
|
|
--ws-auth capability-token \
|
|
--ws-token-file "$$TOKEN_FILE"
|
|
environment:
|
|
- CODEX_HOME=/home/node/.codex
|
|
- RUST_LOG=${CODEX_APPSERVER_RUST_LOG:-warn}
|
|
volumes:
|
|
- codex-appserver-token:/run/codex-appserver
|
|
- codex-appserver-home:/home/node/.codex
|
|
# No `ports:` — internal-only. Reached at ws://codex-app-server:1456 over the
|
|
# compose network by the omniroute app.
|
|
healthcheck:
|
|
test:
|
|
["CMD", "node", "-e", "require('http').get('http://127.0.0.1:1456/readyz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 10s
|
|
profiles:
|
|
- codex-app-server
|
|
|
|
volumes:
|
|
chatgpt-web-codex-browser-data:
|
|
name: omniroute-chatgpt-web-codex-browser-data
|
|
cliproxyapi-data:
|
|
name: cliproxyapi-data
|
|
redis-data:
|
|
name: omniroute-redis-data
|
|
qdrant-data:
|
|
name: omniroute-qdrant-data
|
|
bifrost-data:
|
|
name: omniroute-bifrost-data
|
|
codex-appserver-token:
|
|
name: omniroute-codex-appserver-token
|
|
codex-appserver-home:
|
|
name: omniroute-codex-appserver-home
|