Skip to content

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_keys section of config.yaml (always full access)
  • DB-managed — created via janus keys create or the dashboard

DB keys support scopes:

  • Dashboard logincan_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 return 403 with error.type = "model_not_allowed". GET /v1/models is filtered the same way.
  • Daily budget — optional per-key spend limit (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 spend limit reached. Includes a Retry-After header (seconds until midnight reset)
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

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.