Copy. Paste. Call.
Full request and response examples for every supported surface: chat, SSE, thinking, tool calling (including the tool-result loop), parallel tools, vision, structured outputs, sampling, system prompts and the account endpoints.
JavaScript, Python and PHP examples use the small api() helper from HTTP setup; cURL and PHP use the raw endpoints directly.
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-…
Response:
{
"id": "chatcmpl-7f2c1b1e",
"object": "chat.completion",
"created": 1762300000,
"model": "minimax-m3.1-flash",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "HTTP caching stores responses..." },
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 21,
"completion_tokens": 38,
"total_tokens": 59,
"prompt_tokens_details": { "cached_tokens": 0 }
}
}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}
}' Set stream_options.include_usage to receive a final usage chunk.
Raw stream:
data: {"id":"chatcmpl-7f2c1b1e","object":"chat.completion.chunk","created":1762300000,"model":"minimax-m3.1-flash","choices":[{"index":0,"delta":{"role":"assistant","content":"HTTP "},"finish_reason":null}]}
data: {"id":"chatcmpl-7f2c1b1e","object":"chat.completion.chunk","created":1762300000,"model":"minimax-m3.1-flash","choices":[{"index":0,"delta":{"content":"caching "},"finish_reason":null}]}
data: {"id":"chatcmpl-7f2c1b1e","object":"chat.completion.chunk","created":1762300000,"model":"minimax-m3.1-flash","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-7f2c1b1e","object":"chat.completion.chunk","created":1762300000,"model":"minimax-m3.1-flash","choices":[],"usage":{"prompt_tokens":21,"completion_tokens":38,"total_tokens":59}}
data: [DONE]Accumulate choices[0].delta.content; tool calls arrive as indexed fragments in delta.tool_calls.
Send reasoning_effort: "low" | "medium" | "high" | "max". The gateway forwards it; max asks for the deepest budget the model offers, and the
model decides how much to think. The reasoning text is returned as message.reasoning_content (some models use reasoning or thinking — those fields are passed through unchanged). In streaming it arrives as delta.reasoning_content before the answer.
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 is billed as completion tokens reported for the turn.
Response fields:
{
"id": "chatcmpl-8a1d",
"object": "chat.completion",
"model": "minimax-m3.1-flash",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"reasoning_content": "The caller should retry idempotent requests with exponential backoff...",
"content": "Use idempotency keys, exponential backoff with jitter..."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 120,
"total_tokens": 138,
"completion_tokens_details": { "reasoning_tokens": 64 }
}
}Kitsune is a proxy: it forwards tools, tool_choice and parallel_tool_calls and returns the model's tool_calls.
Your code executes the function, then sends the result back as a role:"tool" message.
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", "description": "City name"}},
"required": ["city"],
"additionalProperties": false
}
}
}],
"tool_choice": "auto"
}' Use strict JSON Schema; the model returns arguments as a JSON string.
First response — no content, tool calls instead:
{
"id": "chatcmpl-9b2e",
"object": "chat.completion",
"model": "minimax-m3.1-flash",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_time",
"arguments": "{\"city\":\"Tashkent\"}"
}
}]
},
"finish_reason": "tool_calls"
}],
"usage": { "prompt_tokens": 42, "completion_tokens": 18, "total_tokens": 60 }
}Then append this message and call again:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"time\":\"2026-11-03T20:30:00Z\",\"timezone\":\"Asia/Tashkent\"}"
}Set parallel_tool_calls: true and the model may return several tool_calls in one message. Execute them (in parallel if you like), then append one role:"tool" message per tool_call_id before the next call.
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": "Check the time in Tashkent and Warsaw."}],
"tools": [/* get_time, get_weather */],
"tool_choice": "auto",
"parallel_tool_calls": true
}' Use a model with capabilities.vision. Pass content as an
array of text and image_url parts. Both HTTPS URLs and
base64 data URLs are accepted (model-dependent).
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"}}
]
}]
}' Send response_format. json_schema with strict: true is the reliable form where the model supports it.
const API_KEY = process.env.KITSUNE_KEY;
const BASE = 'https://kitsune.cv/v1';
async function api(path, body) {
const res = await fetch(BASE + path, {
method: body ? 'POST' : 'GET',
headers: {
authorization: 'Bearer ' + API_KEY,
'content-type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
const completion = await api('/chat/completions', {
model: 'minimax-m3.1-flash',
messages: [{ role: 'user', content: 'Extract the invoice as JSON.' }],
response_format: {
type: 'json_schema',
json_schema: {
name: 'invoice',
strict: true,
schema: {
type: 'object',
properties: {
vendor: { type: 'string' },
total: { type: 'number' },
currency: { type: 'string' }
},
required: ['vendor', 'total', 'currency'],
additionalProperties: false
}
}
}
});
const invoice = JSON.parse(completion.choices[0].message.content);
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": "Write a product name."}],
"temperature": 0.9,
"top_p": 0.95,
"max_tokens": 64,
"stop": ["\n\n"],
"presence_penalty": 0.2,
"frequency_penalty": 0.1,
"seed": 42,
"n": 1
}' Unknown/extra fields are forwarded unchanged. Support varies by model.
temperature · top_p · max_tokens · max_completion_tokens · stop · n · presence_penalty · frequency_penalty · logit_bias · logprobs · top_logprobs · seed · user · metadata · store · service_tier.
Kitsune injects the admin main system prompt and the model's own system prompt before your
messages, then appends the current UTC time (if the model has time context enabled). The
time is appended at the end so the stable prompt prefix can still be cached. Your own role:"system" message is preserved after the injected text.
What the model receives (simplified):
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.
curl https://kitsune.cv/v1/models \
-H "Authorization: Bearer $KITSUNE_KEY"
curl https://kitsune.cv/v1/models/minimax-m3.1-flash \
-H "Authorization: Bearer $KITSUNE_KEY"
# public catalog, no key:
curl https://kitsune.cv/v1/catalog | method | path | auth | notes |
|---|---|---|---|
| POST | /v1/chat/completions | Bearer | chat, streaming, tools, vision, reasoning |
| GET | /v1/models | Bearer | models available to the key |
| GET | /v1/models/{id} | Bearer | one model |
| GET | /v1/catalog | public | prices, display names, capabilities |
| GET | /v1/account | Bearer | balance, accrued, keys, models |
| GET | /v1/plan | Bearer | alias of /v1/account |
| GET | /v1/usage?days=30 | Bearer | daily, per-model, per-key usage |
| GET | /v1/keys | Bearer | list keys |
| POST | /v1/keys | Bearer | create; secret returned once |
| PATCH | /v1/keys/{id} | Bearer | label / spend cap |
| DELETE | /v1/keys/{id} | Bearer | revoke; send JSON content-type |
{
"error": {
"message": "balance is empty — top up to keep using the API",
"type": "gateway_error",
"code": "insufficient_balance"
}
}| status | code | meaning |
|---|---|---|
| 400 | bad_request | invalid body, model id, key request |
| 401 | unauthorized | missing / revoked key |
| 402 | insufficient_balance | top up |
| 403 | key_scope | key cannot use pay-as-you-go |
| 403 | model_not_allowed | restricted model, no grant |
| 404 | model_not_found | not in /v1/models |
| 404 | not_found | unknown key id |
| 503 | no_route | every route failed |
| field | type | default | notes |
|---|---|---|---|
model | string | — | public id from /v1/models |
messages | array | — | system, developer, user, assistant, tool; content string or parts |
stream | bool | false | SSE; ends with data: [DONE] |
stream_options | object | — | {"include_usage":true} adds a final usage chunk |
temperature | number | model | 0–2 |
top_p | number | model | nucleus sampling |
max_tokens | int | model | output cap |
max_completion_tokens | int | model | newer alias; forwarded |
stop | string / array | — | stop sequence(s) |
n | int | 1 | number of choices |
presence_penalty | number | 0 | −2 to 2 |
frequency_penalty | number | 0 | −2 to 2 |
logit_bias | object | — | token id → bias |
logprobs | bool | false | return log probabilities |
top_logprobs | int | — | 0–20 with logprobs |
seed | int | — | best-effort determinism |
tools | array | — | JSON-Schema function tools |
tool_choice | string / object | auto | auto, none, required, or a named function |
parallel_tool_calls | bool | model | allow multiple tool calls per turn |
response_format | object | text | text, json_object, json_schema |
reasoning_effort | string | model | low · medium · high · max |
user | string | — | end-user identifier; forwarded |
metadata | object | — | forwarded |
store | bool | — | forwarded; model-dependent |
service_tier | string | — | forwarded; model-dependent |
Any other JSON field is forwarded unchanged. Kitsune itself does not execute tools, browse, or store prompts.
| field | type | notes |
|---|---|---|
id | string | completion id |
object | string | chat.completion or chat.completion.chunk |
model | string | Kitsune public model id |
choices[].index | int | choice position |
choices[].message.role | string | assistant |
choices[].message.content | string / null | answer text; null when a tool call is returned |
choices[].message.reasoning_content | string | thinking text when the model returns it |
choices[].message.tool_calls | array | id, type, function.name, function.arguments (JSON string) |
choices[].finish_reason | string | stop, length, tool_calls, content_filter |
usage.prompt_tokens | int | input tokens, including cached and injected system text |
usage.completion_tokens | int | output tokens, including thinking tokens where reported |
usage.total_tokens | int | sum |
usage.prompt_tokens_details.cached_tokens | int | cache hits; billed at the cache price |
usage.completion_tokens_details.reasoning_tokens | int | reported thinking tokens (optional) |
system_fingerprint | string | fingerprint when supplied |
Streaming: choices[0].delta carries content, reasoning_content and indexed tool_calls; a final usage chunk appears when stream_options.include_usage is set.
| header | value | meaning |
|---|---|---|
x-kitsune-billing | payg | billing rail |
retry-after | seconds | present when a rate limit is surfaced |
No CORS headers are sent. Use the API from a server or the Kitsune console.
No SDK is required. Any HTTP client works: send Authorization: Bearer kts-… and a JSON body to https://kitsune.cv/v1. The examples below use fetch, requests, curl and PHP’s cURL extension — no OpenAI package in any language.
curl https://kitsune.cv/v1/models \
-H "Authorization: Bearer $KITSUNE_KEY" All examples target the same OpenAI-compatible schema over plain HTTPS.
Runtime notes: JavaScript uses the built-in fetch; Python examples use requests (pip install requests); PHP examples need the curl extension. No OpenAI package is used anywhere.
- • The gateway allows 600 requests/min per account. Above that it returns
429 rate_limited; wait for the window to reset before retrying. Balance, key scope and spend caps are enforced on top. - • Request bodies are capped at 25 MB (including base64 images); larger requests return
413. Prompts should stay within the model's context window. - • A
429may also come from the model. Where another route is wired the gateway may fail over, otherwise the status is surfaced. Back off and retry. - • A zero balance returns
402before the request is sent. Failed calls are not billed. - • Aborted streams are billed for the usage reported by the service.
- • Prompt and completion bodies are not stored — zero data retention. Request metadata is kept for billing and the usage page.