API Reference¶
Janus exposes five client-facing API surfaces plus utility endpoints:
| Surface | Base path | Format |
|---|---|---|
| OpenAI | /v1/chat/completions |
OpenAI Chat Completions |
| OpenAI Responses | /v1/responses |
OpenAI Responses API |
| Anthropic | /v1/messages |
Anthropic Messages |
| Gemini | /v1beta/models/{model}:generateContent |
Gemini GenerateContent |
| Ollama | /api/chat, /api/generate, /api/show, /api/tags, /api/version |
Ollama chat & completions (NDJSON streaming) |
| Utility | /v1/health, /v1/models |
JSON |
Authentication¶
When require_api_key is enabled, all endpoints except GET /v1/health require
an API key. The setting can come from YAML (server.require_api_key) or the
dashboard Settings page (DB server_require_api_key — takes precedence when set).
Accepted auth methods:
Authorization: Bearer <key>
x-goog-api-key: <key>
?key=<key>
Keys can be:
- Static — listed in the
api_keyssection ofconfig.yaml(always full access) - DB-managed — created via
janus keys createor the dashboard
DB keys support scopes:
- Dashboard login —
can_login(default on). API-only keys authenticate for/v1/*but cannot open the dashboard. - Model allowlist — exact IDs (
openai/gpt-4o) and prefix wildcards (openai/*). Empty/unset means all models. Disallowed models return403witherror.type = "model_not_allowed".GET /v1/modelsis filtered the same way. Bare provider/default aliases are authorized against the resolved namespaced model, so allowingopenai/gpt-4opermits anopenaialias whose default isgpt-4o; allowing the bare wordopenaidoes not. Explicit provider aliases accept either the alias or canonical scope, soxiaomi/*authorizesmimo/mimo-v2.5. Native model IDs that contain a slash are also treated as bare when their leading segment is not a registered provider prefix, such asopenai/gpt-4oresolving toopenrouter/openai/gpt-4o. A combo's exact allowlist entry intentionally grants its curated members without requiring every member to be listed separately. - Budgets — optional daily and/or absolute lifetime per-key spending limits. Absolute limits never reset and include past recorded usage (see Budgets).
When require_api_key is false, no authentication is required (suitable for
local single-user setups).
POST /v1/chat/completions¶
OpenAI Chat Completions format. Accepts the standard OpenAI request body and returns standard OpenAI responses.
Request Body¶
| Field | Type | Description |
|---|---|---|
model |
string |
prefix/model (e.g. openai/gpt-4o) or a combo name |
messages |
array |
Chat messages (role + content) |
stream |
bool |
Stream the response via SSE (default false) |
max_tokens |
int |
Maximum tokens to generate |
temperature |
float |
Sampling temperature |
tools |
array |
Tool/function definitions |
tool_choice |
object |
Tool selection mode (auto, none, required, or specific) |
Non-Streaming¶
curl http://localhost:20128/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100
}'
Response — standard OpenAI ChatCompletion JSON:
{
"id": "chatcmpl-...",
"object": "chat.completion",
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 8,
"total_tokens": 18
}
}
Streaming¶
curl http://localhost:20128/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100,
"stream": true
}'
Response — text/event-stream with data: {json}\n\n lines, terminated by
data: [DONE]:
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Streaming and fallback
Streaming requests do not retry mid-stream. If the first provider fails before streaming begins, Janus falls back to the next account. Once streaming starts, it cannot be replayed.
POST /v1/responses¶
OpenAI Responses API format — the native protocol of Codex CLI and newer OpenAI SDK clients. Accepts the standard Responses request body and returns standard Responses output; Janus translates to whatever format the upstream provider speaks.
Request Body¶
| Field | Type | Description |
|---|---|---|
model |
string |
prefix/model or a combo name |
input |
string \| array |
Prompt text, or a list of message / function_call / function_call_output items |
instructions |
string |
System prompt |
stream |
bool |
Stream via named SSE events (default false) |
max_output_tokens |
int |
Maximum tokens to generate |
tools |
array |
Flat function tool definitions ({"type":"function","name",...}) |
tool_choice |
string \| object |
auto, none, required, or {"type":"function","name":...} |
reasoning |
object |
{"effort": "low" \| "medium" \| "high"} |
store and previous_response_id are accepted but ignored — Janus is stateless.
Non-Streaming¶
curl http://localhost:20128/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "openai/gpt-4o",
"input": "Hello!"
}'
Response — a response object with output items (message, function_call,
reasoning) and usage.
Streaming¶
With "stream": true, the response is text/event-stream using named events
(response.created, response.output_item.added, response.output_text.delta,
response.function_call_arguments.delta, …, response.completed). There is no
[DONE] sentinel — the stream ends after response.completed.
POST /v1/messages¶
Anthropic Messages format. Accepts the standard Anthropic request body and returns standard Anthropic responses.
Request Body¶
| Field | Type | Description |
|---|---|---|
model |
string |
prefix/model (e.g. anthropic/claude-sonnet-4-20250514) or a combo name |
messages |
array |
Chat messages (role + content) |
max_tokens |
int |
Maximum tokens to generate |
system |
string \| array |
System prompt (string or array of content blocks) |
stream |
bool |
Stream the response via SSE (default false) |
tools |
array |
Tool definitions |
Non-Streaming¶
curl http://localhost:20128/v1/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "anthropic/claude-sonnet-4-20250514",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Hello!"}]
}'
Response — standard Anthropic Message JSON:
{
"id": "msg_...",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [
{
"type": "text",
"text": "Hello! How can I help you?"
}
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 8
}
}
Streaming¶
Set "stream": true. The response is a text/event-stream with event-type-prefixed
SSE events:
curl http://localhost:20128/v1/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "anthropic/claude-sonnet-4-20250514",
"max_tokens": 100,
"stream": true,
"messages": [{"role": "user", "content": "Hello!"}]
}'
event: message_start
data: {"type":"message_start","message":{"id":"msg_...","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-20250514","usage":{"input_tokens":10,"output_tokens":1}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":8}}
event: message_stop
data: {"type":"message_stop"}
POST /v1beta/models/{model}:generateContent¶
Gemini GenerateContent format. Point Gemini-native tools at Janus by using the
prefix/model routing convention in the URL path. Janus translates and routes
the request to any configured provider — the upstream does not need to be Gemini.
Routing key
The {model} segment uses prefix/model (e.g. openai/gpt-4o) just like the
other endpoints. This is what Janus uses for routing; the actual upstream model
name is the part after the slash.
Non-Streaming¶
curl "http://localhost:20128/v1beta/models/openai/gpt-4o:generateContent" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"contents": [{"role": "user", "parts": [{"text": "Hello!"}]}]
}'
Response — standard Gemini GenerateContentResponse JSON:
{
"candidates": [
{
"content": {"role": "model", "parts": [{"text": "Hello! How can I help?"}]},
"finishReason": "STOP",
"index": 0
}
],
"usageMetadata": {
"promptTokenCount": 3,
"candidatesTokenCount": 5,
"totalTokenCount": 8
}
}
Streaming¶
Use :streamGenerateContent instead of :generateContent:
curl "http://localhost:20128/v1beta/models/openai/gpt-4o:streamGenerateContent" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"contents": [{"role": "user", "parts": [{"text": "Hello!"}]}]
}'
The response is a text/event-stream of JSON objects, one per chunk.
Authentication
When require_api_key is on, Gemini-style auth is also accepted: the
x-goog-api-key header or ?key= query parameter.
POST /api/chat (Ollama)¶
Ollama chat format — for tools that only support Ollama endpoints. Point the
tool's Ollama host at Janus (http://localhost:20128).
| Field | Type | Description |
|---|---|---|
model |
string |
prefix/model or a combo name |
messages |
array |
Chat messages (system/user/assistant/tool, images supported) |
stream |
bool |
Defaults to true (Ollama convention); NDJSON chunks |
tools |
array |
Function tool definitions (Ollama/OpenAI nested shape) |
options |
object |
num_predict, temperature, top_p, stop |
curl http://localhost:20128/api/chat \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "openai/gpt-4o",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": false
}'
Streaming responses are application/x-ndjson — one JSON object per line,
ending with a "done": true object carrying done_reason,
prompt_eval_count, and eval_count.
POST /api/generate (Ollama)¶
Ollama completion format — bare prompt instead of messages. Janus translates
to chat internally and remaps the response to Ollama's generate shape (response
field instead of message).
| Field | Type | Description |
|---|---|---|
model |
string |
prefix/model or a combo name |
prompt |
string |
Completion prompt |
stream |
bool |
Defaults to true; NDJSON chunks with response deltas |
options |
object |
Same as /api/chat (num_predict, temperature, etc.) |
curl http://localhost:20128/api/generate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{
"model": "openai/gpt-4o",
"prompt": "Hello!",
"stream": false
}'
Non-streaming responses include "done": true and "response": "...". Streaming
uses application/x-ndjson with per-chunk response text and a final "done": true
line.
POST /api/show (Ollama)¶
Model metadata handshake for Ollama clients. Accepts name (or model) and returns
stub metadata for routable models/combos; unknown or allowlist-blocked models return
404.
curl http://localhost:20128/api/show \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-janus-yourkey" \
-d '{"name": "openai/gpt-4o"}'
Response includes details, capabilities (completion), and a minimal template.
GET /api/tags lists models in Ollama's tags shape; GET /api/version returns a
static version for client handshakes (no auth required). Both respect the API key
model allowlist when auth is enabled.
GET /v1/models¶
Lists all registered provider models and combos.
curl http://localhost:20128/v1/models \
-H "Authorization: Bearer sk-janus-yourkey"
Response:
{
"object": "list",
"data": [
{
"id": "openai/gpt-4o",
"object": "model",
"created": 0,
"owned_by": "openai"
},
{
"id": "anthropic/claude-sonnet-4-20250514",
"object": "model",
"created": 0,
"owned_by": "anthropic"
},
{
"id": "best-effort",
"object": "model",
"created": 0,
"owned_by": "combo"
}
]
}
Provider models have owned_by set to the config id. Combos have owned_by
set to "combo".
GET /v1/health¶
Health check. No authentication required.
curl http://localhost:20128/v1/health
{"status": "ok"}
Error Responses¶
| Status | When | Details |
|---|---|---|
401 |
Invalid API key | Returned when require_api_key is on and the key is missing or unrecognized |
402 → fallback |
Upstream payment error | Classified as a payment error, fallback-eligible, cools the account for 300s (5 minutes) |
429 |
Budget exceeded | Daily or absolute spend limit reached. Daily rejections include Retry-After until midnight; absolute rejections never reset and omit it |
503 |
All accounts cooling down | Every account for the requested model is currently in cooldown (none exhausted, all just waiting). Includes a Retry-After header — seconds until the earliest cooldown expiry across those accounts |
503 |
All providers exhausted | Every account in the fallback chain was tried in this request and failed. detail contains the last error |
503 — All Accounts Cooling Down¶
Returned before any attempt is made, when resolving targets for a model finds every candidate account already in cooldown:
{"detail": "All accounts for 'openai/gpt-4o' are cooling down; retry after 42s"}
With header: Retry-After: 42
The value is computed from the soonest cooldown expiry among the model's accounts, so clients can back off precisely instead of guessing.
Body-text rate-limit detection¶
Some upstream providers disguise a rate limit as a non-429 status — e.g. an
HTTP 400 with a body like {"error": "quota exceeded"}. Janus scans error
response bodies (dict, list, string, or other JSON shapes) for markers such as
rate limit, too many requests, quota exceeded, capacity, overloaded,
and resource exhausted. A match upgrades the error to a rate limit
regardless of status code: the account is cooled down with rate-limit backoff
(exponential, starting at 2s, capped at 300s) and the request falls back to
the next account — the client never sees the disguised error.
401 — Invalid API Key¶
{"detail": "Invalid API key"}
429 — Budget Exceeded¶
{
"error": {
"message": "Daily budget exceeded. Spent $5.23 of $5.00 limit. Resets at midnight.",
"type": "budget_exceeded",
"today_spend": 5.23,
"daily_limit": 5.0
}
}
With header: Retry-After: 34567
An absolute budget rejection instead returns budget_period: "absolute",
total_spend, absolute_limit, and resets_at: null alongside the same
type: "budget_exceeded". It has no Retry-After header: the lifetime cap must be
increased or removed before the key can make more requests. When both daily and
absolute limits are reached, the absolute rejection takes precedence. See
Budgets for the full response and
accounting limitations.
503 — All Providers Exhausted¶
{"detail": "All providers exhausted: openai-personal: 429"}
Inventory¶
POST /dashboard/api/inventory/push¶
Programmatically ingest upstream API keys. Requires INVENTORY_PUSH_TOKEN as
Bearer auth. See Key Inventory — Push API for request
format and rate limits.