ollamaMQ is a high-performance, asynchronous message queue dispatcher and load balancer designed to sit in front of one or more Ollama or LM Studio API instances. It acts as a smart proxy that queues incoming requests from multiple users and dispatches them in parallel to multiple backends using a fair-share round-robin scheduler with least-connections load balancing.
/api/* vs OpenAI /v1/*) and routes each request only to a compatible backend β even when a specific model is requested, so an Ollama-family call never reaches an LM Studio server that merely lists the same model./api/ps, LM Studio loaded instances; vLLM-style backends that don't report loaded state count as ready when they list the model), then to cold backends with nothing loaded, and only as a last resort to a backend that would have to evict a different loaded model β so a request for qwen3.8-27b goes to the server that has it in GPU memory instead of making a busy server with another model load it. Name matching is deterministic and bounded β exact, :tag/case-insensitive, or publisher-/quant-suffixed variants of the same id (qwen3.8-27b reaches unsloth/qwen3.8-27b@q8_0) β but never arbitrary substrings (so it will not be routed to an unrelated ...-abliterated build).:latest tags and case-insensitivity. For example, a request for llama3 will correctly match llama3:latest on the backend.L/U) or the admin HTTP API β without touching the backend servers themselves.ollamaMQ can process multiple requests simultaneously (one per available backend), significantly increasing throughput for multiple users./api/tags and /v1/models). Endpoints a backend rejects are remembered and skipped until re-checked, so incompatible backends don't spam warnings. Offline instances are temporarily skipped and marked in the TUI.503 after stuck_timeout seconds instead of hanging forever. Requests merely waiting for a busy or loading backend are unaffected.max_concurrent_requests caps how many in-flight requests one backend may serve for that model, bounded globally by settings.max_concurrent_per_backend (default 1 β the historical one-request-per-backend behavior; raise it to let a single backend handle several requests at once).X-User-ID header) has their own FIFO queue.X-User-ID) passed to and from the backend, ensuring compatibility with tools like Claude Code.tokio and axum for high concurrency.
Ensure you have Rust (2024 edition or later) and Ollama installed.
cargo install ollamaMQ
Clone the repository:
git clone https://github.com/Chleba/ollamaMQ.git
cd ollamaMQ
Build and install locally:
cargo install --path .
localhost:11434).docker compose up -d
First build the image from the local Dockerfile:
docker build -t chlebon/ollamamq .
Then run the container:
docker run -d \
--name ollamamq \
-p 11435:11435 \
--restart unless-stopped \
chlebon/ollamamq
Point your LLM clients to the ollamaMQ port (11435) and include the X-User-ID header.
GET /health (Internal health check)GET / (Backend Status)POST /api/generatePOST /api/chatPOST /api/embedPOST /api/embeddingsGET /api/tagsPOST /api/showPOST /api/createPOST /api/copyDELETE /api/deletePOST /api/pullPOST /api/pushGET/HEAD/POST /api/blobs/{digest}GET /api/psGET /api/versionPOST /v1/chat/completions (OpenAI Compatible)POST /v1/completions (OpenAI Compatible)POST /v1/embeddings (OpenAI Compatible)GET /v1/models (OpenAI Compatible)GET /v1/models/{model} (OpenAI Compatible)curl -X POST http://localhost:11435/api/chat \
-H "X-User-ID: developer-1" \
-d '{
"model": "qwen3.5:35b",
"messages": [{"role": "user", "content": "Explain quantum computing."}],
"stream": true
}'
Models can be loaded and unloaded on connected backends without touching the backend servers. Supported backends:
POST /api/generate with an empty prompt (the scheduler loads the model and returns immediately), unloading is the same call with keep_alive: 0.POST /api/v1/models/load / POST /api/v1/models/unload endpoints (unload targets a specific loaded instance). Older LM Studio versions are detected and a friendly error points at the lms load/lms unload CLI.All three endpoints are served by the proxy itself (they are never proxied to a backend) and are protected by the same optional OLLAMA_MQ_API_KEY auth as the proxy routes.
GET /admin/modelsPer-backend inventory: index, URL, online status, detected API type, LM Studio flag, active request count, available models, currently loaded models, and any in-flight control operation.
curl -s http://localhost:11435/admin/models | python3 -m json.tool
POST /admin/models/load and POST /admin/models/unloadBody:
{ "backend": 0, "model": "llama3", "num_ctx": 16384, "keep_alive": 3600, "identifier": "big-ctx" }
backend accepts:
--backend-urls),"any" β the proxy picks a suitable online, idle backend (for load: the first backend where the model resolves; for unload: the first backend that actually has it loaded).model is resolved against each backend's model list: exact match, then :latest/case-insensitive, then a unique substring (admin ops only). Ambiguous names are rejected β the proxy never guesses.
Optional fields (load only):
num_ctx β max context window, sent as Ollama options.num_ctx or LM Studio context_length (omitted = backend default).keep_alive β overrides --load-keep-alive (seconds) for this load. -1 = keep forever.identifier β free-form label shown with the operation in the TUI and API responses.Responses:
| Status | Meaning |
|---|---|
202 | Accepted β the operation started in the background (response body contains the canonical model name and the backend URL). |
400 | Bad request β empty model, backend offline, unsupported backend type, or unloading a model that is not loaded. |
404 | Unknown backend, model not found on the backend, or no suitable backend for "any". |
409 | Conflict β another control operation is already in flight on that backend, or the backend is busy with active requests. |
# Load (llama3 resolves to llama3:latest)
curl -s -X POST http://localhost:11435/admin/models/load \
-H "Content-Type: application/json" \
-d '{"backend": 0, "model": "llama3"}'
# Unload by URL (frees GPU memory on Ollama)
curl -s -X POST http://localhost:11435/admin/models/unload \
-H "Content-Type: application/json" \
-d '{"backend": "http://10.0.0.2:11434", "model": "qwen2.5:7b"}'
# Let the proxy pick a backend
curl -s -X POST http://localhost:11435/admin/models/load \
-H "Content-Type: application/json" \
-d '{"backend": "any", "model": "llama3:latest"}'
# With API-key auth enabled
curl -s http://localhost:11435/admin/models -H "Authorization: Bearer supersecret"
Note: Ollama keeps a model loaded for keep_alive seconds only (its own default is 5 minutes). A control "load" sends the --load-keep-alive value (default 24 h) so the model actually stays resident; an "unload" sends keep_alive: 0.
ollamaMQ uses tracing and behaves differently depending on the mode:
ollamamq.log file in the current working directory. This keeps the terminal clear for the dashboard. Default level is info.--no-tui mode (Docker/CI/service): Logs are written to stderr at a default level of debug, so you can see everything β backend health checks, per-request routing, backend detection, and errors. This is ideal for capturing output via the systemd journal, Docker, or piping to a file.Override the level at any time with the standard RUST_LOG environment variable:
# Most verbose (all debug detail)
RUST_LOG=debug ollamaMQ --no-tui
# Quieter (info + errors only)
RUST_LOG=info ollamaMQ --no-tui
To run ollamaMQ as a background service with full log visibility in the journal:
Copy the provided service file to your system's unit directory:
sudo cp ollamamq.service /etc/systemd/system/
Edit the file and set the correct ExecStart path to your installed binary (e.g. ~/.cargo/bin/ollamaMQ) and your backend URLs:
ExecStart=/usr/local/bin/ollamaMQ --no-tui --port 11435 --backend-urls http://localhost:11434
Note: If you installed
ollamaMQwithcargo install, the binary is at~/.cargo/bin/ollamaMQ, not/usr/local/bin/ollamaMQ. PointExecStartat the real path, or create a symlink so the default path works:# Find the real path which ollamaMQ # Option A: edit ExecStart to use the real path # Option B: symlink it to the expected location sudo ln -s "$HOME/.cargo/bin/ollamaMQ" /usr/local/bin/ollamaMQ
Reload systemd, then enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now ollamamq
Follow the logs in real time via the journal:
journalctl -u ollamamq -f
Or show the most recent 100 lines:
journalctl -u ollamamq -n 100
Because --no-tui defaults to debug logging on stderr, all dispatcher events (backend health, request routing, errors) are captured in the journal β no separate log file needed.
appconf.yaml)Everything about your ollamaMQ instance lives in one YAML file with three sections: backends (what to connect to), settings (runtime options) and models (which models to load where). The default path is appconf.yaml in the working directory; override it with -c/--model-config. A missing file is fine β defaults apply.
# --- Backends: every Ollama / LM Studio / OpenAI-compatible server you want to use
backends:
- http://10.137.1.1:11434 # e.g. Ollama
- http://10.137.1.2:1234 # e.g. LM Studio
# --- Runtime settings (all optional β defaults shown)
settings:
port: 11435 # proxy listen port (default 11435)
host: 127.0.0.1 # bind interface; use 0.0.0.0 for LAN/Docker access (default 127.0.0.1)
timeout: 300 # per-request timeout in seconds (default 300)
load_keep_alive: 86400 # how long model-control loads stay resident; -1 = forever (default 86400)
allow_all_routes: false # also proxy non-standard endpoints as fallback (default false)
stuck_timeout: 60 # fail-fast 503 after N s when no backend can ever serve a queued request (default 60)
max_concurrent_per_backend: 1 # global cap of in-flight requests per backend; raise to let one backend handle several at once (default 1)
# --- Models to load on startup / reload, via the model-control logic
models:
- name: "gpt-oss:120b" # model name as the backend knows it
identifier: "my-gpt" # label attached to this entry's load op (TUI / admin API)
max_ctx: 128000 # context window β Ollama num_ctx / LM Studio context_length
keep_alive: 86400 # seconds the model stays resident after load (Ollama; -1 = forever)
max_concurrent_requests: 3 # in-flight requests allowed for this model on one backend (default 1)
backends: # which backends to load it on (exact or substring URL match); omitted/empty = any suitable backend
- http://10.137.1.1:11434
How it's applied:
backends and settings are read at startup only β restart to change them.models is applied automatically at startup (once backend probes have run) and re-applied any time you press r in the TUI. Application is additive: each target endpoint is checked live first (Ollama /api/ps, LM Studio loaded instances), so models already resident there β e.g. still loaded from an earlier run because of long keep_alive β are skipped instead of being loaded twice; everything else gets a load started. It never unloads anything, and loads for the same backend run one at a time (backends reject parallel control ops). Every attempt, including skips, is reported in the TUI Logs panel (β³ CTL) and in the log output.--backend-urls over backends, --port/--host/--timeout over settings).See appconf.yaml.example for a fully commented template.
Every proxied request (IN) and upstream response (OUT) is written as JSON lines to settings.request_log_path (default ollamamq-requests.jsonl). Each line is one JSON object:
| field | meaning |
|---|---|
ts | unix time in milliseconds |
dir | IN (proxied request) or OUT (upstream response) |
user | client address |
model | model name, when resolvable (otherwise null) |
backend | upstream backend URL (may be null) |
method | HTTP method |
path | request path |
status | response status code (responses only) |
bytes | total body size in bytes |
content_type | body content type |
content | body preview, truncated to log_content_limit with a ...[truncated: N bytes total] marker |
Rotation: when the file exceeds request_log_max_bytes it is renamed to .0 (existing .0 β .1, etc.), at most request_log_max_files rotated files are kept and the oldest is deleted. The current file size is re-read at startup, so sizing/rotation survives restarts.
TUI: the bottom "Requests" panel shows these newest-first with a one-line content preview; press Enter on a row for the full-content detail view (j/k or PgUp/PgDn scroll, q/Esc/Enter close).
Note: request/response headers are deliberately not logged β this keeps API keys out of the file.
The included docker-compose.yml provides a ready-to-use configuration:
services:
ollamamq:
build: .
image: chlebon/ollamamq:latest
container_name: ollamamq
ports:
- "11435:11435"
environment:
# URLs of backend servers (Ollama, LM Studio, etc.)
- BACKEND_URLS=http://host.docker.internal:11434,http://host.docker.internal:1234
- PORT=11435
- TIMEOUT=300
- RUST_LOG=info
command: ["--no-tui"]
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:11435/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
Note for Linux Users: When running in Docker on Linux to access a host-based Ollama:
0.0.0.0. You can do this by setting export OLLAMA_HOST=0.0.0.0 before starting the Ollama service (or editing the systemd unit file).ufw) allows traffic from the Docker bridge (usually 172.17.0.1/16) to port 11434.extra_hosts setting in docker-compose.yml maps host.docker.internal to your host's IP address.The Dockerfile uses a multi-stage build:
rust:1.97-alpine (pinned) to compile the release binaryalpine:3.20 with only ca-certificates for a minimal footprint (~10MB)| Variable | Description | Default |
|---|---|---|
BACKEND_URLS | URLs of the backend servers (Ollama, LM Studio, β¦) | http://localhost:11434 |
OLLAMA_URLS | Legacy alias for BACKEND_URLS (used only when BACKEND_URLS is unset) | β |
PORT | Port for ollamaMQ to listen on | 11435 |
HOST | Host/interface to bind to | 0.0.0.0 |
TIMEOUT | Request timeout in seconds | 300 |
OLLAMA_MQ_API_KEY | Optional API key; enables auth when set | (unset β auth disabled) |
By default, ollamaMQ only listens on loopback (127.0.0.1), so it is only reachable from the local machine. If you expose it to the network with --host 0.0.0.0, set the OLLAMA_MQ_API_KEY environment variable to a secret key to protect the proxy. Auth is opt-in: if the variable is unset or empty, behavior is unchanged and no key is required.
When a key is set, every request except /health must present it via one of two headers:
Authorization: Bearer <key>X-API-Key: <key>Requests with a missing or wrong key get 401 Unauthorized ({"error":"unauthorized"}). The /health endpoint always stays unauthenticated so health checks and Docker healthchecks keep working.
# Enable auth
OLLAMA_MQ_API_KEY=supersecret ollamaMQ --no-tui
# Call the API with the key (either header works)
curl -X POST http://localhost:11435/api/chat \
-H "Authorization: Bearer supersecret" \
-H "X-User-ID: developer-1" \
-d '{"model": "llama3", "messages": [{"role": "user", "content": "Hi"}]}'
# ...or with X-API-Key
curl http://localhost:11435/api/tags -H "X-API-Key: supersecret"
# /health never requires a key
curl http://localhost:11435/health
In Docker, just pass the variable through β the binary reads it directly from the environment:
docker run -d --name ollamamq -p 11435:11435 \
-e OLLAMA_MQ_API_KEY=supersecret \
chlebon/ollamamq
To change the proxy port, use the PORT environment variable:
docker run -d \
--name ollamamq \
-p 8080:8080 \
-e PORT=8080 \
chlebon/ollamamq
To publish a new version of ollamaMQ to Docker Hub, follow these steps:
Update Version: Update the version number in Cargo.toml.
Build and Tag:
# Build the image for the current version
docker build -t chlebon/ollamamq:v0.2.4 .
# Tag it as latest
docker tag chlebon/ollamamq:v0.2.4 chlebon/ollamamq:latest
Push to Hub:
# Log in to Docker Hub (if not already logged in)
docker login
# Push the versioned tag
docker push chlebon/ollamamq:v0.2.4
# Push the latest tag
docker push chlebon/ollamamq:latest
You can use the provided test_dispatcher.sh script to simulate multiple users and verify the dispatcher's behavior under load:
./test_dispatcher.sh

This project is licensed under the MIT License - see the LICENSE file for details (if applicable).
Hacker News (1)
Rust
93.7%
Shell
4.0%
Python
1.4%
ollamaMQ is a high-performance, asynchronous message queue dispatcher and load balancer designed to sit in front of one or more Ollama or LM Studio API instances. It acts as a smart proxy that queues incoming requests from multiple users and dispatches them in parallel to multiple backends using a fair-share round-robin scheduler with least-connections load balancing.
/api/* vs OpenAI /v1/*) and routes each request only to a compatible backend β even when a specific model is requested, so an Ollama-family call never reaches an LM Studio server that merely lists the same model./api/ps, LM Studio loaded instances; vLLM-style backends that don't report loaded state count as ready when they list the model), then to cold backends with nothing loaded, and only as a last resort to a backend that would have to evict a different loaded model β so a request for qwen3.8-27b goes to the server that has it in GPU memory instead of making a busy server with another model load it. Name matching is deterministic and bounded β exact, :tag/case-insensitive, or publisher-/quant-suffixed variants of the same id (qwen3.8-27b reaches unsloth/qwen3.8-27b@q8_0) β but never arbitrary substrings (so it will not be routed to an unrelated ...-abliterated build).:latest tags and case-insensitivity. For example, a request for llama3 will correctly match llama3:latest on the backend.L/U) or the admin HTTP API β without touching the backend servers themselves.ollamaMQ can process multiple requests simultaneously (one per available backend), significantly increasing throughput for multiple users./api/tags and /v1/models). Endpoints a backend rejects are remembered and skipped until re-checked, so incompatible backends don't spam warnings. Offline instances are temporarily skipped and marked in the TUI.503 after stuck_timeout seconds instead of hanging forever. Requests merely waiting for a busy or loading backend are unaffected.max_concurrent_requests caps how many in-flight requests one backend may serve for that model, bounded globally by settings.max_concurrent_per_backend (default 1 β the historical one-request-per-backend behavior; raise it to let a single backend handle several requests at once).X-User-ID header) has their own FIFO queue.X-User-ID) passed to and from the backend, ensuring compatibility with tools like Claude Code.tokio and axum for high concurrency.
Ensure you have Rust (2024 edition or later) and Ollama installed.
cargo install ollamaMQ
Clone the repository:
git clone https://github.com/Chleba/ollamaMQ.git
cd ollamaMQ
Build and install locally:
cargo install --path .
localhost:11434).docker compose up -d
First build the image from the local Dockerfile:
docker build -t chlebon/ollamamq .
Then run the container:
docker run -d \
--name ollamamq \
-p 11435:11435 \
--restart unless-stopped \
chlebon/ollamamq
Point your LLM clients to the ollamaMQ port (11435) and include the X-User-ID header.
GET /health (Internal health check)GET / (Backend Status)POST /api/generatePOST /api/chatPOST /api/embedPOST /api/embeddingsGET /api/tagsPOST /api/showPOST /api/createPOST /api/copyDELETE /api/deletePOST /api/pullPOST /api/pushGET/HEAD/POST /api/blobs/{digest}GET /api/psGET /api/versionPOST /v1/chat/completions (OpenAI Compatible)POST /v1/completions (OpenAI Compatible)POST /v1/embeddings (OpenAI Compatible)GET /v1/models (OpenAI Compatible)GET /v1/models/{model} (OpenAI Compatible)curl -X POST http://localhost:11435/api/chat \
-H "X-User-ID: developer-1" \
-d '{
"model": "qwen3.5:35b",
"messages": [{"role": "user", "content": "Explain quantum computing."}],
"stream": true
}'
Models can be loaded and unloaded on connected backends without touching the backend servers. Supported backends:
POST /api/generate with an empty prompt (the scheduler loads the model and returns immediately), unloading is the same call with keep_alive: 0.POST /api/v1/models/load / POST /api/v1/models/unload endpoints (unload targets a specific loaded instance). Older LM Studio versions are detected and a friendly error points at the lms load/lms unload CLI.All three endpoints are served by the proxy itself (they are never proxied to a backend) and are protected by the same optional OLLAMA_MQ_API_KEY auth as the proxy routes.
GET /admin/modelsPer-backend inventory: index, URL, online status, detected API type, LM Studio flag, active request count, available models, currently loaded models, and any in-flight control operation.
curl -s http://localhost:11435/admin/models | python3 -m json.tool
POST /admin/models/load and POST /admin/models/unloadBody:
{ "backend": 0, "model": "llama3", "num_ctx": 16384, "keep_alive": 3600, "identifier": "big-ctx" }
backend accepts:
--backend-urls),"any" β the proxy picks a suitable online, idle backend (for load: the first backend where the model resolves; for unload: the first backend that actually has it loaded).model is resolved against each backend's model list: exact match, then :latest/case-insensitive, then a unique substring (admin ops only). Ambiguous names are rejected β the proxy never guesses.
Optional fields (load only):
num_ctx β max context window, sent as Ollama options.num_ctx or LM Studio context_length (omitted = backend default).keep_alive β overrides --load-keep-alive (seconds) for this load. -1 = keep forever.identifier β free-form label shown with the operation in the TUI and API responses.Responses:
| Status | Meaning |
|---|---|
202 | Accepted β the operation started in the background (response body contains the canonical model name and the backend URL). |
400 | Bad request β empty model, backend offline, unsupported backend type, or unloading a model that is not loaded. |
404 | Unknown backend, model not found on the backend, or no suitable backend for "any". |
409 | Conflict β another control operation is already in flight on that backend, or the backend is busy with active requests. |
# Load (llama3 resolves to llama3:latest)
curl -s -X POST http://localhost:11435/admin/models/load \
-H "Content-Type: application/json" \
-d '{"backend": 0, "model": "llama3"}'
# Unload by URL (frees GPU memory on Ollama)
curl -s -X POST http://localhost:11435/admin/models/unload \
-H "Content-Type: application/json" \
-d '{"backend": "http://10.0.0.2:11434", "model": "qwen2.5:7b"}'
# Let the proxy pick a backend
curl -s -X POST http://localhost:11435/admin/models/load \
-H "Content-Type: application/json" \
-d '{"backend": "any", "model": "llama3:latest"}'
# With API-key auth enabled
curl -s http://localhost:11435/admin/models -H "Authorization: Bearer supersecret"
Note: Ollama keeps a model loaded for keep_alive seconds only (its own default is 5 minutes). A control "load" sends the --load-keep-alive value (default 24 h) so the model actually stays resident; an "unload" sends keep_alive: 0.
ollamaMQ uses tracing and behaves differently depending on the mode:
ollamamq.log file in the current working directory. This keeps the terminal clear for the dashboard. Default level is info.--no-tui mode (Docker/CI/service): Logs are written to stderr at a default level of debug, so you can see everything β backend health checks, per-request routing, backend detection, and errors. This is ideal for capturing output via the systemd journal, Docker, or piping to a file.Override the level at any time with the standard RUST_LOG environment variable:
# Most verbose (all debug detail)
RUST_LOG=debug ollamaMQ --no-tui
# Quieter (info + errors only)
RUST_LOG=info ollamaMQ --no-tui
To run ollamaMQ as a background service with full log visibility in the journal:
Copy the provided service file to your system's unit directory:
sudo cp ollamamq.service /etc/systemd/system/
Edit the file and set the correct ExecStart path to your installed binary (e.g. ~/.cargo/bin/ollamaMQ) and your backend URLs:
ExecStart=/usr/local/bin/ollamaMQ --no-tui --port 11435 --backend-urls http://localhost:11434
Note: If you installed
ollamaMQwithcargo install, the binary is at~/.cargo/bin/ollamaMQ, not/usr/local/bin/ollamaMQ. PointExecStartat the real path, or create a symlink so the default path works:# Find the real path which ollamaMQ # Option A: edit ExecStart to use the real path # Option B: symlink it to the expected location sudo ln -s "$HOME/.cargo/bin/ollamaMQ" /usr/local/bin/ollamaMQ
Reload systemd, then enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now ollamamq
Follow the logs in real time via the journal:
journalctl -u ollamamq -f
Or show the most recent 100 lines:
journalctl -u ollamamq -n 100
Because --no-tui defaults to debug logging on stderr, all dispatcher events (backend health, request routing, errors) are captured in the journal β no separate log file needed.
appconf.yaml)Everything about your ollamaMQ instance lives in one YAML file with three sections: backends (what to connect to), settings (runtime options) and models (which models to load where). The default path is appconf.yaml in the working directory; override it with -c/--model-config. A missing file is fine β defaults apply.
# --- Backends: every Ollama / LM Studio / OpenAI-compatible server you want to use
backends:
- http://10.137.1.1:11434 # e.g. Ollama
- http://10.137.1.2:1234 # e.g. LM Studio
# --- Runtime settings (all optional β defaults shown)
settings:
port: 11435 # proxy listen port (default 11435)
host: 127.0.0.1 # bind interface; use 0.0.0.0 for LAN/Docker access (default 127.0.0.1)
timeout: 300 # per-request timeout in seconds (default 300)
load_keep_alive: 86400 # how long model-control loads stay resident; -1 = forever (default 86400)
allow_all_routes: false # also proxy non-standard endpoints as fallback (default false)
stuck_timeout: 60 # fail-fast 503 after N s when no backend can ever serve a queued request (default 60)
max_concurrent_per_backend: 1 # global cap of in-flight requests per backend; raise to let one backend handle several at once (default 1)
# --- Models to load on startup / reload, via the model-control logic
models:
- name: "gpt-oss:120b" # model name as the backend knows it
identifier: "my-gpt" # label attached to this entry's load op (TUI / admin API)
max_ctx: 128000 # context window β Ollama num_ctx / LM Studio context_length
keep_alive: 86400 # seconds the model stays resident after load (Ollama; -1 = forever)
max_concurrent_requests: 3 # in-flight requests allowed for this model on one backend (default 1)
backends: # which backends to load it on (exact or substring URL match); omitted/empty = any suitable backend
- http://10.137.1.1:11434
How it's applied:
backends and settings are read at startup only β restart to change them.models is applied automatically at startup (once backend probes have run) and re-applied any time you press r in the TUI. Application is additive: each target endpoint is checked live first (Ollama /api/ps, LM Studio loaded instances), so models already resident there β e.g. still loaded from an earlier run because of long keep_alive β are skipped instead of being loaded twice; everything else gets a load started. It never unloads anything, and loads for the same backend run one at a time (backends reject parallel control ops). Every attempt, including skips, is reported in the TUI Logs panel (β³ CTL) and in the log output.--backend-urls over backends, --port/--host/--timeout over settings).See appconf.yaml.example for a fully commented template.
Every proxied request (IN) and upstream response (OUT) is written as JSON lines to settings.request_log_path (default ollamamq-requests.jsonl). Each line is one JSON object:
| field | meaning |
|---|---|
ts | unix time in milliseconds |
dir | IN (proxied request) or OUT (upstream response) |
user | client address |
model | model name, when resolvable (otherwise null) |
backend | upstream backend URL (may be null) |
method | HTTP method |
path | request path |
status | response status code (responses only) |
bytes | total body size in bytes |
content_type | body content type |
content | body preview, truncated to log_content_limit with a ...[truncated: N bytes total] marker |
Rotation: when the file exceeds request_log_max_bytes it is renamed to .0 (existing .0 β .1, etc.), at most request_log_max_files rotated files are kept and the oldest is deleted. The current file size is re-read at startup, so sizing/rotation survives restarts.
TUI: the bottom "Requests" panel shows these newest-first with a one-line content preview; press Enter on a row for the full-content detail view (j/k or PgUp/PgDn scroll, q/Esc/Enter close).
Note: request/response headers are deliberately not logged β this keeps API keys out of the file.
The included docker-compose.yml provides a ready-to-use configuration:
services:
ollamamq:
build: .
image: chlebon/ollamamq:latest
container_name: ollamamq
ports:
- "11435:11435"
environment:
# URLs of backend servers (Ollama, LM Studio, etc.)
- BACKEND_URLS=http://host.docker.internal:11434,http://host.docker.internal:1234
- PORT=11435
- TIMEOUT=300
- RUST_LOG=info
command: ["--no-tui"]
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:11435/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
Note for Linux Users: When running in Docker on Linux to access a host-based Ollama:
0.0.0.0. You can do this by setting export OLLAMA_HOST=0.0.0.0 before starting the Ollama service (or editing the systemd unit file).ufw) allows traffic from the Docker bridge (usually 172.17.0.1/16) to port 11434.extra_hosts setting in docker-compose.yml maps host.docker.internal to your host's IP address.The Dockerfile uses a multi-stage build:
rust:1.97-alpine (pinned) to compile the release binaryalpine:3.20 with only ca-certificates for a minimal footprint (~10MB)| Variable | Description | Default |
|---|---|---|
BACKEND_URLS | URLs of the backend servers (Ollama, LM Studio, β¦) | http://localhost:11434 |
OLLAMA_URLS | Legacy alias for BACKEND_URLS (used only when BACKEND_URLS is unset) | β |
PORT | Port for ollamaMQ to listen on | 11435 |
HOST | Host/interface to bind to | 0.0.0.0 |
TIMEOUT | Request timeout in seconds | 300 |
OLLAMA_MQ_API_KEY | Optional API key; enables auth when set | (unset β auth disabled) |
By default, ollamaMQ only listens on loopback (127.0.0.1), so it is only reachable from the local machine. If you expose it to the network with --host 0.0.0.0, set the OLLAMA_MQ_API_KEY environment variable to a secret key to protect the proxy. Auth is opt-in: if the variable is unset or empty, behavior is unchanged and no key is required.
When a key is set, every request except /health must present it via one of two headers:
Authorization: Bearer <key>X-API-Key: <key>Requests with a missing or wrong key get 401 Unauthorized ({"error":"unauthorized"}). The /health endpoint always stays unauthenticated so health checks and Docker healthchecks keep working.
# Enable auth
OLLAMA_MQ_API_KEY=supersecret ollamaMQ --no-tui
# Call the API with the key (either header works)
curl -X POST http://localhost:11435/api/chat \
-H "Authorization: Bearer supersecret" \
-H "X-User-ID: developer-1" \
-d '{"model": "llama3", "messages": [{"role": "user", "content": "Hi"}]}'
# ...or with X-API-Key
curl http://localhost:11435/api/tags -H "X-API-Key: supersecret"
# /health never requires a key
curl http://localhost:11435/health
In Docker, just pass the variable through β the binary reads it directly from the environment:
docker run -d --name ollamamq -p 11435:11435 \
-e OLLAMA_MQ_API_KEY=supersecret \
chlebon/ollamamq
To change the proxy port, use the PORT environment variable:
docker run -d \
--name ollamamq \
-p 8080:8080 \
-e PORT=8080 \
chlebon/ollamamq
To publish a new version of ollamaMQ to Docker Hub, follow these steps:
Update Version: Update the version number in Cargo.toml.
Build and Tag:
# Build the image for the current version
docker build -t chlebon/ollamamq:v0.2.4 .
# Tag it as latest
docker tag chlebon/ollamamq:v0.2.4 chlebon/ollamamq:latest
Push to Hub:
# Log in to Docker Hub (if not already logged in)
docker login
# Push the versioned tag
docker push chlebon/ollamamq:v0.2.4
# Push the latest tag
docker push chlebon/ollamamq:latest
You can use the provided test_dispatcher.sh script to simulate multiple users and verify the dispatcher's behavior under load:
./test_dispatcher.sh

This project is licensed under the MIT License - see the LICENSE file for details (if applicable).
Hacker News (1)
Rust
93.7%
Shell
4.0%
Python
1.4%