mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
Feature/batch allow big (#3128)
Podman deployment options + larger upload body-size limits (+CONTAINER_HOST docs). Integrated into release/v3.8.10.
This commit is contained in:
38
.env.example
38
.env.example
@@ -6,7 +6,6 @@
|
||||
# │ Reference: docs/ENVIRONMENT.md for full details and usage scenarios. │
|
||||
# └─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 1. REQUIRED SECRETS — Must be set before first run!
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -62,7 +61,6 @@ DISABLE_SQLITE_AUTO_BACKUP=false
|
||||
# Default: redis://localhost:6379 (or redis://redis:6379 in Docker)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 3. NETWORK & PORTS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -159,6 +157,11 @@ OMNIROUTE_USE_TURBOPACK=1
|
||||
# Values: production | development | Default: production
|
||||
NODE_ENV=production
|
||||
|
||||
# Container runtime — controls startup script behavior (permissions, advice).
|
||||
# Values: docker | podman | Default: docker
|
||||
# Set to "podman" when running under rootless Podman so the entrypoint
|
||||
# gives the correct fix instructions (podman unshare chown vs sudo chown).
|
||||
CONTAINER_HOST=docker
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 4. SECURITY & AUTHENTICATION
|
||||
@@ -238,7 +241,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
|
||||
# OUTBOUND_SSRF_GUARD_ENABLED=true
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -271,7 +273,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# PII_RESPONSE_SANITIZATION=false
|
||||
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 6. TOOL & ROUTING POLICIES
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -291,7 +292,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# Default: 5000 | Minimum: 1000
|
||||
# OMNIROUTE_PAYLOAD_RULES_RELOAD_MS=5000
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 7. URLS & CLOUD SYNC
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -365,7 +365,6 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 8. OUTBOUND PROXY (Upstream Provider Calls)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -393,7 +392,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# Used by: open-sse/services/claudeTurnstileSolver.ts
|
||||
# OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS=false
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 9. CLI TOOL INTEGRATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -423,7 +421,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_QODER_BIN=qoder
|
||||
# CLI_QWEN_BIN=qwen
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 10. INTERNAL AGENT & MCP INTEGRATIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -554,7 +551,6 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
|
||||
# Default: ~/.gemini/antigravity-cli/antigravity-oauth-token
|
||||
#AGY_TOKEN_FILE=
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 11. OAUTH PROVIDER CREDENTIALS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -701,7 +697,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# CLI_USER_ID= # legacy alias for OMNIROUTE_USER_ID
|
||||
# SERVER_URL= # legacy alias for OMNIROUTE_SERVER
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 12. PROVIDER USER-AGENT OVERRIDES
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -737,7 +732,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
|
||||
# CODEX_CLIENT_VERSION=0.132.0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -766,7 +760,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
#KIMI_CLI_VERSION=1.36.0
|
||||
#KIMI_CODING_DEVICE_ID=
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 14. API KEY PROVIDERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -789,7 +782,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers
|
||||
# also work for embeddings.
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 15. TIMEOUT SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -868,7 +860,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
||||
# Default: 30000 (30 seconds)
|
||||
# SHUTDOWN_TIMEOUT_MS=30000
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 16. LOGGING
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -942,7 +933,6 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 100000
|
||||
# PROXY_LOGS_TABLE_MAX_ROWS=100000
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1022,7 +1012,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/utils/usageTracking.ts
|
||||
# USAGE_TOKEN_BUFFER=100
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 18. PRICING SYNC
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1038,7 +1027,6 @@ APP_LOG_TO_FILE=true
|
||||
# Comma-separated data sources. Default: litellm
|
||||
# PRICING_SYNC_SOURCES=litellm
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 19. MODEL SYNC (Dev)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1047,7 +1035,6 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 86400 (24 hours)
|
||||
# MODELS_DEV_SYNC_INTERVAL=86400
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 20. PROVIDER-SPECIFIC SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1128,7 +1115,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
|
||||
# LOCAL_HOSTNAMES=omlx,mlx-audio
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 21. PROXY HEALTH
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1154,7 +1140,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
|
||||
# HEALTHCHECK_STAGGER_MS=3000
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 22. DEBUGGING
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1214,7 +1199,6 @@ APP_LOG_TO_FILE=true
|
||||
# Enable E2E test mode — relaxes auth and enables test harness hooks.
|
||||
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 23. GITHUB INTEGRATION (Issue Reporting)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1232,7 +1216,6 @@ APP_LOG_TO_FILE=true
|
||||
# GITHUB_ISSUES_TOKEN when unset.
|
||||
# GITHUB_TOKEN=
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1337,7 +1320,6 @@ APP_LOG_TO_FILE=true
|
||||
# SKILLS_SANDBOX_NETWORK_ENABLED=0
|
||||
# SKILLS_ALLOWED_SANDBOX_IMAGES=
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 25. TEST & E2E
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1448,8 +1430,8 @@ INSPECTOR_MASK_SECRETS=true
|
||||
INSPECTOR_LLM_HOSTS_EXTRA=
|
||||
INSPECTOR_INTERNAL_INGEST_TOKEN=
|
||||
# Quota Sharing (Group B — planos 16+22)
|
||||
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
|
||||
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
|
||||
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
|
||||
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
|
||||
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
|
||||
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
|
||||
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
|
||||
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
|
||||
|
||||
17
README.md
17
README.md
@@ -540,6 +540,22 @@ devbox run npm run dev
|
||||
|
||||
📖 [Docker Guide](docs/guides/DOCKER_GUIDE.md) — Compose profiles, Caddy HTTPS, Cloudflare tunnels.
|
||||
|
||||
**🦭 Podman**
|
||||
|
||||
```bash
|
||||
# 1. Build the image
|
||||
podman build --target runner-base -t omniroute:base .
|
||||
|
||||
# 2. Fix data directory permissions for rootless Podman
|
||||
mkdir -p data && podman unshare chown 1000:1000 ./data
|
||||
|
||||
# 3. Set runtime in .env, then run (see contrib/podman/ for Quadlet)
|
||||
echo "CONTAINER_HOST=podman" >> .env
|
||||
podman compose --profile base up -d
|
||||
```
|
||||
|
||||
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
|
||||
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
@@ -779,6 +795,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
|
||||
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
|
||||
| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
|
||||
| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
|
||||
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
|
||||
|
||||
128
contrib/podman/README.md
Normal file
128
contrib/podman/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Podman Deployment
|
||||
|
||||
Run OmniRoute with Podman via **Quadlet** (systemd integration) or **podman compose**.
|
||||
|
||||
---
|
||||
|
||||
## Option A: Quadlet (recommended)
|
||||
|
||||
### 1. Build the image
|
||||
|
||||
```bash
|
||||
cd /path/to/omniroute
|
||||
podman build --target runner-base -t omniroute:base .
|
||||
# For web-cookie providers (gemini-web, claude-web, claude-turnstile):
|
||||
podman build --target runner-web -t omniroute:web .
|
||||
# For CLI tool support:
|
||||
podman build --target runner-cli -t omniroute:cli .
|
||||
```
|
||||
|
||||
### 2. Copy Quadlet files to the systemd directory
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/containers/systemd/omniroute
|
||||
cp contrib/podman/*.container ~/.config/containers/systemd/omniroute/
|
||||
cp contrib/podman/*.network ~/.config/containers/systemd/omniroute/
|
||||
cp contrib/podman/*.volume ~/.config/containers/systemd/omniroute/
|
||||
```
|
||||
|
||||
### 3. Mount the project .env for secrets
|
||||
|
||||
Edit `~/.config/containers/systemd/omniroute/omniroute.container` and
|
||||
uncomment/replace the `EnvironmentFile` line with the absolute path to
|
||||
your project `.env`:
|
||||
|
||||
```
|
||||
EnvironmentFile=/home/USER/code/docker/OmniRoute/.env
|
||||
```
|
||||
|
||||
Make sure `CONTAINER_HOST=podman` is set in that `.env`.
|
||||
|
||||
Alternatively, edit the env vars directly in the `.container` file.
|
||||
|
||||
### 4. Reload systemd and start
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start omniroute-redis
|
||||
systemctl --user start omniroute
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
|
||||
```bash
|
||||
systemctl --user status omniroute
|
||||
curl http://localhost:20128/v1/models
|
||||
```
|
||||
|
||||
To follow logs:
|
||||
|
||||
```bash
|
||||
journalctl --user -u omniroute -f
|
||||
```
|
||||
|
||||
### 6. Enable on boot
|
||||
|
||||
```bash
|
||||
systemctl --user enable omniroute-redis
|
||||
systemctl --user enable omniroute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option B: podman compose
|
||||
|
||||
The project's `docker-compose.yml` now works with both Docker and Podman.
|
||||
Just set `CONTAINER_HOST=podman` in `.env` before starting.
|
||||
|
||||
### 1. Prepare the data directory
|
||||
|
||||
Rootless Podman maps container UIDs into a subordinate range. The
|
||||
`node` user (UID 1000) inside the container maps to a different UID
|
||||
on the host, so it cannot write to `./data` owned by your host user.
|
||||
|
||||
Fix the ownership **before** starting:
|
||||
|
||||
```bash
|
||||
mkdir -p data
|
||||
podman unshare chown 1000:1000 ./data
|
||||
```
|
||||
|
||||
### 2. Set the runtime in `.env`
|
||||
|
||||
Make sure `.env` contains:
|
||||
|
||||
```env
|
||||
CONTAINER_HOST=podman
|
||||
```
|
||||
|
||||
### 3. Start
|
||||
|
||||
```bash
|
||||
podman compose --profile base up -d
|
||||
```
|
||||
|
||||
### Profiles
|
||||
|
||||
Same profiles as `docker compose`:
|
||||
|
||||
| Profile | Command |
|
||||
| ------------------------------ | ---------------------------------------------------- |
|
||||
| `base` (no CLIs) | `podman compose --profile base up -d` |
|
||||
| `web` (+Chromium/Playwright) | `podman compose --profile web up -d` |
|
||||
| `cli` (+CLI tools) | `podman compose --profile cli up -d` |
|
||||
| `host` (host-mounted binaries) | `podman compose --profile host up -d` |
|
||||
| `cliproxyapi` (sidecar) | `podman compose --profile cliproxyapi up -d` |
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
The `docker-compose.yml` uses fully-qualified image names
|
||||
(`docker.io/library/redis:7-alpine`) and flat variable expansions so it
|
||||
works with both Docker and Podman without a separate compose file.
|
||||
|
||||
The entrypoint script (`check-permissions.sh`) reads `CONTAINER_HOST`
|
||||
from `.env` to give the correct fix instructions:
|
||||
- **docker**: `sudo chown -R ... ./data`
|
||||
- **podman**: `podman unshare chown 1000:1000 ./data`
|
||||
26
contrib/podman/omniroute-redis.container
Normal file
26
contrib/podman/omniroute-redis.container
Normal file
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=OmniRoute Redis (rate-limiter backend)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Container]
|
||||
Image=docker.io/redis:7-alpine
|
||||
ContainerName=omniroute-redis
|
||||
|
||||
Network=omniroute.network
|
||||
NetworkAlias=redis
|
||||
|
||||
Volume=omniroute-redis.volume:/data
|
||||
|
||||
Exec=redis-server --save 60 1 --loglevel warning
|
||||
|
||||
HealthCmd=/bin/sh -c "redis-cli ping | grep -q PONG"
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthTimeout=5s
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
3
contrib/podman/omniroute-redis.volume
Normal file
3
contrib/podman/omniroute-redis.volume
Normal file
@@ -0,0 +1,3 @@
|
||||
# Persistent Redis data
|
||||
[Volume]
|
||||
Label=omniroute
|
||||
50
contrib/podman/omniroute.container
Normal file
50
contrib/podman/omniroute.container
Normal file
@@ -0,0 +1,50 @@
|
||||
[Unit]
|
||||
Description=OmniRoute — Unified AI Proxy
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Requires=omniroute-redis.service
|
||||
After=omniroute-redis.service
|
||||
|
||||
[Container]
|
||||
# Use this image if your building locally
|
||||
Image=localhost/omniroute:base
|
||||
# Use this image if you are pulling from Docker Hub
|
||||
# Image=diegosouzapw/omniroute:base
|
||||
|
||||
ContainerName=omniroute
|
||||
|
||||
Network=omniroute.network
|
||||
PublishPort=20128:20128
|
||||
PublishPort=20129:20129
|
||||
|
||||
Volume=omniroute.volume:/app/data:Z
|
||||
|
||||
# Use a bind mount if you need direct host access to the data:
|
||||
# Volume=/home/USER/code/docker/OmniRoute/data:/app/data:Z
|
||||
|
||||
Environment=DATA_DIR=/app/data
|
||||
Environment=PORT=20128
|
||||
Environment=DASHBOARD_PORT=20128
|
||||
Environment=API_PORT=20129
|
||||
Environment=API_HOST=0.0.0.0
|
||||
Environment=REDIS_URL=redis://redis:6379
|
||||
Environment=JWT_SECRET=change-me-to-a-random-base64-string
|
||||
Environment=API_KEY_SECRET=change-me-to-a-random-base64-string
|
||||
Environment=INITIAL_PASSWORD=change-me-to-a-random-hex-string
|
||||
Environment=NODE_ENV=production
|
||||
Environment=REQUIRE_API_KEY=true
|
||||
|
||||
# Load additional secrets (API keys, OAuth creds) from the project .env:
|
||||
# EnvironmentFile=%h/code/docker/OmniRoute/.env
|
||||
|
||||
HealthCmd=node /app/healthcheck.mjs
|
||||
HealthInterval=30s
|
||||
HealthRetries=3
|
||||
HealthTimeout=5s
|
||||
HealthStartPeriod=15s
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
2
contrib/podman/omniroute.network
Normal file
2
contrib/podman/omniroute.network
Normal file
@@ -0,0 +1,2 @@
|
||||
[Network]
|
||||
Label=omniroute
|
||||
3
contrib/podman/omniroute.volume
Normal file
3
contrib/podman/omniroute.volume
Normal file
@@ -0,0 +1,3 @@
|
||||
# OmniRoute persistent data (SQLite DB, logs, backups)
|
||||
[Volume]
|
||||
Label=omniroute
|
||||
@@ -27,7 +27,7 @@ x-common: &common
|
||||
environment:
|
||||
- DATA_DIR=/app/data # Must match the volume mount below
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
||||
@@ -43,7 +43,7 @@ x-common: &common
|
||||
services:
|
||||
# ── Redis (Rate Limiter Backend) ──────────────────────────────────
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
image: docker.io/library/redis:7-alpine
|
||||
container_name: omniroute-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
target: runner-base
|
||||
image: omniroute:base
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
profiles:
|
||||
- base
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
target: runner-web
|
||||
image: omniroute:web
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
profiles:
|
||||
- web
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
target: runner-cli
|
||||
image: omniroute:cli
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
@@ -116,12 +116,12 @@ services:
|
||||
target: runner-base
|
||||
image: omniroute:base
|
||||
ports:
|
||||
- "${DASHBOARD_PORT:-${PORT:-20128}}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||
environment:
|
||||
- DATA_DIR=/app/data
|
||||
- PORT=${PORT:-20128}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-${PORT:-20128}}
|
||||
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
||||
- API_PORT=${API_PORT:-20129}
|
||||
- API_HOST=${API_HOST:-0.0.0.0}
|
||||
- CLI_MODE=host
|
||||
|
||||
@@ -877,6 +877,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | Maximum SQLite backup files retained on disk. |
|
||||
| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. |
|
||||
| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. |
|
||||
| `CONTAINER_HOST` | `docker` | `scripts/check-permissions.sh` | Container runtime hint for the entrypoint permission check. Set to `podman` under rootless Podman so the fix instructions use `podman unshare chown` instead of `sudo chown`. |
|
||||
| `QUOTA_STORE_DRIVER` | `sqlite` | `src/lib/quota/storeFactory.ts` | Quota-share consumption store backend: `sqlite` (default) or `redis`. |
|
||||
| `QUOTA_STORE_REDIS_URL` | _(unset)_ | `src/lib/quota/storeFactory.ts` | Redis connection string used when `QUOTA_STORE_DRIVER=redis` (e.g. `redis://localhost:6379`). |
|
||||
| `QUOTA_SATURATION_THRESHOLD` | `0.5` | `src/lib/quota/enforce.ts` | Pool saturation ratio (0..1); at/above it the pool enters strict mode (no borrowing). |
|
||||
|
||||
@@ -104,6 +104,10 @@ const nextConfig = {
|
||||
serverActions: {
|
||||
bodySizeLimit: process.env.OMNIROUTE_SERVER_ACTIONS_BODY_LIMIT || "50mb",
|
||||
},
|
||||
// Next.js proxy (middleware) has a default 10MB body clone limit. File
|
||||
// uploads (OpenAI-compatible /v1/files) routinely exceed this. Match the
|
||||
// 512 MB server-side cap; tune via env if needed.
|
||||
proxyClientMaxBodySize: process.env.NEXT_PROXY_BODY_LIMIT || "512mb",
|
||||
},
|
||||
outputFileTracingRoot: projectRoot,
|
||||
outputFileTracingIncludes: {
|
||||
|
||||
@@ -8,11 +8,18 @@ if [ -n "$OMNIROUTE_MEMORY_MB" ]; then
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=${OMNIROUTE_MEMORY_MB}"
|
||||
fi
|
||||
|
||||
if [ -d "/app/data" ] && [ ! -w "/app/data" ]; then
|
||||
echo "WARNING: /app/data is not writable by the current user (UID $(id -u))."
|
||||
echo "Run this on the Docker host to fix:"
|
||||
echo " sudo chown -R $(id -u):$(id -g) /app/data"
|
||||
echo " chmod -R u+rwX ./data"
|
||||
DATA_PATH="${DATA_DIR:-/app/data}"
|
||||
if [ -d "$DATA_PATH" ] && [ ! -w "$DATA_PATH" ]; then
|
||||
echo "WARNING: $DATA_PATH is not writable by the current user (UID $(id -u))."
|
||||
if [ "${CONTAINER_HOST:-}" = "podman" ]; then
|
||||
echo "Rootless Podman maps container UIDs into a subordinate range."
|
||||
echo "Run this on the host to fix (using the host-side bind-mount path):"
|
||||
echo " podman unshare chown -R $(id -u):$(id -g) <host-data-dir>"
|
||||
else
|
||||
echo "Run this on the Docker host to fix (using the host-side bind-mount path):"
|
||||
echo " sudo chown -R $(id -u):$(id -g) <host-data-dir>"
|
||||
echo " chmod -R u+rwX <host-data-dir>"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -26,6 +26,9 @@ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024;
|
||||
/** Larger limit for audio transcription uploads: 100 MB */
|
||||
export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024;
|
||||
|
||||
/** Larger limit for file uploads: 500 MB */
|
||||
export const MAX_BODY_BYTES_FILE = 500 * 1024 * 1024;
|
||||
|
||||
/** Configured limit — reads from env or falls back to 10 MB */
|
||||
export const MAX_BODY_BYTES = parseRequestBodyLimitBytes(process.env.MAX_BODY_SIZE_BYTES);
|
||||
|
||||
@@ -34,6 +37,7 @@ type BodySizeRule = { prefix: string; limit: number };
|
||||
const ROUTE_LIMITS: BodySizeRule[] = [
|
||||
{ prefix: "/api/db-backups/import", limit: MAX_BODY_BYTES_IMPORT },
|
||||
{ prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO },
|
||||
{ prefix: "/api/v1/files", limit: MAX_BODY_BYTES_FILE },
|
||||
];
|
||||
|
||||
export function getDefaultRequestBodyLimitMb(): number {
|
||||
@@ -62,7 +66,7 @@ export function checkBodySize(request: Request, limit: number = MAX_BODY_BYTES):
|
||||
const contentLength = request.headers.get("content-length");
|
||||
|
||||
if (contentLength) {
|
||||
const bytes = parseInt(contentLength, 10);
|
||||
const bytes = Number.parseInt(contentLength, 10);
|
||||
if (!Number.isNaN(bytes) && bytes > limit) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
MAX_BODY_BYTES_AUDIO,
|
||||
MAX_BODY_BYTES_FILE,
|
||||
getBodySizeLimit,
|
||||
checkBodySize,
|
||||
} from "../../src/shared/middleware/bodySizeGuard.ts";
|
||||
@@ -40,3 +41,39 @@ test("checkBodySize reports the configured request limit in 413 responses", asyn
|
||||
assert.equal(body.error.code, "PAYLOAD_TOO_LARGE");
|
||||
assert.match(body.error.message, /100 MB/);
|
||||
});
|
||||
|
||||
test("/api/v1/files route has 512 MB dedicated limit floor", () => {
|
||||
const limit = getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 1 });
|
||||
assert.equal(limit, MAX_BODY_BYTES_FILE);
|
||||
});
|
||||
|
||||
test("/api/v1/files route guard allows 500 MB file upload", () => {
|
||||
const thirtyMb = 500 * 1024 * 1024;
|
||||
const request = new Request("http://localhost/api/v1/files", {
|
||||
method: "POST",
|
||||
headers: { "content-length": String(thirtyMb) },
|
||||
});
|
||||
assert.equal(checkBodySize(request, getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 10 })), null);
|
||||
});
|
||||
|
||||
test("/api/v1/files route guard rejects >512 MB file upload", async () => {
|
||||
const tooBig = 600 * 1024 * 1024;
|
||||
const request = new Request("http://localhost/api/v1/files", {
|
||||
method: "POST",
|
||||
headers: { "content-length": String(tooBig) },
|
||||
});
|
||||
const response = checkBodySize(request, getBodySizeLimit("/api/v1/files", { maxBodySizeMb: 10 }));
|
||||
assert.ok(response);
|
||||
assert.equal(response.status, 413);
|
||||
const body = await response.json();
|
||||
assert.equal(body.error.code, "PAYLOAD_TOO_LARGE");
|
||||
});
|
||||
|
||||
test("/api/v1/files route guard allows 15 MB (10 MB+ real-world scenario)", () => {
|
||||
const fifteenMb = 15 * 1024 * 1024;
|
||||
const request = new Request("http://localhost/api/v1/files", {
|
||||
method: "POST",
|
||||
headers: { "content-length": String(fifteenMb) },
|
||||
});
|
||||
assert.equal(checkBodySize(request, getBodySizeLimit("/api/v1/files")), null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user