Add MkDocs Material documentation site with 40 pages and auto-generated API reference

Sets up a complete documentation website with 7 navigable sections (Home, Getting
Started, User Guide, Architecture, API Reference, Deployment, Development), light/dark
mode, search, code copy, and Mermaid diagram support. API reference pages use
mkdocstrings to auto-generate docs from source docstrings. GitHub Actions workflow
deploys to GitHub Pages on push to main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-02-21 06:09:36 +00:00
co-authored by Claude Opus 4.6
parent 990d7d8a79
commit f75afefcfb
45 changed files with 10822 additions and 0 deletions
+455
View File
@@ -0,0 +1,455 @@
# API Server
OpenJarvis includes an OpenAI-compatible API server built on FastAPI and uvicorn. It exposes chat completion, model listing, and health check endpoints, making it a drop-in replacement for the OpenAI API when working with local models.
## Starting the Server
The server requires the `[server]` extra (FastAPI + uvicorn):
```bash
pip install 'openjarvis[server]'
```
Start with default settings:
```bash
jarvis serve
```
The server reads defaults from `~/.openjarvis/config.toml` and auto-detects available engines and models. Override any option via CLI flags:
```bash
jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b --agent orchestrator
```
### CLI Options
| Option | Description | Default |
|----------------------|------------------------------------------------------------------------------|--------------------|
| `--host` | Network address to bind to | From config (`0.0.0.0`) |
| `--port` | Port number to listen on | From config (`8000`) |
| `-e` / `--engine` | Inference engine backend (`ollama`, `vllm`, `llamacpp`, `sglang`) | Auto-detected |
| `-m` / `--model` | Default model for completions | First available |
| `-a` / `--agent` | Agent for non-streaming requests (`simple`, `orchestrator`, `react`, `openhands`) | From config (`orchestrator`) |
On startup, the server prints a summary:
```
Starting OpenJarvis API server
Engine: ollama
Model: qwen3:8b
Agent: orchestrator
URL: http://0.0.0.0:8000
```
!!! warning "Server dependency check"
If the `[server]` extra is not installed, `jarvis serve` exits with a clear error message explaining how to install the required dependencies.
## Endpoints
### `POST /v1/chat/completions`
The primary endpoint for generating chat completions. Accepts the same request format as the OpenAI Chat Completions API.
#### Request Body
```json
{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of France?"}
],
"temperature": 0.7,
"max_tokens": 1024,
"stream": false,
"tools": null
}
```
| Parameter | Type | Default | Description |
|---------------|-------------------|---------|--------------------------------------------------------------|
| `model` | `string` | -- | **Required.** Model identifier to use for generation. |
| `messages` | `array` | -- | **Required.** Array of message objects with `role` and `content`. |
| `temperature` | `float` | `0.7` | Sampling temperature (0.0 to 2.0). |
| `max_tokens` | `integer` | `1024` | Maximum number of tokens to generate. |
| `stream` | `boolean` | `false` | Whether to stream the response via SSE. |
| `tools` | `array` or `null` | `null` | Tool definitions in OpenAI function-calling format. |
Each message object:
| Field | Type | Description |
|----------------|-------------------|-------------------------------------------------------|
| `role` | `string` | One of `system`, `user`, `assistant`, or `tool`. |
| `content` | `string` | The message content. |
| `name` | `string` or `null`| Optional name for the message author. |
| `tool_calls` | `array` or `null` | Tool calls made by the assistant (in assistant messages). |
| `tool_call_id` | `string` or `null`| ID of the tool call this message responds to (in tool messages). |
#### Response (Non-Streaming)
```json
{
"id": "chatcmpl-abc123def456",
"object": "chat.completion",
"created": 1740100800,
"model": "qwen3:8b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The capital of France is Paris.",
"tool_calls": null
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 8,
"total_tokens": 33
}
}
```
When an agent is configured on the server, non-streaming requests are routed through the agent, which can perform multi-turn reasoning with tool calls before returning a final response. When no agent is configured, requests go directly to the inference engine.
#### Tool Calls
When `tools` are provided in the request, the engine may return `tool_calls` in the assistant message:
```json
{
"choices": [
{
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "calculator",
"arguments": "{\"expression\": \"2 + 2\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}
```
### `GET /v1/models`
Lists all models available on the configured inference engine.
#### Response
```json
{
"object": "list",
"data": [
{
"id": "qwen3:8b",
"object": "model",
"created": 1740100800,
"owned_by": "openjarvis"
},
{
"id": "llama3.1:8b",
"object": "model",
"created": 1740100800,
"owned_by": "openjarvis"
}
]
}
```
### `GET /health`
Health check endpoint that verifies the inference engine is responsive.
#### Response (Healthy)
HTTP 200:
```json
{"status": "ok"}
```
#### Response (Unhealthy)
HTTP 503:
```json
{"detail": "Engine unhealthy"}
```
### `GET /dashboard`
Serves the built-in Savings Dashboard, an HTML page that displays real-time statistics on inference calls served locally and estimated cost savings compared to cloud API providers. The dashboard auto-refreshes every 5 seconds by polling the `/v1/savings` endpoint.
## Streaming via SSE
When `"stream": true` is set in the request, the server returns a `text/event-stream` response using Server-Sent Events (SSE). The response follows the same format as the OpenAI streaming API.
Each event is a `data:` line containing a JSON chunk, followed by a blank line:
```
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"content":"The"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{"content":" capital"},"finish_reason":null}]}
...
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1740100800,"model":"qwen3:8b","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
The stream follows this sequence:
1. **Role chunk** -- first chunk contains `"delta": {"role": "assistant"}` with no content.
2. **Content chunks** -- subsequent chunks each contain a `"delta": {"content": "..."}` with one or more tokens.
3. **Finish chunk** -- a chunk with an empty `delta` and `"finish_reason": "stop"`.
4. **Done signal** -- the literal string `data: [DONE]` indicates the stream is complete.
Response headers include `Cache-Control: no-cache` and `Connection: keep-alive` for proper SSE behavior.
## Client Examples
=== "curl"
**Non-streaming request:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Explain quantum computing in one paragraph."}
],
"temperature": 0.7,
"max_tokens": 256
}'
```
**Streaming request:**
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Write a haiku about programming."}
],
"stream": true
}'
```
**List models:**
```bash
curl http://localhost:8000/v1/models
```
**Health check:**
```bash
curl http://localhost:8000/health
```
=== "Python (openai)"
The OpenAI Python library works as a drop-in client by pointing `base_url` at the local server:
```python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed", # Required by the library but not validated
)
# Non-streaming
response = client.chat.completions.create(
model="qwen3:8b",
messages=[
{"role": "user", "content": "What is the capital of France?"}
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)
# Streaming
stream = client.chat.completions.create(
model="qwen3:8b",
messages=[
{"role": "user", "content": "Write a short poem about AI."}
],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
# List models
models = client.models.list()
for model in models.data:
print(model.id)
```
=== "Python (httpx)"
Using `httpx` for direct HTTP requests:
```python
import httpx
import json
BASE_URL = "http://localhost:8000"
# Non-streaming request
response = httpx.post(
f"{BASE_URL}/v1/chat/completions",
json={
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "What is the capital of France?"}
],
"temperature": 0.7,
"max_tokens": 256,
},
)
data = response.json()
print(data["choices"][0]["message"]["content"])
# Streaming request
with httpx.stream(
"POST",
f"{BASE_URL}/v1/chat/completions",
json={
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Write a haiku about code."}
],
"stream": True,
},
) as response:
for line in response.iter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
chunk = json.loads(line[6:])
content = chunk["choices"][0]["delta"].get("content", "")
if content:
print(content, end="", flush=True)
print()
# List models
response = httpx.get(f"{BASE_URL}/v1/models")
for model in response.json()["data"]:
print(model["id"])
# Health check
response = httpx.get(f"{BASE_URL}/health")
print(response.json())
```
## Configuration via `config.toml`
The `[server]` section of `~/.openjarvis/config.toml` controls default server behavior:
```toml
[server]
host = "0.0.0.0"
port = 8000
agent = "orchestrator"
model = ""
workers = 1
```
| Key | Type | Default | Description |
|-----------|-----------|-----------------|----------------------------------------------------------------------------|
| `host` | `string` | `"0.0.0.0"` | Network address to bind to. Use `"127.0.0.1"` for localhost-only access. |
| `port` | `integer` | `8000` | Port number. |
| `agent` | `string` | `"orchestrator"`| Default agent for non-streaming requests. Set to `""` for direct engine mode. |
| `model` | `string` | `""` | Default model name. When empty, falls back to `[intelligence] default_model` or the first model discovered on the engine. |
| `workers` | `integer` | `1` | Number of uvicorn workers (for future use). |
CLI flags override config file values. For example, `jarvis serve --port 9000` overrides the `port` setting in the config file.
The server also reads from other config sections at startup:
- **`[engine]`** -- determines which inference backend to connect to and its host URL.
- **`[intelligence]`** -- provides the fallback `default_model` when no model is specified.
- **`[agent]`** -- supplies `max_turns` for multi-turn agents like `orchestrator`.
## Running Behind a Reverse Proxy
For production deployments, run OpenJarvis behind a reverse proxy like Nginx or Caddy for TLS termination, rate limiting, and authentication.
### Nginx
```nginx
server {
listen 443 ssl;
server_name jarvis.example.com;
ssl_certificate /etc/ssl/certs/jarvis.pem;
ssl_certificate_key /etc/ssl/private/jarvis.key;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE streaming support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
}
}
```
!!! important "Disable buffering for SSE"
The `proxy_buffering off` directive is critical for streaming responses. Without it, Nginx buffers the SSE chunks and delivers them in batches, defeating the purpose of streaming.
### Caddy
```
jarvis.example.com {
reverse_proxy 127.0.0.1:8000 {
flush_interval -1
}
}
```
The `flush_interval -1` setting disables response buffering, which is required for SSE streaming.
### Bind to Localhost
When running behind a reverse proxy, bind the server to `127.0.0.1` so it only accepts connections from the proxy:
```bash
jarvis serve --host 127.0.0.1 --port 8000
```
Or in `config.toml`:
```toml
[server]
host = "127.0.0.1"
port = 8000
```
+337
View File
@@ -0,0 +1,337 @@
# Docker Deployment
OpenJarvis provides Docker images for both CPU-only and GPU-accelerated deployments, along with a Docker Compose configuration that bundles the API server with an Ollama inference backend.
## Quick Start
The fastest way to get OpenJarvis running in Docker is with Docker Compose, which starts both the API server and an Ollama backend:
```bash
docker compose up -d
```
This brings up two services:
| Service | Port | Description |
|----------|-------|------------------------------------|
| `jarvis` | 8000 | OpenJarvis API server |
| `ollama` | 11434 | Ollama inference engine |
Verify the server is running:
```bash
curl http://localhost:8000/health
```
Expected response:
```json
{"status": "ok"}
```
## Docker Images
### CPU-Only Image (`Dockerfile`)
The default `Dockerfile` uses a multi-stage build based on `python:3.12-slim` to produce a minimal image.
**Build stages:**
1. **Builder stage** -- installs `uv` and the `openjarvis[server]` package (which includes FastAPI, uvicorn, and all server dependencies) from the project source.
2. **Runtime stage** -- copies only the installed Python packages and application code from the builder, keeping the final image small.
```dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server]"
FROM python:3.12-slim
COPY --from=builder /usr/local /usr/local
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 8000
ENTRYPOINT ["jarvis"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
```
Build it manually:
```bash
docker build -t openjarvis:latest .
```
Run it standalone:
```bash
docker run -d -p 8000:8000 openjarvis:latest
```
### GPU Image (`Dockerfile.gpu`)
The GPU image is built on `nvidia/cuda:12.4.0-runtime-ubuntu22.04` and includes the CUDA 12.4 runtime libraries, enabling GPU-accelerated inference when paired with a GPU-capable engine like vLLM or SGLang.
```dockerfile
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04 AS builder
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server]"
FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local /usr/local
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 8000
ENTRYPOINT ["jarvis"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8000"]
```
Build the GPU image:
```bash
docker build -f Dockerfile.gpu -t openjarvis:gpu .
```
Run with GPU access (requires the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)):
```bash
docker run -d --gpus all -p 8000:8000 openjarvis:gpu
```
!!! note "NVIDIA Container Toolkit required"
The host machine must have the NVIDIA Container Toolkit installed for `--gpus` to work. See the [NVIDIA installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for setup instructions.
## Docker Compose Configuration
The `docker-compose.yml` defines a complete deployment with the OpenJarvis API server and an Ollama backend:
```yaml
version: "3.9"
services:
jarvis:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
restart: unless-stopped
volumes:
ollama-models:
```
### Environment Variables
The `jarvis` service is configured through environment variables:
| Variable | Description | Default |
|-------------------------------|---------------------------------------------------------|----------------------------|
| `OPENJARVIS_ENGINE_DEFAULT` | Inference engine backend to use | `ollama` |
| `OPENJARVIS_OLLAMA_HOST` | URL of the Ollama server (uses Docker service name) | `http://ollama:11434` |
### Volumes
The `ollama-models` named volume persists downloaded models across container restarts, so models do not need to be re-pulled after a `docker compose down` / `docker compose up` cycle.
### Service Dependencies
The `jarvis` service declares `depends_on: ollama`, ensuring the Ollama container starts before the API server. Both services use `restart: unless-stopped` to automatically recover from crashes.
## Custom Configuration
### Mounting a Configuration File
To use a custom `config.toml`, mount it into the container at the expected path (`~/.openjarvis/config.toml`, which is `/root/.openjarvis/config.toml` in the container):
```yaml
services:
jarvis:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./my-config.toml:/root/.openjarvis/config.toml:ro
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
```
### Persisting Data
To persist telemetry data, memory databases, and trace records across container restarts, mount the entire OpenJarvis data directory:
```yaml
services:
jarvis:
# ... other config ...
volumes:
- openjarvis-data:/root/.openjarvis
volumes:
ollama-models:
openjarvis-data:
```
This preserves:
- `telemetry.db` -- inference call telemetry records
- `memory.db` -- the default SQLite memory backend
- `traces.db` -- interaction trace records
- `config.toml` -- user configuration
### Using the GPU Image with Compose
To use the GPU Dockerfile in your Compose setup, change the `dockerfile` field and add GPU resource reservations:
```yaml
services:
jarvis:
build:
context: .
dockerfile: Dockerfile.gpu
ports:
- "8000:8000"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
environment:
- OPENJARVIS_ENGINE_DEFAULT=ollama
- OPENJARVIS_OLLAMA_HOST=http://ollama:11434
depends_on:
- ollama
restart: unless-stopped
```
## Health Check
The API server exposes a `GET /health` endpoint that checks whether the underlying inference engine is responsive:
```bash
curl http://localhost:8000/health
```
A healthy response returns HTTP 200:
```json
{"status": "ok"}
```
An unhealthy engine returns HTTP 503:
```json
{"detail": "Engine unhealthy"}
```
You can integrate this into your Docker Compose healthcheck:
```yaml
services:
jarvis:
# ... other config ...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
```
## Building Custom Images
### Adding Extra Dependencies
To include additional engine backends (such as vLLM or ColBERT memory), modify the install command in the Dockerfile:
```dockerfile
RUN pip install --no-cache-dir uv && \
uv pip install --system ".[server,inference-vllm,memory-colbert]"
```
### Overriding the Default Command
The entrypoint is `jarvis` and the default command is `serve --host 0.0.0.0 --port 8000`. Override the command to change server options:
```bash
docker run -d -p 9000:9000 openjarvis:latest \
serve --host 0.0.0.0 --port 9000 --engine ollama --model qwen3:8b
```
Or in Docker Compose:
```yaml
services:
jarvis:
build: .
command: ["serve", "--host", "0.0.0.0", "--port", "9000", "--model", "qwen3:8b"]
ports:
- "9000:9000"
```
### Available CLI Options for `jarvis serve`
| Option | Description |
|----------------------|-----------------------------------------------------|
| `--host` | Bind address (default: from config, typically `0.0.0.0`) |
| `--port` | Port number (default: from config, typically `8000`) |
| `-e` / `--engine` | Engine backend (`ollama`, `vllm`, `llamacpp`, `sglang`) |
| `-m` / `--model` | Default model name |
| `-a` / `--agent` | Agent for non-streaming requests (`simple`, `orchestrator`, `react`, `openhands`) |
## Pulling Models
After starting the Ollama container, you need to pull at least one model before the API server can serve requests:
```bash
docker compose exec ollama ollama pull qwen3:8b
```
Verify models are available through the API:
```bash
curl http://localhost:8000/v1/models
```
+251
View File
@@ -0,0 +1,251 @@
# launchd Service (macOS)
OpenJarvis includes a launchd property list (plist) for running the API server as a background service on macOS. This provides automatic startup at login, automatic restart if the process exits, and log capture.
## Prerequisites
Before installing the service, ensure that OpenJarvis is installed and the `jarvis` command is available at `/usr/local/bin/jarvis`. If you installed via `uv` or `pip` with a different prefix, adjust the path in the plist accordingly.
```bash
pip install 'openjarvis[server]'
which jarvis # Verify the installation path
```
Also ensure that an inference engine (such as Ollama) is running and accessible on the machine.
## Installing the Service
Copy the plist file to `~/Library/LaunchAgents` and load it:
```bash
cp deploy/launchd/com.openjarvis.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
```
The service starts immediately (due to `RunAtLoad`) and will automatically restart at each login.
Verify it is running:
```bash
launchctl list | grep openjarvis
```
You should see a line with the PID and the label `com.openjarvis`. A `0` in the status column indicates the service is running normally.
Confirm the server is responding:
```bash
curl http://localhost:8000/health
```
## Plist Reference
The provided plist file at `deploy/launchd/com.openjarvis.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openjarvis</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/openjarvis.stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/openjarvis.stderr.log</string>
</dict>
</plist>
```
### Key-by-Key Explanation
| Key | Value | Description |
|----------------------|--------------------------------|------------------------------------------------------------------------------------------------------|
| `Label` | `com.openjarvis` | Unique identifier for the service. Used with `launchctl` commands to manage the service. |
| `ProgramArguments` | `["/usr/local/bin/jarvis", "serve", "--host", "0.0.0.0", "--port", "8000"]` | The command and arguments to execute. Each element of the command line is a separate string in the array. |
| `RunAtLoad` | `true` | Start the service immediately when the plist is loaded (and on each login). |
| `KeepAlive` | `true` | Automatically restart the service if it exits for any reason. launchd monitors the process and relaunches it. |
| `StandardOutPath` | `/tmp/openjarvis.stdout.log` | File where standard output is written. Contains server startup messages and access logs. |
| `StandardErrorPath` | `/tmp/openjarvis.stderr.log` | File where standard error is written. Contains error messages and stack traces. |
## Viewing Logs
Server output is written to the two log files specified in the plist:
```bash
# View standard output (startup messages, access logs)
cat /tmp/openjarvis.stdout.log
# View standard error (errors, warnings)
cat /tmp/openjarvis.stderr.log
# Follow logs in real time
tail -f /tmp/openjarvis.stdout.log /tmp/openjarvis.stderr.log
```
!!! tip "Persistent log location"
Files in `/tmp` may be cleared on reboot. For persistent logs, change the paths in the plist to a permanent location:
```xml
<key>StandardOutPath</key>
<string>/Users/yourname/.openjarvis/openjarvis.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/yourname/.openjarvis/openjarvis.stderr.log</string>
```
After changing the plist, unload and reload the service for the changes to take effect.
## Managing the Service
### Loading and Unloading
```bash
# Load the service (starts it due to RunAtLoad)
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
# Unload the service (stops it and prevents it from starting at login)
launchctl unload ~/Library/LaunchAgents/com.openjarvis.plist
```
### Starting and Stopping
If the service is loaded but you want to manually stop or start it without unloading:
```bash
# Stop the service
launchctl stop com.openjarvis
# Start the service
launchctl start com.openjarvis
```
!!! warning
Because `KeepAlive` is set to `true`, using `launchctl stop` will cause launchd to restart the service almost immediately. To fully stop the service, use `launchctl unload` instead.
### Checking Status
```bash
# List all loaded services matching "openjarvis"
launchctl list | grep openjarvis
```
The output columns are:
| Column | Description |
|--------|----------------------------------------------------------------|
| PID | Process ID (or `-` if not running) |
| Status | Last exit status (`0` = normal) |
| Label | The service label (`com.openjarvis`) |
## Configuration Changes
### Changing the Port or Host
Edit the `ProgramArguments` array in the plist. Each argument must be a separate `<string>` element:
```xml
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>127.0.0.1</string>
<string>--port</string>
<string>9000</string>
</array>
```
### Specifying an Engine and Model
Add additional arguments to the array:
```xml
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
<string>--engine</string>
<string>ollama</string>
<string>--model</string>
<string>qwen3:8b</string>
</array>
```
### Setting Environment Variables
Add an `EnvironmentVariables` dictionary to the plist:
```xml
<key>EnvironmentVariables</key>
<dict>
<key>OPENJARVIS_ENGINE_DEFAULT</key>
<string>ollama</string>
<key>OPENJARVIS_OLLAMA_HOST</key>
<string>http://localhost:11434</string>
</dict>
```
### Using a Different `jarvis` Binary Path
If `jarvis` is installed in a virtual environment or a non-standard location, update the first element of `ProgramArguments`:
```xml
<key>ProgramArguments</key>
<array>
<string>/Users/yourname/.local/bin/jarvis</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8000</string>
</array>
```
### Applying Changes
After editing the plist file, unload and reload the service:
```bash
launchctl unload ~/Library/LaunchAgents/com.openjarvis.plist
launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
```
## System-Wide Installation
The instructions above install the service as a **user agent** (runs only when you are logged in). To run OpenJarvis as a system-wide daemon that starts at boot regardless of user login:
1. Copy the plist to `/Library/LaunchDaemons/` (requires `sudo`).
2. Set the file ownership to `root:wheel`.
3. Optionally add a `UserName` key to run as a specific user.
```bash
sudo cp deploy/launchd/com.openjarvis.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.openjarvis.plist
sudo launchctl load /Library/LaunchDaemons/com.openjarvis.plist
```
!!! note
System daemons in `/Library/LaunchDaemons/` run as root by default. Add a `UserName` key to run as a less-privileged user:
```xml
<key>UserName</key>
<string>openjarvis</string>
```
+242
View File
@@ -0,0 +1,242 @@
# systemd Service (Linux)
OpenJarvis includes a systemd unit file for running the API server as a managed background service on Linux. This provides automatic startup on boot, crash recovery, and integration with standard Linux service management tools.
## Prerequisites
Before installing the service, ensure that:
1. OpenJarvis is installed in a virtual environment at `/opt/openjarvis/.venv` (or adjust paths accordingly).
2. A dedicated `openjarvis` system user exists (recommended for security).
3. An inference engine (such as Ollama) is running and accessible.
Create the user and installation directory:
```bash
sudo useradd --system --create-home --home-dir /opt/openjarvis openjarvis
sudo -u openjarvis python3 -m venv /opt/openjarvis/.venv
sudo -u openjarvis /opt/openjarvis/.venv/bin/pip install 'openjarvis[server]'
```
## Installing the Service
Copy the unit file to the systemd directory, reload the daemon, and enable the service:
```bash
sudo cp deploy/systemd/openjarvis.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable openjarvis
sudo systemctl start openjarvis
```
Verify it is running:
```bash
sudo systemctl status openjarvis
```
## Service File Reference
The provided unit file at `deploy/systemd/openjarvis.service`:
```ini
[Unit]
Description=OpenJarvis API Server
After=network.target
[Service]
Type=simple
User=openjarvis
WorkingDirectory=/opt/openjarvis
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
Environment=HOME=/opt/openjarvis
[Install]
WantedBy=multi-user.target
```
### `[Unit]` Section
| Directive | Value | Description |
|---------------|--------------------|-----------------------------------------------------------------------------|
| `Description` | `OpenJarvis API Server` | Human-readable name shown in `systemctl status` and logs. |
| `After` | `network.target` | Delays startup until the network stack is available, since the server binds to a network socket and may need to reach a remote engine. |
### `[Service]` Section
| Directive | Value | Description |
|--------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| `Type` | `simple` | The process started by `ExecStart` is the main service process. systemd considers the service started immediately. |
| `User` | `openjarvis` | Runs the server as the `openjarvis` user rather than root, limiting the blast radius of any security issue. |
| `WorkingDirectory` | `/opt/openjarvis` | Sets the working directory for the process. This is where OpenJarvis looks for local files and writes data. |
| `ExecStart` | `/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000` | The command to start the server. Uses the full path to the `jarvis` binary inside the virtual environment. |
| `Restart` | `on-failure` | Automatically restarts the service if it exits with a non-zero exit code. Does not restart on clean shutdown (`systemctl stop`). |
| `RestartSec` | `5` | Waits 5 seconds before attempting a restart, preventing rapid restart loops if the service crashes immediately on startup. |
| `Environment` | `HOME=/opt/openjarvis` | Sets the `HOME` environment variable so OpenJarvis finds its configuration at `~/.openjarvis/config.toml` (resolving to `/opt/openjarvis/.openjarvis/config.toml`). |
### `[Install]` Section
| Directive | Value | Description |
|--------------|---------------------|---------------------------------------------------------------------------------------------|
| `WantedBy` | `multi-user.target` | The service starts when the system reaches multi-user mode (standard boot target for servers). `systemctl enable` creates a symlink under this target. |
## Configuration Options
### Changing the Bind Address and Port
Edit the `ExecStart` line to change the host or port:
```ini
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 127.0.0.1 --port 9000
```
!!! tip
Binding to `127.0.0.1` restricts access to localhost only. Use this when running behind a reverse proxy like Nginx or Caddy.
### Setting the Engine and Model
Pass additional flags to `jarvis serve`:
```ini
ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b
```
### Adding Environment Variables
Add multiple `Environment` directives or use `EnvironmentFile` for complex configurations:
```ini
[Service]
Environment=HOME=/opt/openjarvis
Environment=OPENJARVIS_ENGINE_DEFAULT=vllm
Environment=OPENJARVIS_OLLAMA_HOST=http://localhost:11434
```
Or load from a file:
```ini
[Service]
EnvironmentFile=/opt/openjarvis/.env
```
### Changing the User
If you prefer a different service user, update both the `User` directive and the paths:
```ini
[Service]
User=myuser
WorkingDirectory=/home/myuser/openjarvis
ExecStart=/home/myuser/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
Environment=HOME=/home/myuser/openjarvis
```
### Using a Configuration File
Ensure the configuration file exists at the path where `HOME` points:
```bash
sudo -u openjarvis mkdir -p /opt/openjarvis/.openjarvis
sudo -u openjarvis cp config.toml /opt/openjarvis/.openjarvis/config.toml
```
The server reads `~/.openjarvis/config.toml` on startup, where `~` resolves from the `HOME` environment variable.
## Viewing Logs
OpenJarvis logs are captured by journald. View them with `journalctl`:
```bash
# View all logs for the service
sudo journalctl -u openjarvis
# Follow logs in real time
sudo journalctl -u openjarvis -f
# View logs since the last boot
sudo journalctl -u openjarvis -b
# View logs from the last hour
sudo journalctl -u openjarvis --since "1 hour ago"
# View only error-level messages
sudo journalctl -u openjarvis -p err
```
## Managing the Service
### Start, Stop, and Restart
```bash
# Start the service
sudo systemctl start openjarvis
# Stop the service
sudo systemctl stop openjarvis
# Restart the service (stop + start)
sudo systemctl restart openjarvis
# Reload configuration without full restart (sends SIGHUP)
sudo systemctl reload-or-restart openjarvis
```
### Check Status
```bash
sudo systemctl status openjarvis
```
Example output:
```
● openjarvis.service - OpenJarvis API Server
Loaded: loaded (/etc/systemd/system/openjarvis.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-02-21 10:00:00 UTC; 2h ago
Main PID: 12345 (jarvis)
Tasks: 4 (limit: 4915)
Memory: 256.0M
CPU: 1min 23s
CGroup: /system.slice/openjarvis.service
└─12345 /opt/openjarvis/.venv/bin/python /opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
```
### Enable and Disable on Boot
```bash
# Enable automatic start on boot
sudo systemctl enable openjarvis
# Disable automatic start on boot
sudo systemctl disable openjarvis
```
### Apply Changes After Editing the Unit File
After modifying `/etc/systemd/system/openjarvis.service`, reload the systemd daemon and restart the service:
```bash
sudo systemctl daemon-reload
sudo systemctl restart openjarvis
```
## Running Alongside Ollama
If Ollama is also managed via systemd, you can add an ordering dependency so the OpenJarvis service waits for Ollama to start:
```ini
[Unit]
Description=OpenJarvis API Server
After=network.target ollama.service
Requires=ollama.service
```
| Directive | Description |
|------------|--------------------------------------------------------------------------|
| `After` | Ensures OpenJarvis starts after Ollama. |
| `Requires` | If Ollama fails to start, OpenJarvis will not start either. |
!!! note
Use `Wants` instead of `Requires` if you want OpenJarvis to start even when Ollama is unavailable (for example, if you plan to start Ollama manually later).