mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
511 lines
15 KiB
Markdown
511 lines
15 KiB
Markdown
# 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.<platform>]` 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
|
|
```
|