# 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 git clone https://github.com/open-jarvis/OpenJarvis.git cd OpenJarvis uv sync --extra 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. ### `GET /v1/channels` List registered channel backends and their connection status. #### Response ```json { "channels": ["slack", "discord", "telegram"] } ``` ### `POST /v1/channels/send` Send a message to a specific channel. #### Request Body ```json { "target": "slack", "message": "Hello from Jarvis!" } ``` #### Response ```json { "status": "sent", "target": "slack" } ``` ### `GET /v1/channels/status` Show connection status for all configured channels. #### Response ```json { "channels": { "slack": "connected", "discord": "connected", "telegram": "disconnected" } } ``` !!! note "Channel endpoints" Channel endpoints require `[channel] enabled = true` in your config and platform-specific credentials configured in `[channel.]` sub-sections. When not configured, `GET /v1/channels` returns an empty list and other channel endpoints return 503. ## 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 ```