Build on Kitsune.
OpenAI-compatible chat completions at https://kitsune.cv/v1. One key, every
model, billed per token from your balance. Streaming, tools, vision and reasoning follow
the standard schema.
https://kitsune.cv/v1 Quickstart
from zero to a streamed answer
Create a key
Sign in, open Console → API keys, and create a key. It
starts with kts-, is shown once, and can carry a spend cap and an
expiry date. Revoking it takes effect immediately.
Send a request
No SDK required. Send JSON over HTTPS with the key as a Bearer token.
curl https://kitsune.cv/v1/chat/completions \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-m3.1-flash",
"messages": [
{"role": "user", "content": "Explain HTTP caching in two sentences."}
]
}' base https://kitsune.cv/v1 · Authorization: Bearer kts-…
Raw HTTP in every language — no SDK package is required.
Stream tokens
Set stream: true to receive server-sent events. Add stream_options.include_usage to get a final usage object.
curl -N https://kitsune.cv/v1/chat/completions \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-m3.1-flash",
"messages": [{"role": "user", "content": "Count to five."}],
"stream": true,
"stream_options": {"include_usage": true}
}' The stream ends with data: [DONE].
Install
wire up a coding agent in one command
The installer asks for your key, reads the catalogue, and writes each agent's config with every model your key can use. It only touches files in your home directory, and it backs up anything it rewrites.
curl -fsSL https://kitsune.cv/install | bash Run it from a terminal — the key prompt needs one.
Paste a key
It is checked against /v1/models before anything is written, so a
revoked or out-of-credit key fails immediately. Create one at Console → API keys.
Pick the agents
Agents already on the machine are pre-selected. OpenCode, [CC], Codex CLI, Aider
and Zed are configured for you; env writes ~/.config/kitsune/env for anything else that reads OPENAI_BASE_URL and OPENAI_API_KEY.
Pick a default model
Every model in the catalogue is added; the one you choose becomes the default. Switch at any time from the agent's own model picker.
Flags: --agent, --all, --list, --yes, --dry-run, --force. An agent already pointed at a different
endpoint is skipped until you pass --force.
Authentication
bearer tokens, scoped in the console
Every request needs an Authorization: Bearer header. Keys are created in
the console, can carry a spend cap and expiry, and are revoked instantly. Legacy sk-dr-… keys created before the rename still authenticate.
- Header
Authorization: Bearer kts-…- Key format
kts-+ 48 hexadecimal characters- Billing
- One rail — pay as you go, charged from the account balance.
- CORS
- No CORS headers are sent. Call the API from a server, not from another origin in a browser.
Models & pricing
usd per 1m tokens · live catalog
Prices and capabilities come from the live catalog. A model with no cache price bills
cached prompt tokens at the input price — the cache column reads = input.
| model | input | cache | output | context | max out | capabilities |
|---|---|---|---|---|---|---|
MiniMax M3.1 Flash minimax-m3.1-flash | $0.05 | $0.004 | $0.19 | 1.0M | 500k | vision thinking tools |
DeepSeek V4.1 Flash 1017 deepseek-v4.1-flash-1017 | $0.09 | $0.004 | $0.19 | 1.0M | 128k | vision thinking tools |
DeepSeek V4.1 Flash 1017 Abliterated deepseek-v4.1-flash-1017-abliterated | $0.20 | $0.009 | $0.45 | 1.0M | 128k | vision thinking tools |
Get the same list programmatically from GET /v1/catalog (public) or GET /v1/models (key-scoped).
/v1/models only returns models the key can use; a granted account may also see a free_quota object with the remaining monthly input/output token allowance.Chat completions
POST /v1/chat/completions
Send model and messages. A message has a role and content: system, developer, user, assistant or tool. Content is a string or
an array of content parts for images.
Response
The assistant reply is in choices[0].message.content. Usage is reported by
the model and billed for that turn.
{
"id": "chatcmpl-7f2c1b1e",
"object": "chat.completion",
"created": 1762300000,
"model": "minimax-m3.1-flash",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "HTTP caching stores responses so later requests can reuse them."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 21,
"completion_tokens": 38,
"total_tokens": 59,
"prompt_tokens_details": { "cached_tokens": 0 }
}
} Streaming
server-sent events
Set stream: true. Content arrives in choices[0].delta.content,
thinking in delta.reasoning_content, and tool calls as indexed fragments
in delta.tool_calls. The stream ends with data: [DONE].
data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"HTTP "},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{"content":"caching "},"finish_reason":null}]}
data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"choices":[],"usage":{"prompt_tokens":21,"completion_tokens":38,"total_tokens":59}}
data: [DONE] stream_options.include_usage. Without it, billing still uses the token counts reported for the turn.Thinking level
reasoning_effort · model dependent
Send reasoning_effort: "low" | "medium" | "high" | "max". The gateway forwards it
unchanged; max asks for the deepest budget the model offers, and the model decides
how much to think. Reasoning text comes back as message.reasoning_content — some models use reasoning or thinking, and those fields pass through untouched.
curl https://kitsune.cv/v1/chat/completions \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-m3.1-flash",
"messages": [
{"role": "user", "content": "Design a retry strategy for a payment API."}
],
"reasoning_effort": "high"
}' Thinking tokens are billed as completion tokens reported for the turn.
Whether a model actually reasons is shown by the thinking badge in the model table above and on Live models.
Tool calling
proxy, not an executor
Kitsune forwards tools, tool_choice and parallel_tool_calls, then returns the model's tool_calls. Your
code executes the function and sends the result back as a role:"tool" message with the same tool_call_id before calling again.
curl https://kitsune.cv/v1/chat/completions \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-m3.1-flash",
"messages": [{"role": "user", "content": "What is the time in Tashkent?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_time",
"description": "Return the current time for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": false
}
}
}],
"tool_choice": "auto",
"parallel_tool_calls": true
}' // 1. assistant returns a call
{ "role": "assistant", "content": null, "tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_time", "arguments": "{\"city\":\"Tashkent\"}" }
}] }
// 2. you run it, then append the result
{ "role": "tool", "tool_call_id": "call_abc123",
"content": "{\"time\":\"2026-11-03T20:30:00Z\"}" }
// 3. call again with the same tools -> final assistant content The complete multi-turn loop in curl, JavaScript and Python is in the cookbook.
Vision & structured output
content parts · response_format
Use a model with the vision badge. Pass content as an array of text and image_url parts —
HTTPS URLs and base64 data URLs are both accepted, model permitting.
curl https://kitsune.cv/v1/chat/completions \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-m3.1-flash",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "https://example.com/chart.png"}}
]
}]
}' Switch tabs for JSON-schema structured output.
response_format also accepts json_object for loose JSON. For
strict schemas, use json_schema with strict: true where the
model supports it.
System prompt & time
applied by the gateway
Kitsune prepends the admin main system prompt and the model's own
prompt, keeps your role:"system" message, then appends the current UTC time
when the model has time context enabled. The time goes at the end so the stable prefix
stays cacheable.
system: <main prompt>
<model prompt>
<your system message>
Current UTC time: 2026-11-03 20:30:00 UTC
user: Explain HTTP caching... The injected text counts as prompt tokens and is billed normally.
Billing & cache
pay as you go · sub-cent accrual
Each successful request is priced from the model row at the time of the call:
cost = uncached_input × input_price
+ cached_input × cache_price
+ output_tokens × output_price - When a model has no cache price, cached prompt tokens bill at the input price.
- Cache hits are reported as
prompt_tokens_details.cached_tokensorcache_read_input_tokens; both are counted and never exceed the prompt. - Sub-cent amounts accrue on the account and are charged in whole cents as they cross the threshold.
- Failed calls are not billed. Aborted streams are billed for the usage reported for the turn.
- An empty balance returns
402 insufficient_balancebefore the request is sent.
Response headers
x-kitsune-billing: payg
content-type: application/json x-kitsune-billing reports the billing mode used for the request.
Failover
ordered route chain
A model can carry more than one route. Where a second route is wired, the first healthy
one takes the request and a retryable failure moves to the next — transport errors, 401/402/403/408/409/425/429 and 5xx. A failing route is rested
with an exponential cooldown.
Errors
json body · stable codes
Errors use a single envelope. Branch on error.code, not the message.
{
"error": {
"message": "balance is empty — top up to keep using the API",
"type": "gateway_error",
"code": "insufficient_balance"
}
} | status | code | meaning |
|---|---|---|
| 400 | bad_request | the body or model id is invalid |
| 401 | unauthorized | missing or revoked key |
| 402 | insufficient_balance | top up to continue |
| 403 | key_scope | the key cannot use pay-as-you-go |
| 403 | model_not_allowed | restricted model and the account has no grant |
| 404 | model_not_found | see /v1/models |
| 404 | not_found | unknown key id |
| 413 | payload_too_large | request body is above 25 MB |
| 429 | rate_limited | more than 600 requests/min for the account; wait and retry |
| 500 | internal | unexpected gateway error |
| 503 | no_route | every route in the chain failed |
Endpoints
everything the gateway exposes
| method | path | auth | notes |
|---|---|---|---|
| POST | /v1/chat/completions | Bearer | chat, streaming, tools, vision, reasoning |
| GET | /v1/models | Bearer | models available to the key, cache prices included |
| GET | /v1/models/{id} | Bearer | one model |
| GET | /v1/catalog | public | price list, display names, capabilities |
| GET | /v1/account | Bearer | balance, accrued, key count, models |
| GET | /v1/plan | Bearer | alias of /v1/account |
| GET | /v1/usage?days=30 | Bearer | daily, per-model and per-key usage |
| GET | /v1/keys | Bearer | list keys |
| POST | /v1/keys | Bearer | create a key; the secret is returned once |
| PATCH | /v1/keys/{id} | Bearer | label, spend cap |
| DELETE | /v1/keys/{id} | Bearer | revoke; send Content-Type: application/json |
Account, usage & keys
The same key manages the account it belongs to. Key creation returns the secret once — store it before leaving the response.
curl https://kitsune.cv/v1/account \
-H "Authorization: Bearer $KITSUNE_KEY"
curl "https://kitsune.cv/v1/usage?days=30" \
-H "Authorization: Bearer $KITSUNE_KEY"
curl https://kitsune.cv/v1/keys \
-H "Authorization: Bearer $KITSUNE_KEY"
# create a key (secret is returned once)
curl https://kitsune.cv/v1/keys \
-H "Authorization: Bearer $KITSUNE_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"ci","spend_cap_cents":500,"expires_days":90}' Request parameters
OpenAI schema + pass-through
| field | type | notes |
|---|---|---|
model | string | public id from the catalog |
messages | array | system, developer, user, assistant, tool; content string or parts |
stream | bool | SSE; ends with data: [DONE] |
stream_options | object | {"include_usage":true} adds a final usage chunk |
temperature · top_p | number | sampling; forwarded to the model |
max_tokens · max_completion_tokens | int | output cap; either alias is forwarded |
stop · n · seed | mixed | stop sequences, choice count, best-effort determinism |
presence_penalty · frequency_penalty · logit_bias | mixed | sampling controls; forwarded |
logprobs · top_logprobs | bool / int | token log probabilities |
tools · tool_choice | array / string | JSON-Schema function calling; auto, none, required, or a named function |
parallel_tool_calls | bool | allow several tool calls in one turn |
response_format | object | text, json_object, json_schema |
reasoning_effort | string | low · medium · high · max; model-dependent |
user · metadata · store · service_tier | mixed | forwarded unchanged; support depends on the model |
Any other JSON field is forwarded unchanged. Kitsune does not execute tools, browse, or store prompts. The complete parameter table and every request example live in the cookbook.
Full cookbook
copy, paste, call
Every feature, every example.
Basic chat, streaming, thinking, the complete tool-result loop, parallel tools, vision, structured output, sampling, response schema, headers and HTTP setup — with the full JSON responses.