Eine Base-URL. Jedes Modell.
Richte ein beliebiges OpenAI-SDK auf unbottled.ai. 202 Modelle, Streaming, Tool-Aufrufe. Dein Schlüssel, deine Credits, kein neuer Client.
In 60 Sekunden zum ersten Aufruf
Die API spricht das OpenAI-Format. Läuft dein Code gegen OpenAI, läuft er auch hier, nach zwei Änderungen: Base-URL und Schlüssel.
Schlüssel holen
Erstelle unter Einstellungen > API-Schlüssel einen sk-unb-Schlüssel. Er wird nur einmal angezeigt. Die API gehört zu jedem bezahlten Tarif: ein aktives Abo oder ein gekauftes Credit-Paket schaltet sie frei.
Jede Sprache im Pfad funktioniert (en-US, de-DE, pl-PL). Sie bestimmt nur die Sprache der Fehlermeldungen.
Anfrage senden
Reines HTTP. Funktioniert in jeder Sprache.
curl http://localhost:3000/api/de-DE/v1/chat/completions \
-H "Authorization: Bearer sk-unb-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2",
"messages": [
{ "role": "system", "content": "You answer in one sentence." },
{ "role": "user", "content": "Why is the sky blue?" }
]
}'Oder ein OpenAI-SDK nutzen
baseURL und apiKey ändern. Mehr nicht.
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:3000/api/de-DE/v1",
api_key="sk-unb-YOUR_KEY",
)
reply = client.chat.completions.create(
model="kimi-k2",
messages=[{"role": "user", "content": "Why is the sky blue?"}],
)
print(reply.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "http://localhost:3000/api/de-DE/v1",
apiKey: process.env.UNBOTTLED_API_KEY, // sk-unb-YOUR_KEY
});
const reply = await client.chat.completions.create({
model: "kimi-k2",
messages: [{ role: "user", content: "Why is the sky blue?" }],
});
console.log(reply.choices[0].message.content);Authentifizierung
Sende deinen Schlüssel bei jeder Anfrage als Bearer-Token. Schlüssel beginnen mit sk-unb-.
Authorization: Bearer sk-unb-DEIN_SCHLUESSELWir speichern einen Hash, nicht den Schlüssel. Verloren heißt: neuen erstellen.
Ein Schlüssel gilt nur für /v1. Auf Konto, Zahlungen oder Einstellungen kommt er nicht.
Bis zu 20 aktive Schlüssel pro Konto. Jeden Schlüssel kannst du jederzeit widerrufen: Er stoppt sofort.
Fehlender oder widerrufener Schlüssel liefert 401. Ein Schlüssel, dessen Konto keinen bezahlten Tarif hat, liefert 402.
GET /v1/models
Listet die Modelle, die dein Schlüssel jetzt aufrufen kann, mit Kontextgröße, Tool-Unterstützung und Credit-Preisen pro Million Token. Die id nutzt du als model-Feld.
curl http://localhost:3000/api/de-DE/v1/models \
-H "Authorization: Bearer sk-unb-YOUR_KEY"{
"object": "list",
"data": [
{
"id": "kimi-k2",
"object": "model",
"created": 1735689600,
"owned_by": "moonshot",
"name": "Kimi K2",
"context_length": 131072,
"supports_tools": true,
"input_modalities": ["text"],
"output_modalities": ["text"],
"pricing": {
"input_credits_per_million_tokens": 8,
"output_credits_per_million_tokens": 32
}
}
]
}Die Liste hängt von deinem Konto ab. Unbekannte oder nicht verfügbare IDs liefern 404 model_not_found. Die API tauscht dein Modell nie heimlich aus.
POST /v1/chat/completions
Standard-Chat-Completions im OpenAI-Format. Nachrichten rein, eine Assistenten-Nachricht raus. Text- und Bildeingaben (image_url als https-URL oder base64-Data-URL).
Ohne Streaming
Der Standard. Du bekommst einen JSON-Body, sobald das Modell fertig ist. usage.credits_charged zeigt, was der Aufruf gekostet hat.
curl http://localhost:3000/api/de-DE/v1/chat/completions \
-H "Authorization: Bearer sk-unb-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2",
"messages": [
{ "role": "system", "content": "You answer in one sentence." },
{ "role": "user", "content": "Why is the sky blue?" }
]
}'{
"id": "chatcmpl-3f9c1a...",
"object": "chat.completion",
"created": 1767225600,
"model": "kimi-k2",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Air scatters blue light more than red." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 31,
"completion_tokens": 9,
"total_tokens": 40,
"credits_charged": 0.1
}
}Streaming
Mit stream: true kommen Server-Sent-Events im OpenAI-Chunk-Format, abgeschlossen durch data: [DONE]. Mit stream_options.include_usage erhältst du einen letzten Chunk mit Token-Zahlen und credits_charged. Schließt du die Verbindung, stoppt die Generierung. Du zahlst nur, was lief.
curl -N http://localhost:3000/api/de-DE/v1/chat/completions \
-H "Authorization: Bearer sk-unb-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [{ "role": "user", "content": "Count to three." }]
}'data: {"id":"chatcmpl-3f9c...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-3f9c...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"One, "},"finish_reason":null}]}
data: {"id":"chatcmpl-3f9c...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-3f9c...","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":7,"total_tokens":19,"credits_charged":0.1}}
data: [DONE]stream = client.chat.completions.create(
model="kimi-k2",
messages=[{"role": "user", "content": "Count to three."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")const stream = await client.chat.completions.create({
model: "kimi-k2",
messages: [{ role: "user", content: "Count to three." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Anfragefelder
Tool-Aufrufe
Function Calling funktioniert wie bei OpenAI. Du beschreibst Funktionen, das Modell entscheidet sich für einen Aufruf, du führst ihn aus und schickst das Ergebnis zurück. Deine Tools führen wir nie aus.
Der Ablauf
Schicke tools zusammen mit deinen Nachrichten.
Das Modell antwortet mit tool_calls und finish_reason: tool_calls. Jeder Aufruf hat eine id, einen Funktionsnamen und Argumente als JSON-String.
Führe die Funktion selbst aus. Hänge die Assistenten-Nachricht an, danach pro Aufruf eine tool-Nachricht mit der passenden tool_call_id.
Rufe erneut auf. Das Modell nutzt die Ergebnisse und antwortet.
1. Anfrage mit Tools
curl http://localhost:3000/api/de-DE/v1/chat/completions \
-H "Authorization: Bearer sk-unb-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2",
"messages": [{ "role": "user", "content": "Weather in Berlin?" }],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}'2. Das Modell verlangt einen Aufruf
{
"object": "chat.completion",
"model": "kimi-k2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_8x2k",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Berlin\"}" }
}
]
},
"finish_reason": "tool_calls"
}
]
}3. Ergebnis zurückgeben
# Run get_weather yourself, then send the result back:
curl http://localhost:3000/api/de-DE/v1/chat/completions \
-H "Authorization: Bearer sk-unb-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kimi-k2",
"messages": [
{ "role": "user", "content": "Weather in Berlin?" },
{ "role": "assistant", "content": null, "tool_calls": [
{ "id": "call_8x2k", "type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Berlin\"}" } }
]},
{ "role": "tool", "tool_call_id": "call_8x2k", "content": "18C, cloudy" }
],
"tools": [ ...same tools as before... ]
}'Derselbe Ablauf mit dem Python-SDK
import json
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "Weather in Berlin?"}]
first = client.chat.completions.create(
model="kimi-k2", messages=messages, tools=tools
)
call = first.choices[0].message.tool_calls[0]
result = get_weather(**json.loads(call.function.arguments)) # your code
messages += [
first.choices[0].message,
{"role": "tool", "tool_call_id": call.id, "content": result},
]
final = client.chat.completions.create(
model="kimi-k2", messages=messages, tools=tools
)
print(final.choices[0].message.content)Tool-Aufrufe im Stream
Mit stream: true kommen Aufrufe als delta.tool_calls-Chunks, adressiert über index. Der erste Chunk eines Index trägt id und function.name. Spätere Chunks hängen an function.arguments an. Setze sie zusammen und parse das JSON, sobald finish_reason tool_calls ist. Jedes OpenAI-SDK erledigt das für dich.
Das Modell muss Tools unterstützen: Prüfe supports_tools in GET /v1/models. Andere liefern 400 tools_not_supported.
tool_choice none, auto und required funktionieren wie bei OpenAI. Eine Funktion, die nicht in tools steht, liefert 400.
Funktionsnamen: Buchstaben, Ziffern, Unterstrich, Bindestrich, bis zu 64 Zeichen.
Tool-Definitionen und Tool-Ergebnisse sind Eingabe-Token. Aufrufe sind Ausgabe-Token. Sie werden wie alle anderen Token abgerechnet.
Credits und Abrechnung
Die API zieht von denselben Credits wie die Chat-App, zu denselben Preisen. Ein Guthaben, eine Rechnung.
Kosten = Eingabe- plus Ausgabe-Token zum Credit-Preis des Modells. Die Preise pro Million Token stehen in GET /v1/models.
Jede Antwort nennt usage.credits_charged. Dein Guthaben findest du in der App unter Credits.
Vor jedem Aufruf prüfen wir dein Guthaben gegen eine Worst-Case-Schätzung. Reicht es nicht: HTTP 402 insufficient_credits, nichts läuft, nichts wird berechnet.
Nach dem Aufruf zahlst du die echte Token-Nutzung, die der Anbieter meldet, nicht die Schätzung.
Credits leer? Kaufe ein Paket oder verlängere deinen Tarif. Der nächste Aufruf läuft sofort.
HTTP/1.1 402 Payment Required
{
"error": {
"message": "Insufficient credits. This call needs about 1.2, you have 0.4. Top up to continue.",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_credits"
}
}Rate Limits
Jeder Schlüssel erlaubt standardmäßig 60 Anfragen pro Minute. Darüber liefert die API HTTP 429 mit einem Retry-After-Header in Sekunden. Warte so lange und versuche es erneut. Limits gelten pro Schlüssel: Verteile große Lasten auf mehrere Schlüssel.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{ "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", ... } }Fehler
Fehler nutzen den OpenAI-Umschlag, die SDKs werfen also ihre gewohnten Exceptions. Werte error.code aus, nicht die Meldung.
{
"error": {
"message": "Invalid or revoked API key. ...",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}Bau auf jedem Modell.
Schlüssel erstellen, SDK umstellen, ausliefern.