kuiper Docs

Agent services

Models

Call OpenAI, Anthropic, Google, Kimi, GLM, MiniMax and more through one gateway with your own provider keys, each provider in its own API. Kuiper routes, limits and records every call, can keep what each call said, and adds no charge.

On this page

One gateway for your own keys#

Kuiper's model gateway serves each provider's own API at one address: OpenAI's, Anthropic's and Google's Gemini API. Glove and the providers' own SDKs work as they are: you change their base URL and their key. You bring the provider keys. Kuiper sends each call to the right one, keeps the keys out of your units, limits and records every call, and shows you the tokens and what each call will cost you. Turn logging on and it keeps what each call said too: prompts, tool calls and their results, reasoning, images and audio. Kuiper adds no charge: your providers bill you directly, as they do today.

The gateway is at https://gateway.kuiper.sh/models and takes a gateway key (kgw_…) that includes the models service.

Add a provider key#

Provider keys belong to the organisation. Owners and admins add them in the dashboard, under Access, then Providers, or with the CLI. --key - reads the key from standard input, so it stays out of your shell's history:

Terminal
kuiper providers add openai --key - < openai.key
kuiper providers add anthropic --key -
kuiper providers add kimi --field region=china --key -
kuiper providers add bedrock --field region=us-east-1 --key -
kuiper providers add custom --name my-vllm \
  --field base_url=https://llm.example.com/v1 --projects proxima --key -
kuiper providers test openai
kuiper providers list --service models
kuiper providers kinds --service models
  • The kind is the provider: openai, kimi, custom. The name (openai, my-vllm) is the prefix of the key's model IDs, as in openai/gpt-5 or my-vllm/meta-llama/Llama-3.3-70B. It defaults to the kind; a custom key needs one.
  • Projects. A key is shared with every project unless --projects names some. kuiper providers projects NAME a,b or --all changes it.
  • Write-only. Kuiper shows a key as its last four characters, never again. kuiper providers set NAME --key - replaces it and kuiper providers rm NAME removes it.
  • Status. test lists the provider's models with the key and says ok, invalid (the provider refused the key) or unreachable. A call the provider answers with 401 marks the key invalid as well.

Members see the list but can't change it.

Kinds#

KindProviderSettings
openaiOpenAI
anthropicAnthropic
googleGoogle (the Gemini API)
openrouterOpenRouter
vercelVercel AI Gateway
togetherTogether AI
fireworksFireworks AI
groqGroq
mistralMistral
deepseekDeepSeek
xaixAI
basetenBaseten
kimiKimi (Moonshot AI)region: international (api.moonshot.ai, the default) or china (api.moonshot.cn)
glmGLM (Z.ai, Zhipu)region: international (Z.ai, the default) or china (Zhipu, open.bigmodel.cn)
minimaxMiniMaxregion: international (api.minimax.io, the default) or china (api.minimax.cn)
mimoXiaomi MiMo
bedrockAmazon Bedrockregion: the AWS Region the key works in, such as us-east-1
customCustom endpointbase_url: where its API is, such as https://llm.example.com/v1

A key works on the platform it was made on: a China key needs region=china.

Amazon Bedrock takes a Bedrock API key, not AWS access keys. Kuiper calls Bedrock's Chat Completions, Responses and Messages APIs in the key's Region, with Bedrock's model IDs: bedrock/openai.gpt-oss-120b-1:0, bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0.

A custom endpoint is a server of your own, such as vLLM, SGLang or an internal gateway. Kuiper sends it what it would send a provider, under its base URL: /chat/completions, /responses, /embeddings, /messages and the rest. The base URL must be https:// on a public address: Kuiper never calls into private networks. A model on your own machine, in Ollama or LM Studio, needs a public HTTPS address in front of it, such as a tunnel.

Point Glove or an SDK at Kuiper#

From a unit on Kuiper#

Point your variables at the built-in models resource. Each running copy of the unit gets its own key and its region's gateway:

Terminal
kuiper env set -p proxima -u agent \
  'MODELS_BASE_URL=${{models.BASE_URL}}' \
  'ANTHROPIC_BASE_URL=${{models.ANTHROPIC_BASE_URL}}' \
  'GEMINI_BASE_URL=${{models.GEMINI_BASE_URL}}' \
  'MODELS_API_KEY=${{models.API_KEY}}'
kuiper restart -p proxima -u agent
OutputValue
models.BASE_URLhttps://gateway.kuiper.sh/models/v1, or your region's gateway. The base URL for Glove's providers (all but anthropic) and OpenAI's SDK
models.ANTHROPIC_BASE_URLhttps://gateway.kuiper.sh/models. The base URL for Glove's anthropic provider and Anthropic's SDK, which add /v1/messages
models.GEMINI_BASE_URLhttps://gateway.kuiper.sh/models. The base URL for Google's Gen AI SDK, which adds /v1beta/models/…
models.API_KEYA key made for this running copy (secret)

Your provider keys never reach the unit. The unit's key is revoked when that copy stops, so a copy left on a lost machine can't call models.

From anywhere else#

Make a key for the project:

Terminal
kuiper keys create laptop -p proxima --services models
export MODELS_BASE_URL=https://gateway.kuiper.sh/models/v1
export MODELS_API_KEY=kgw_…

The gateway reads the key from Authorization: Bearer, x-api-key (what Anthropic's SDK sends), x-goog-api-key or ?key= (what Google's SDK sends).

Glove#

createAdapter takes a baseURL and an apiKey. Keep the provider and model you use, and give it the gateway and your gateway key:

TypeScript
import { createAdapter } from 'glove-core/models/providers';

const kimi = createAdapter({
  provider: 'kimi',                       // or openai, gemini, glm, minimax, mimo, …
  model: 'kimi-k2.5',
  baseURL: process.env.MODELS_BASE_URL,   // ${{models.BASE_URL}}
  apiKey: process.env.MODELS_API_KEY,     // ${{models.API_KEY}}
});

const claude = createAdapter({
  provider: 'anthropic',
  model: 'claude-sonnet-5-5',
  baseURL: process.env.ANTHROPIC_BASE_URL, // ${{models.ANTHROPIC_BASE_URL}}
  apiKey: process.env.MODELS_API_KEY,
});

const router = createAdapter({
  provider: 'openrouter',
  model: 'openrouter/anthropic/claude-sonnet-5-5', // your key's name, then OpenRouter's ID
  baseURL: process.env.MODELS_BASE_URL,
  apiKey: process.env.MODELS_API_KEY,
});
  • A provider's own model names (gpt-5, claude-…, gemini-…, kimi-…, glm-…, MiniMax-…, mimo-…) find your key of that provider. Other names start with the key's name: openrouter/…, vercel/…, my-vllm/….
  • Reasoning fields (reasoning_effort, reasoning_content, thinking blocks) pass through untouched.
  • Glove's bedrock provider signs requests with AWS credentials, which the gateway doesn't take. Use a bedrock key through anthropic or openai instead, with a model such as bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0.
  • For ollama and lmstudio, add the server as a custom key and use my-ollama/llama3.2.

OpenAI SDK#

js
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: process.env.MODELS_BASE_URL, // ${{models.BASE_URL}}
  apiKey: process.env.MODELS_API_KEY,   // ${{models.API_KEY}}
});
const r = await client.responses.create({
  model: 'gpt-5-mini',
  input: 'Say hello',
});
python
import os
from openai import OpenAI

client = OpenAI(base_url=os.environ["MODELS_BASE_URL"], api_key=os.environ["MODELS_API_KEY"])
r = client.chat.completions.create(
    model="glm-4.6",
    messages=[{"role": "user", "content": "Say hello"}],
    stream=True,
)
for chunk in r:
    print(chunk.choices[0].delta.content or "", end="")

Anthropic SDK#

js
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({
  baseURL: process.env.ANTHROPIC_BASE_URL, // ${{models.ANTHROPIC_BASE_URL}}
  apiKey: process.env.MODELS_API_KEY,      // ${{models.API_KEY}}
});
const msg = await anthropic.messages.create({
  model: 'claude-sonnet-5-5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Say hello' }],
});

Google Gen AI SDK#

js
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({
  apiKey: process.env.MODELS_API_KEY,                     // ${{models.API_KEY}}
  httpOptions: { baseUrl: process.env.GEMINI_BASE_URL }, // ${{models.GEMINI_BASE_URL}}
});
const r = await ai.models.generateContent({
  model: 'gemini-2.5-flash',
  contents: 'Say hello',
});
python
import os
from google import genai
from google.genai import types

client = genai.Client(
    api_key=os.environ["MODELS_API_KEY"],
    http_options=types.HttpOptions(base_url=os.environ["GEMINI_BASE_URL"]),
)
r = client.models.generate_content(model="gemini-2.5-flash", contents="Say hello")

What the gateway serves#

RouteAPI
POST /models/v1/chat/completionsChat Completions, streamed or not
POST /models/v1/responsesResponses, streamed or not
GET, DELETE /models/v1/responses/{id}, POST …/{id}/cancelStored responses, below
POST /models/v1/embeddingsEmbeddings
POST /models/v1/images/generationsImage generation
POST /models/v1/images/editsImage edits, as a form (or JSON, as xAI takes them)
POST /models/v1/audio/speechSpeech: the audio streams back
POST /models/v1/audio/transcriptionsTranscription, as a form
POST /models/v1/messagesAnthropic's Messages, streamed or not
POST /models/v1/messages/count_tokensAnthropic's token counting
POST /models/v1beta/models/{model}:generateContentGemini, and :streamGenerateContent (with ?alt=sse or without)
POST /models/v1beta/models/{model}:countTokensGemini's token counting
POST /models/v1beta/models/{model}:embedContentGemini's embeddings, and :batchEmbedContents
GET /models/v1/modelsEvery model your project's provider keys offer, as name/model
GET /models/v1/calls/{id}One of your project's calls

Gemini's routes work under /models/v1/models/… too. Every call is recorded under its route (chat.completions, responses, embeddings, images.generations, images.edits, audio.speech, audio.transcriptions, messages, count_tokens, generate_content). Counting tokens is recorded but isn't usage.

Your request goes to the provider as you wrote it. Kuiper changes only the model (to the provider's own ID), asks a Chat Completions stream for its token usage, and asks OpenRouter for its cost. Answers stream through as they come, and a client that goes away closes the provider's connection, so generation stops. Every answer carries x-kuiper-call-id and x-kuiper-provider, and the provider's rate-limit headers.

Which provider serves what#

KindChat CompletionsResponsesEmbeddingsImagesAudioMessagesGemini
openaiyesyes, and storedyesgenerations, editsspeech, transcriptions
anthropicyesyes, and count_tokens
googleyesyesgenerationsyes
openrouteryesyesyes
vercelyesyesyesyes, and count_tokens
togetheryesyesyesgenerationsspeech, transcriptions
fireworksyesyesyesyes
groqyesyesspeech, transcriptions
mistralyesyestranscriptions
deepseekyesyes
xaiyesyes, and storedyesgenerations, edits
basetenyesyes
kimiyesyesyes (international)
glmyes
minimaxyesyes, and count_tokens (international)
mimoyesyes
bedrockyesyes, and storedyes
customyesyes, and storedyesgenerations, editsspeech, transcriptionsyes, and count_tokens

A call to an API its provider doesn't offer is a 400 that names the key, the provider and the API.

Forms#

Image edits and transcriptions stream to the provider as they arrive. The gateway holds a form only until its model field has passed, up to 128 MiB, so the model can pick the key. A form is sent once: there are no fallbacks for it.

Stored responses#

Reading, removing or cancelling a stored response goes to the key that made it. The gateway that served the call remembers that for 30 days. Otherwise, name the key in an x-kuiper-provider header (every answer names its key there), or the project's only key whose provider stores responses is used.

Which provider a model goes to#

  1. name/model goes to the provider key called name: openai/gpt-5, or/moonshotai/kimi-k3, my-vllm/meta-llama/Llama-3.3-70B. On Gemini's routes, where the model is in the path, write name~model.
  2. A provider's own model names go to that provider, so an SDK only changes its base URL. gpt-…, o1, o3, o4, chatgpt-… and OpenAI's embedding, image and audio models go to openai. claude-… goes to anthropic. gemini-…, gemma-… and text-embedding-004 go to google. kimi-… and moonshot-… go to kimi, glm-… to glm, MiniMax-… to minimax and mimo-… to mimo. On Anthropic's routes every other name without a prefix is Anthropic's, and on Gemini's, Google's.
  3. Anything else is a 400 that lists your provider keys. A provider's own model with no key for that provider is a 400 that says to add one, and a key not shared with the project is a 403.

When the organisation has several keys of a provider, the one named after it is used, otherwise the oldest shared with the project.

Fallbacks#

Send an OpenRouter-style list and the gateway tries it in order:

JSON
{ "model": "anthropic/claude-sonnet-5-5", "models": ["openai/gpt-5", "or/google/gemini-2.5-pro"], "messages": [] }

The next model is tried when one can't be reached, doesn't start answering in time, or answers 408, 429 or 5xx, as long as nothing has reached your client yet. There are at most three attempts. Once a stream has started there is no failover: a stream that breaks ends with an error in its API's format. A list works on every route whose body names the model; Gemini's routes, forms and stored responses make one attempt.

Calls, usage and costs#

Kuiper records every call: the model asked for, the provider key and model that answered, the status and any error, tokens, time to first token and latency, whether it streamed, the unit or gateway key, and its conversation. Prompts and completions are never in a record. They are kept only when you turn logging on (below).

Terminal
kuiper models calls -p proxima --since 1h --status error
kuiper models calls -p proxima --has tool_calls,images --unit agent
kuiper models call 01JA… -p proxima
kuiper models usage -p proxima --by model
kuiper models list -p proxima
  • Tokens are the provider's own count: input, cached input read and written, and output, with reasoning shown apart.
  • Estimated cost is what the provider will bill for the call: the provider's own figure where it gives one, such as OpenRouter's, otherwise the tokens at Kuiper's list of prices for that model, otherwise nothing. It is never part of Kuiper's bill. Owners and admins see amounts; members see tokens.
  • Calls are kept for 30 days. Hourly totals are kept for good, on the project's Models page and in the Model calls section of the organisation's Usage page.

The project's Models page in the dashboard shows usage by model, unit, key, provider or day, a call log with filters, and each call in a drawer. Agents connected over MCP get list_model_calls, get_model_call and get_model_usage.

Conversations#

Every call belongs to a conversation. Name it yourself with an x-kuiper-session header, the same on every turn of one agent run:

js
const client = new OpenAI({
  baseURL: process.env.OPENAI_BASE_URL,
  apiKey: process.env.OPENAI_API_KEY,
  defaultHeaders: { 'x-kuiper-session': runId },
});

Without one, a logged call's conversation is a key made from its system prompt and its first user message (c_…), which stays the same while the agent's history grows. Click a call's conversation on the Models page, or run kuiper models calls --conversation KEY, to see its calls in order.

Log prompts, answers and traces#

By default no prompt or completion is kept anywhere. An owner or admin turns logging on for a project and says how long logged calls are kept, from 1 to 30 days (7 unless you say):

Terminal
kuiper models logging full -p proxima --retention 14
kuiper models logging metadata -p proxima

You can do the same on the project's Models page. With full, each call keeps:

  • Its trace. The call in one shape, whichever API it used: the parameters (temperature, max tokens, reasoning effort or thinking budget, tool choice, response format), the tools offered, and every message in and out, in order. A message's parts are its text, the model's reasoning (or that it was encrypted), tool calls with their arguments, tool results, images, audio, files and refusals. With them come the tokens, with cache reads and writes and reasoning apart, time to first token, latency, output tokens a second, and why the answer ended.
  • Its raw request and answer, each up to 1 MiB, as they were sent. A streamed answer is kept as the stream your client got.

Kuiper builds the trace after the call ends, so logging adds no latency. Images and audio sent inline are kept as files; an image you send as a URL stays a URL, and Kuiper never fetches it.

Your agent sends its whole history again on every turn, but Kuiper keeps each message once per project and each image or sound once per organisation. A long agent run costs about what its messages weigh once.

Owners and admins read logged calls:

  • in the call's drawer on the Models page: the input, the reasoning, the output with images and audio players, and each tool call linked to its result and to the call that carried it;
  • with kuiper models call ID -p proxima, which prints the messages (--json prints the call and its trace as JSON, --raw the request and answer as they were sent);
  • with the get_model_call MCP tool;
  • at GET /v1/projects/{project}/models/calls/{id}, whose answer includes trace.

Members see each call's record but never what it said.

Limits#

LimitValuePast it
Calls a minute per gateway key600429 with Retry-After
Calls in flight per organisation100429 with Retry-After
Until the provider starts answering300 sthe next fallback, else 504
A whole call, stream included30 minthe stream ends with an error
A request body32 MiB413
A form (image edits, transcriptions)128 MiB413

Your providers' own limits still apply to your keys, and their 429s come back to you with their headers.

Errors#

Errors come in the format of the API you called, so SDKs raise their usual exceptions: OpenAI's routes answer {"error": {"message", "type", "code"}}, Anthropic's {"type": "error", "error": {"type", "message"}} and Gemini's {"error": {"code", "message", "status"}}. A provider's own error is passed on as the provider sent it.

StatusCodeMeaning
400invalid_modelThe model names no provider key, there is no key for that provider, or the provider doesn't offer that API
401invalid_api_keyNo gateway key, or an unknown or expired one
403permission_deniedThe key lacks the models service, or the provider key isn't shared with the project
429rate_limited, too_many_callsA limit above
502provider_unreachable, provider_errorThe provider couldn't be reached
503unavailableThe gateway can't reach Kuiper yet and doesn't have your provider keys
504timeoutThe provider didn't answer in time