Base URL: http://127.0.0.1:8080 (configurable via octohub.toml)
OctoHub has two authentication layers, both controlled by the api_key setting in octohub.toml.
If api_key is not set (default): the server starts without authentication. All client endpoints are open, admin endpoints are disabled. A warning is printed at startup.
If api_key is set: full authentication is enforced on both layers.
Authenticated with a client API key issued via the admin API. Pass it as a Bearer token:
Authorization: Bearer <client-api-key>
Authenticated with the master key set in octohub.toml (api_key):
Authorization: Bearer <master-key>
All requests to admin endpoints without a valid master key return 401 Unauthorized. Admin endpoints are unavailable when no master key is configured.
All errors return JSON:
{
"error": {
"message": "Description of what went wrong",
"type": "invalid_request_error"
}
}OctoHub exposes two completion APIs. Use whichever fits your client:
| Endpoint | Style | Best for |
|---|---|---|
POST /v1/completions |
OctoHub Responses API | Octomind and native OctoHub clients. Multi-turn chains, reasoning replay, richer output shape. |
POST /v1/chat/completions |
Classic OpenAI Chat Completions | Any OpenAI-compatible SDK or tool (LangChain, LiteLLM, curl with openai libs, etc.). Drop-in replacement. |
POST /v1/embeddings |
OpenAI-compatible | Embedding clients. |
Both completion endpoints hit the same proxy engine and write to the same completions table with the same id, auth, metrics, and logging. The only difference is the wire format.
Note:
POST /v1/chat/completionsdoes not support streaming ("stream": true). Requests with streaming enabled receive501 Not Implemented.
OctoHub's native Responses API. Supports multi-turn conversation chains, reasoning-block replay, and a richer output structure. Used by Octomind.
{
"model": "string",
"input": "string | InputItem[]",
"instructions": "string | null",
"previous_completion_id": "string | null",
"temperature": 1.0,
"max_output_tokens": 0,
"tools": "ToolDefinition[] | null"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
model |
string | ✅ | — | Model identifier. Use "provider:model" format (e.g. "openai:gpt-4o") or a mapped name from config. |
input |
string | array | ✅ | — | Input content. A plain string becomes a user message. An array allows typed items (see below). |
instructions |
string | — | null |
System instructions prepended to the conversation. |
previous_completion_id |
string | — | null |
Chain to a previous completion for multi-turn conversations. OctoHub reconstructs the full history automatically. |
temperature |
float | — | 1.0 |
Sampling temperature (0.0–2.0). |
max_output_tokens |
integer | — | 0 |
Maximum tokens in the response. 0 = provider default. |
tools |
array | — | null |
Function definitions for tool/function calling. |
When input is an array, each element is a tagged object:
Message:
{"type": "message", "role": "user", "content": "What is Rust?"}Function call output (tool result, requires previous_completion_id):
{"type": "function_call_output", "call_id": "call_abc123", "output": "72°F sunny"}{
"type": "function",
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
}
}
}{
"id": "cmpl_<uuid>",
"object": "completion",
"output": [OutputItem],
"usage": Usage,
"created_at": 1700000000
}Message:
{
"type": "message",
"id": "msg_<uuid>",
"role": "assistant",
"content": [
{"type": "output_text", "text": "Hello!"}
]
}Function call:
{
"type": "function_call",
"id": "fc_<uuid>",
"call_id": "call_abc123",
"name": "get_weather",
"arguments": "{\"location\":\"NYC\"}"
}{
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cache_read_tokens": null,
"cache_write_tokens": null,
"cost": 0.0001,
"request_time_ms": 500
}cache_read_tokens, cache_write_tokens, cost, and request_time_ms are omitted when null.
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": "Explain Rust in one sentence."
}'curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": "What should I learn first?",
"instructions": "You are a programming tutor. Be concise.",
"temperature": 0.7
}'# First turn
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": "What is the capital of France?"
}'
# Response: {"id": "cmpl_abc123", ...}
# Second turn — chains automatically
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": "And what is its population?",
"previous_completion_id": "cmpl_abc123"
}'# Step 1: Ask with tools
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": "What is the weather in NYC?",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
]
}'
# Response includes: {"type": "function_call", "call_id": "call_xyz", "name": "get_weather", ...}
# Step 2: Send tool result back
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"input": [
{"type": "function_call_output", "call_id": "call_xyz", "output": "72°F, sunny"}
],
"previous_completion_id": "cmpl_from_step1"
}'With this config:
[models]
"my-model" = ["openai:gpt-4o", "anthropic:claude-sonnet-4-20250514"]curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "my-model",
"input": "Hello!"
}'OctoHub randomly picks one provider from the list for load balancing.
Classic OpenAI Chat Completions API. Compatible with any OpenAI-compatible client or SDK. Internally converts to the same engine call as /v1/completions — all completions are stored in the DB with the same id, auth key, tokens, and provider metadata.
{
"model": "string",
"messages": [{"role": "user", "content": "Hello"}],
"temperature": 1.0,
"top_p": 1.0,
"max_tokens": null,
"stream": false,
"tools": null,
"tool_choice": null
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
model |
string | ✅ | — | Model identifier — same as /v1/completions. |
messages |
array | ✅ | — | Conversation history. Roles: system, user, assistant, tool. |
temperature |
float | — | 1.0 |
Sampling temperature. |
top_p |
float | — | 1.0 |
Nucleus sampling. |
max_tokens |
integer | — | provider default | Maximum output tokens. |
stream |
bool | — | false |
Streaming is not supported — true returns 501. |
tools |
array | — | null |
Classic tool definitions (nested function object). |
tool_choice |
any | — | null |
Accepted and ignored — provider decides. |
Message conversion rules:
role=systemmessages →instructions(concatenated with newline if multiple)role=toolmessages →function_call_output(keyed bytool_call_id)role=assistantwithtool_calls→ onefunction_callinput item per call- All other messages →
messageinput items (content string or parts preserved)
Standard OpenAI Chat Completions response shape:
{
"id": "cmpl_01JXXXXXXXXXX",
"object": "chat.completion",
"created": 1749300000,
"model": "openai:gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help?",
"tool_calls": null
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 8,
"total_tokens": 18
}
}idis the same ULID-based completion ID stored in the DB — use it to look up the record via the admin APIfinish_reasonis always a string; falls back to"stop"when the upstream doesn't report one- Tool call responses:
choices[0].message.contentisnull,tool_callsis populated - Reasoning blocks from thinking models are not surfaced in the classic response (invisible to classic clients)
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8080/v1",
api_key="<client-api-key>",
)
response = client.chat.completions.create(
model="openai:gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain Rust in one sentence."},
],
)
print(response.choices[0].message.content)curl -X POST http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"messages": [
{"role": "system", "content": "Be concise."},
{"role": "user", "content": "What is Rust?"}
]
}'# Step 1: ask with tools
curl -X POST http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"messages": [{"role": "user", "content": "What is the weather in NYC?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}
}
]
}'
# Response: choices[0].message.tool_calls[0] = {id, type, function: {name, arguments}}
# Step 2: send result back
curl -X POST http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:gpt-4o",
"messages": [
{"role": "user", "content": "What is the weather in NYC?"},
{"role": "assistant", "tool_calls": [{"id": "call_xyz", "type": "function", "function": {"name": "get_weather", "arguments": "{\"location\":\"NYC\"}"}}]},
{"role": "tool", "tool_call_id": "call_xyz", "content": "72°F, sunny"}
]
}'Generate vector embeddings for text input.
{
"model": "string",
"input": "string | string[]"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
model |
string | ✅ | — | Model identifier. Use "provider:model" format (e.g. "voyage:voyage-3.5") or a mapped name from config. |
input |
string | string[] | ✅ | — | Text(s) to embed. A single string or an array of strings. |
Returns the embedding vector(s) directly:
Single input:
[0.0023, -0.0091, 0.0234, ...]Batch input:
[
[0.0023, -0.0091, ...],
[0.0156, 0.0089, ...],
[0.0034, -0.0123, ...]
]curl -X POST http://127.0.0.1:8080/v1/embeddings \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "voyage:voyage-3.5",
"input": "The quick brown fox jumps over the lazy dog."
}'curl -X POST http://127.0.0.1:8080/v1/embeddings \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "openai:text-embedding-3-small",
"input": [
"First document to embed",
"Second document to embed",
"Third document to embed"
]
}'With this config:
[embedding_models]
"my-embeddings" = ["voyage:voyage-3.5"]curl -X POST http://127.0.0.1:8080/v1/embeddings \
-H "Authorization: Bearer <client-api-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "my-embeddings",
"input": "Embed this text"
}'Health check endpoint. No authentication required.
{"status": "ok"}curl http://127.0.0.1:8080/healthAll admin endpoints require the master key from octohub.toml:
Authorization: Bearer <master-key>
Create a new client API key.
{
"name": "string",
"allowed_models": ["string"],
"owner": "string",
"owner_concurrency": 10
}| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Human-readable label for this key (e.g. "mobile-app", "ci-pipeline"). |
allowed_models |
string[] | — | Whitelist of model values this key may request. Match is exact against the model field as sent — either an alias from [models]/[embedding_models] or a raw provider:model string. Omit the field for an unrestricted key. Requests to a non-allowed model return 403 Forbidden. |
owner |
string | — | Opaque grouping label (e.g. the tenant/account id in YOUR system). All keys sharing an owner share one in-flight request budget — see owner_concurrency. Omit for an ungrouped key. |
owner_concurrency |
int | — | Max in-flight requests (completions + embeddings together) shared by ALL keys with this owner. A saturated budget queues a request up to 30s, then returns 429 Too Many Requests. Omit or 0 = unlimited. |
{
"id": 1,
"name": "mobile-app",
"key": "abc123...xyz",
"key_hint": "...xyz",
"status": "active",
"allowed_models": ["gpt", "openai:gpt-4o"],
"owner": "acct-42",
"owner_concurrency": 10,
"created_at": 1700000000
}Important: The
keyfield is only returned on creation. Store it securely — it cannot be retrieved again.
| Field | Description |
|---|---|
id |
Numeric key ID. Used in admin queries to filter by key. |
name |
Label as provided. |
key |
Full 43-character base64url key. Only shown once. |
key_hint |
Last 4 characters prefixed with ... for identification. |
status |
"active" or "revoked". |
allowed_models |
The model whitelist as configured. null means unrestricted. |
owner |
Grouping label for the shared concurrency budget. null means ungrouped. |
owner_concurrency |
Shared in-flight budget for all keys of this owner. null/0 means unlimited. |
created_at |
Unix timestamp. |
# Unrestricted key — may call any model
curl -X POST http://127.0.0.1:8080/v1/admin/keys \
-H "Authorization: Bearer <master-key>" \
-H "Content-Type: application/json" \
-d '{"name": "mobile-app"}'
# Restricted key — limited to two models
curl -X POST http://127.0.0.1:8080/v1/admin/keys \
-H "Authorization: Bearer <master-key>" \
-H "Content-Type: application/json" \
-d '{"name": "ci-pipeline", "allowed_models": ["gpt", "anthropic:claude-haiku-4-5"]}'List all API keys. The full key value is never returned — only the hint.
{
"data": [
{
"id": 1,
"name": "mobile-app",
"key_hint": "...xyz",
"status": "active",
"allowed_models": null,
"created_at": 1700000000
},
{
"id": 2,
"name": "ci-pipeline",
"key_hint": "...abc",
"status": "revoked",
"allowed_models": ["gpt", "anthropic:claude-haiku-4-5"],
"created_at": 1700001000
}
]
}curl http://127.0.0.1:8080/v1/admin/keys \
-H "Authorization: Bearer <master-key>"Get a single API key by its numeric ID.
{
"id": 1,
"name": "mobile-app",
"key_hint": "...xyz",
"status": "active",
"allowed_models": null,
"created_at": 1700000000
}Returns 404 if the key does not exist.
curl http://127.0.0.1:8080/v1/admin/keys/1 \
-H "Authorization: Bearer <master-key>"Revoke an API key. Revoked keys are rejected on all client endpoints immediately. Keys cannot be deleted — only revoked — because usage records are linked to the key ID.
{"status": "revoked"}Returns 404 if the key does not exist.
curl -X POST http://127.0.0.1:8080/v1/admin/keys/1/revoke \
-H "Authorization: Bearer <master-key>"Replace an active key's allowed_models list in place — the key value stays valid, so deployed credentials keep working while their model access changes. Absent/null = unrestricted.
curl -X POST http://127.0.0.1:8080/v1/admin/keys/1/models \
-H "Authorization: Bearer <master-key>" \
-H "Content-Type: application/json" \
-d '{"allowed_models": ["gpt", "voyage-4"]}'Returns 200 {"status": "updated"}, or 404 if the key does not exist or is not active.
Replace an active key's owner grouping and shared owner_concurrency budget in place (same contract as /models). Set this identically on every key of a tenant so all of them drain one budget — minting more keys never widens concurrency.
curl -X POST http://127.0.0.1:8080/v1/admin/keys/1/owner \
-H "Authorization: Bearer <master-key>" \
-H "Content-Type: application/json" \
-d '{"owner": "acct-42", "owner_concurrency": 10}'Returns 200 {"status": "updated"}, or 404 if the key does not exist or is not active. Absent/null owner ungroups the key; absent/null/0 concurrency means unlimited. A capacity change applies to new requests immediately (requests already in flight drain on the old budget).
Aggregated usage statistics. Optionally grouped by time bucket and filtered by key.
| Parameter | Type | Description |
|---|---|---|
key_id |
string | Comma-separated key IDs to filter by (e.g. key_id=1,3). Omit for all keys. |
bucket |
string | Time grouping: hour, day, week, month. Omit for a single total row per key. |
since |
integer | Unix timestamp — only include records after this time. |
until |
integer | Unix timestamp — only include records before this time. |
{
"data": [
{
"period": 1700000000,
"key_id": 1,
"key_name": "mobile-app",
"completions_count": 42,
"embeddings_count": 10,
"total_input_tokens": 8500,
"total_output_tokens": 3200
}
]
}| Field | Description |
|---|---|
period |
Start of the time bucket as unix timestamp. null when no bucket is specified (total). |
key_id |
API key ID. |
key_name |
API key name at time of query. |
completions_count |
Number of completion requests in this period. |
embeddings_count |
Number of embedding requests in this period. |
total_input_tokens |
Sum of input tokens across all requests. |
total_output_tokens |
Sum of output tokens across completion requests. |
# Total usage across all keys
curl "http://127.0.0.1:8080/v1/admin/usage" \
-H "Authorization: Bearer <master-key>"
# Daily breakdown for key 1 in the last week
curl "http://127.0.0.1:8080/v1/admin/usage?key_id=1&bucket=day&since=1699395200" \
-H "Authorization: Bearer <master-key>"
# Hourly breakdown for keys 1 and 3
curl "http://127.0.0.1:8080/v1/admin/usage?key_id=1,3&bucket=hour" \
-H "Authorization: Bearer <master-key>"List raw completion records with full input/output.
| Parameter | Type | Default | Description |
|---|---|---|---|
key_id |
string | all | Comma-separated key IDs to filter by. |
since |
integer | — | Unix timestamp lower bound. |
until |
integer | — | Unix timestamp upper bound. |
limit |
integer | 100 |
Max records to return. |
offset |
integer | 0 |
Pagination offset. |
{
"data": [
{
"id": "cmpl_<uuid>",
"api_key_id": 1,
"session_id": "<uuid>",
"input_model": "my-model",
"resolved_model": "gpt-4o",
"provider": "openai",
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"total_tokens": 15,
"cost": 0.0001,
"request_time_ms": 320
},
"input": [...],
"output": [...],
"created_at": 1700000000
}
]
}# Last 50 completions for key 2
curl "http://127.0.0.1:8080/v1/admin/completions?key_id=2&limit=50" \
-H "Authorization: Bearer <master-key>"List raw embedding records.
| Parameter | Type | Default | Description |
|---|---|---|---|
key_id |
string | all | Comma-separated key IDs to filter by. |
since |
integer | — | Unix timestamp lower bound. |
until |
integer | — | Unix timestamp upper bound. |
limit |
integer | 100 |
Max records to return. |
offset |
integer | 0 |
Pagination offset. |
{
"data": [
{
"id": "emb_<uuid>",
"api_key_id": 1,
"input_model": "my-embeddings",
"resolved_model": "voyage-3.5",
"provider": "voyage",
"usage": {
"input_tokens": 8,
"total_tokens": 8,
"request_time_ms": 120
},
"input": ["text to embed"],
"created_at": 1700000000
}
]
}curl "http://127.0.0.1:8080/v1/admin/embeddings?key_id=1&limit=20&offset=40" \
-H "Authorization: Bearer <master-key>"Each model alias resolves to a real upstream provider (ollama, openai, anthropic, ...). OctoHub can cap the number of in-flight requests it forwards to each provider. Configure in octohub.toml:
[providers.ollama]
concurrency = 4
[providers.openai]
concurrency = 32Provider names are case-insensitive. Omit a provider to leave it unlimited.
When a provider is at its limit, additional requests queue inside the OctoHub process — the client's HTTP connection stays open and blocks until a slot frees up. This is intentional throttling, not an error; clients see it as a longer-than-usual response, never a 429. The default HTTP/1 keep-alive is disabled at the server, so no stale pooled connection lingers across the wait.
The limiter is process-local. It tracks completions and embeddings together (both flow through the same provider connection).
Use "provider:model" format. Available providers depend on octolib configuration and environment variables (API keys).
| Provider | Format Example |
|---|---|
| OpenAI | openai:gpt-4o |
| Anthropic | anthropic:claude-sonnet-4-20250514 |
google:gemini-2.0-flash |
|
| DeepSeek | deepseek:deepseek-chat |
| MiniMax | minimax:minimax-m2.7 |
| Ollama | ollama:llama3 |
| OpenRouter | openrouter:meta-llama/llama-3-70b |
| Provider | Format Example |
|---|---|
| Voyage | voyage:voyage-3.5 |
| OpenAI | openai:text-embedding-3-small |
| Jina | jina:jina-embeddings-v3 |
google:gemini-embedding-001 |
|
| OpenRouter | openrouter:openai/text-embedding-3-small |
# 1. Start the server (api_key must be set in octohub.toml)
./octohub
# 2. Create a client API key
curl -X POST http://127.0.0.1:8080/v1/admin/keys \
-H "Authorization: Bearer <master-key>" \
-H "Content-Type: application/json" \
-d '{"name": "my-app"}'
# → {"id": 1, "key": "abc...xyz", ...} ← save this key!
# 3. Use the client key for completions
curl -X POST http://127.0.0.1:8080/v1/completions \
-H "Authorization: Bearer abc...xyz" \
-H "Content-Type: application/json" \
-d '{"model": "openai:gpt-4o", "input": "Hello!"}'
# 4. Check usage
curl "http://127.0.0.1:8080/v1/admin/usage?key_id=1&bucket=day" \
-H "Authorization: Bearer <master-key>"All errors follow this format:
{
"error": {
"message": "Description of what went wrong",
"type": "invalid_request_error"
}
}| HTTP Status | Meaning |
|---|---|
400 |
Bad request (missing/invalid fields, bad model name) |
401 |
Missing or invalid API key |
403 |
The API key is not permitted to use the requested model (see allowed_models) |
404 |
Unknown endpoint |
429 |
The key's shared owner concurrency budget stayed saturated for the 30s queue window — reduce parallelism and retry |
500 |
Internal error (provider failure, storage error) |