Call Vantulo from your own apps with a secret key. The same key works with OpenAI-style chat completions and with the Anthropic Messages API.
Sign in, open API keys in the sidebar, and create a key. The full secret (vnt_…) is shown once. Revoking a key stops it immediately.
Send the secret on every request:
Authorization: Bearer vnt_YOUR_KEY
The x-api-key header works the same way. Key management (GET and POST /api/keys, DELETE /api/keys/:id) uses your browser session, not the secret.
Replace the host with wherever this server is running. The examples use http://localhost:3999.
http://localhost:3999/v1
POST /v1/chat/completions. Point an OpenAI SDK at http://localhost:3999/v1 (the /v1 is part of this base URL).
curl http://localhost:3999/v1/chat/completions \
-H "Authorization: Bearer vnt_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"vantulo","messages":[{"role":"user","content":"Hello"}]}'
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:3999/v1",
api_key="vnt_YOUR_KEY",
)
reply = client.chat.completions.create(
model="vantulo",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "http://localhost:3999/v1",
apiKey: "vnt_YOUR_KEY",
});
const reply = await client.chat.completions.create({
model: "vantulo",
messages: [{ role: "user", content: "Hello" }],
});
A successful response:
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1750000000,
"model": "vantulo",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hello." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 20,
"completion_tokens": 4,
"total_tokens": 24
}
}
POST /v1/messages. Point an Anthropic SDK at http://localhost:3999 with no /v1 on the end — the client adds /v1/messages itself. Send x-api-key and anthropic-version: 2023-06-01. Authorization: Bearer is also accepted.
curl http://localhost:3999/v1/messages \
-H "x-api-key: vnt_YOUR_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"vantulo","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'
import anthropic
client = anthropic.Anthropic(
base_url="http://localhost:3999",
api_key="vnt_YOUR_KEY",
)
reply = client.messages.create(
model="vantulo",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.content[0].text)
A successful response is a normal Anthropic message. model is the name you sent (vantulo when you omit it).
{
"id": "msg_…",
"type": "message",
"role": "assistant",
"model": "vantulo",
"content": [{ "type": "text", "text": "Hello." }],
"stop_reason": "end_turn",
"usage": { "input_tokens": 20, "output_tokens": 4 }
}
| Field | What it does |
|---|---|
model | vantulo, or vantulo-thinking. Other names are accepted and echoed back. |
max_tokens | Required. Capped by the server’s own ceiling. |
messages | Required. Roles user and assistant. Content is a string or a list of text, thinking, tool use, and tool result blocks. |
system | A string or a list of text blocks. |
tools, tool_choice | Passed through. The model can call the tools you define. |
stop_sequences | Passed through. |
temperature | 0 to 1. Omitted automatically when thinking is on. |
thinking | { "type": "enabled", "budget_tokens": N }. vantulo-thinking turns thinking on for you. |
stream | true returns Anthropic SSE: event: lines and data: lines, including message_start, content deltas, and message_stop. |
Image blocks are supported: send { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "…" } } or a source.type of url. Requests carrying images are answered by a vision-capable model. POST /v1/messages/count_tokens takes the same body and returns { "input_tokens": N }, estimated from the text length. It does not spend the daily request allowance.
Errors use the Anthropic envelope: { "type": "error", "error": { "type", "message" } }. A reply blocked while streaming arrives as event: error.
| Field | What it does |
|---|---|
model | vantulo, or vantulo-thinking to reason before answering. Other names are accepted and echoed back. |
messages | Required. Roles: system, developer, user, assistant. Content is a string or a list of { "type": "text", "text": "…" }. Must include a user message and end with one. No limit on how many (set API_MAX_MESSAGES to cap it). |
stream | true sends server-sent events, then data: [DONE]. |
max_tokens | Cap on the reply. max_completion_tokens is accepted as an alias. The server also has its own ceiling. |
temperature | Number from 0 to 1. Values above 1 are clamped. Ignored when thinking is on. |
stop | A string or up to 4 strings. |
thinking | true turns on extended thinking. max_tokens has to leave room for it (the error message says how much). |
web_search | true lets Vantulo search the web. The answer text is what comes back. |
response_format | Any type other than text asks for raw JSON. |
stream_options.include_usage | On a stream, adds a final chunk with token counts. |
n must be 1. Tool and function calling is not supported. Image and file parts are rejected.
With thinking on, the reply message may include reasoning. While streaming, reasoning arrives as delta.reasoning_content and the answer as delta.content.
curl http://localhost:3999/v1/chat/completions \
-H "Authorization: Bearer vnt_YOUR_KEY" \
-H "Content-Type: application/json" \
-N \
-d '{"model":"vantulo","stream":true,"messages":[{"role":"user","content":"Hello"}]}'
Each event is data: {…}. The first chunk sets the assistant role. Later chunks carry delta.content. The last choice chunk has finish_reason of stop, length, or content_filter.
GET /v1/models requires a key and returns vantulo and vantulo-thinking. GET /v1 is a short index and does not require a key.
Defaults are 60 requests per minute and 500 requests per UTC day, per key. Change them with API_RPM and API_DAILY_LIMIT in .env (0 removes that cap). Responses include X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests, X-RateLimit-Limit-Day, and X-RateLimit-Remaining-Day when those caps are on.
Every call runs on this server’s model account. The operator pays for it. Turn the API off with API_ENABLED=false.
The same guard that screens chat screens API input, system instructions, tool definitions, and the reply (including tool-call JSON) as it is generated. A blocked OpenAI-style request returns HTTP 400:
{
"error": {
"message": "Vantulo's safeguards flagged this request as possible harmful or unsafe content, so it can't respond.",
"type": "content_filter",
"code": "content_filter"
}
}
If a reply is blocked mid-stream, already-sent text stops and finish_reason is content_filter. A non-streaming call returns the 400 above and does not include the reply.
| HTTP | code | When |
|---|---|---|
| 400 | content_filter | Safety guard blocked the request or the reply. |
| 400 | invalid_request_error | Bad JSON, missing messages, tools, or n other than 1. |
| 401 | missing_api_key / invalid_api_key / revoked_api_key | Auth failed. |
| 429 | rate_limit / daily_limit | Per-key cap. Retry-After is set. |
| 503 | api_disabled | API_ENABLED is off. |
Browsers may call /v1 from another origin. Responses include Access-Control-Allow-Origin: *. Don’t put a secret key in a public web page.