sicherAI-API für Entwickler

Technische Anleitung für die Programmierschnittstelle (API) von sicherAI: Adresse, Anmeldung, Anfragen, Antworten, Fehler und Beispiele. Zahlen zu den Grenzen nennt nur die Leistungsbeschreibung.

Überblick

Base URL
https://api.sicherai.com/v1
Endpunkte
GET /v1/models für die Modellliste, POST /v1/chat/completions für eine Antwort als JSON oder als Stream
Vertragsversion
v1-2026-10-20 für Felder, Fehler und Grenzen

Die API folgt dem Format der OpenAI-API, aber nur in dem geprüften Ausschnitt, den diese Seite beschreibt. Andere Pfade beantwortet sie mit 404 not_found, andere Methoden mit 405 method_not_allowed und unbekannte Felder mit 400 unsupported_parameter. Pfade gelten genau so, wie sie hier stehen: ohne Schrägstrich am Ende, ohne Query-Parameter und in Kleinbuchstaben.

Rufe die API nur von deinem Server auf. Sie setzt keine CORS-Kopfzeilen; Aufrufe aus dem Browser sind nicht vorgesehen.

Sende Anfragen nur über https. Hast du einen Schlüssel über http gesendet, widerrufe ihn und lege einen neuen an. Anfragen über http beantwortet die API mit 400 invalid_request, ohne Weiterleitung.

Schnellstart

  1. Ein Inhaber oder Admin gibt in der App unter Einstellungen › Organisation › API-Zugang die API der Organisation frei und legt ihre API-Monatsgrenze fest.
  2. Dort legt ein Inhaber oder Admin einen Schlüssel an. Der volle Schlüssel erscheint nur einmal; kopiere ihn sofort.
  3. Lege den Schlüssel in der Umgebungsvariable SICHERAI_API_KEY ab. Schreibe ihn nie in den Code.
  4. Rufe GET /v1/models auf und wähle eine Kennung aus der Liste.
  5. Sende damit die erste Anfrage an POST /v1/chat/completions. Die Codebeispiele zeigen beide Schritte.

Die Beispiele verwenden SICHERAI_API_KEY statt OPENAI_API_KEY. So senden andere OpenAI-Programme in derselben Umgebung den Schlüssel nicht an api.openai.com.

Authentisierung

Sende den Schlüssel im Kopf Authorization: Bearer <Schlüssel>. Andere Wege liest die API nicht: weder Query-Parameter noch Cookies noch Kopfzeilen wie x-api-key.

Die Organisation ergibt sich allein aus dem Schlüssel. Kopfzeilen wie OpenAI-Organization oder OpenAI-Project wertet die API nicht aus.

Ein Schlüssel hat die Form sai_<8 Zeichen>_<43 Zeichen> und besteht nur aus Buchstaben und Ziffern. Die App zeigt nach dem Anlegen nur das Präfix sai_<8 Zeichen>.

Fehlt der Schlüssel oder ist er falsch geformt, unbekannt, abgelaufen, widerrufen oder gesperrt, antwortet die API einheitlich mit 401 invalid_api_key und dem Kopf WWW-Authenticate: Bearer.

Modelle

GET /v1/models liefert die Modelle, die dein Schlüssel jetzt nutzen darf. Sende die Anfrage ohne Query-Parameter und ohne Body: Ein Query-Parameter ergibt 404 not_found, ein Body 400 invalid_request.

Antwort von GET /v1/models
{
  "object": "list",
  "data": [
    { "id": "<modell-id>", "object": "model", "created": 1792454400, "owned_by": "sicherai" }
  ]
}

Welche Modelle in der Liste stehen, hängt von deiner Organisation, ihren Modellfreigaben und der optionalen Modellauswahl des Schlüssels ab. Lies die Liste deshalb aus, statt Kennungen fest einzutragen.

created ist der Tag, an dem das Modell in die API aufgenommen wurde, nicht der Zeitpunkt der Abfrage. owned_by ist immer sicherai. Die Liste hat genau eine Seite. Der Abruf verbraucht keine Token; für ihn gilt eine eigene Grenze je Minute.

Steht ein Modell nicht in deiner Liste, beantwortet POST /v1/chat/completions die Anfrage mit 404 model_not_found, gleich aus welchem Grund. Diese Seite nennt keine Modelle: In den Beispielen steht der Platzhalter <modell-id>; setze dort eine Kennung aus deiner Liste ein.

Chat Completions als JSON

POST /v1/chat/completions nimmt einen JSON-Body in UTF-8 an, mit dem Kopf Content-Type: application/json. Pflicht sind model und messages.

messages ist eine Liste von Nachrichten mit genau den Feldern role und content. content ist ein nicht leerer Text oder eine Liste von Textblöcken der Form {"type": "text", "text": "…"}. Die Rolle user mit Text nimmt jedes Modell an. Die Rollen system, developer und assistant sowie Textblöcke gelten nur für Modelle, deren Route sie unterstützt; sonst antwortet die API mit 400 unsupported_parameter.

Felder von POST /v1/chat/completions
GruppeFelderVerhalten
Pflichtmodel, messagesmodel nennt eine Kennung aus GET /v1/models; messages enthält mindestens eine Nachricht.
Unterstütztstream, stream_options, max_completion_tokensstream_options nur zusammen mit "stream": true und nur mit include_usage. max_completion_tokens begrenzt die Ausgabe; ohne das Feld gilt ein Standardwert.
Je nach Modellmax_tokens, temperature, top_p, stop, reasoning_effortNur für Modelle, deren Route das Feld unterstützt, und nur im dort geprüften Wertebereich; sonst 400 unsupported_parameter. max_tokens nie zusammen mit max_completion_tokens.
Ohne Wirkungn: 1, store: false, logprobs: false, modalities: ["text"], tool_choice: "none", frequency_penalty: 0, presence_penalty: 0, userAngenommen nur in genau dieser Form; jede andere ergibt 400 unsupported_parameter. user ist ein Text ohne Wirkung; ist er zu lang, folgt 400 invalid_request.
Abgewiesenaudio, function_call, functions, logit_bias, metadata, parallel_tool_calls, prediction, response_format, seed, service_tier, tools, top_logprobs, web_search_optionsJeder Wert außer null ergibt 400 unsupported_parameter, ebenso jedes unbekannte Feld.

null gilt wie ein fehlendes Feld, außer bei model und messages.

Antwort als JSON
{
  "id": "chatcmpl-0123456789abcdef0123456789abcdef",
  "object": "chat.completion",
  "created": 1792540800,
  "model": "<modell-id>",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "OK" },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 21, "completion_tokens": 2, "total_tokens": 23 }
}

id beginnt mit chatcmpl-, created zählt in Unix-Sekunden, model ist die angefragte Kennung. choices enthält genau einen Eintrag; finish_reason ist stop, length oder content_filter. usage nennt prompt_tokens, completion_tokens einschließlich Reasoning-Token und total_tokens als Summe beider.

Antwortköpfe: content-type: application/json, cache-control: no-store und x-request-id mit einer Kennung aus req_ und 32 Hexzeichen. Fehler, die schon der vorgelagerte Proxy beantwortet, etwa 404 not_found für einen unbekannten Pfad, tragen keine x-request-id.

Die JSON-Antwort kommt erst, wenn die Antwort vollständig vorliegt; bis dahin sendet die API auch keine Kopfzeilen. Für lange Ausgaben nimm den Stream.

Stream

Mit "stream": true antwortet die API mit Server-Sent Events. Kopfzeilen: content-type: text/event-stream, cache-control: no-store, x-accel-buffering: no und x-request-id. Jedes Ereignis ist eine Zeile data: <JSON> mit einer Leerzeile danach, in UTF-8. stream_options ist nur bei "stream": true erlaubt und kennt nur include_usage.

  1. Ein Chunk chat.completion.chunk mit "delta": {"role": "assistant", "content": ""}. id, created und model bleiben im ganzen Stream gleich.
  2. Textteile in choices[0].delta.content; finish_reason ist null.
  3. Ein Abschlusschunk mit leerem delta und finish_reason stop, length oder content_filter.
  4. Nur mit "stream_options": {"include_usage": true}: ein letzter Chunk mit "choices": [] und der vollständigen usage. Alle Chunks davor tragen "usage": null; ohne die Option fehlt das Feld usage.
  5. data: [DONE] genau einmal; danach endet der Stream.

Ein Stream ohne data: [DONE] ist nicht erfolgreich. Die SDKs reichen [DONE] nicht an dich weiter und beenden die Schleife ohne Fehler, wenn der Stream ohne [DONE] endet. Fordere deshalb include_usage an und prüfe am Ende, ob der Usage-Chunk angekommen ist: Er kommt erst, wenn die Antwort vollständig ist.

Fehler vor dem ersten Chunk beantwortet die API als HTTP-Fehler im JSON-Format. Danach sendet sie ein Ereignis data: {"error": {…}} im selben Format und beendet den Stream ohne [DONE]; der HTTP-Status bleibt 200. Die SDKs werfen dann einen APIError mit code, aber ohne HTTP-Status. Nach dem Start eines Streams kommen nur Fehler ohne automatische Wiederholung; aus 503 service_unavailable wird dort 500 internal_error.

Brichst du den Stream ab, etwa mit einem AbortController, beendet die API auch den Aufruf beim Modellanbieter. Während des Streams prüft die API wiederholt Schlüssel, Organisation und Zugang. Fällt eine Voraussetzung weg, etwa durch einen Widerruf, endet der Stream mit einem Fehlerereignis.

Jede Anfrage hat eine Höchstdauer. Danach endet ein Stream mit dem Fehlerereignis upstream_timeout, eine JSON-Anfrage mit 504 upstream_timeout. Lies den Stream zügig: Was dein Client bis zur Höchstdauer nicht gelesen hat, verfällt mit der Verbindung.

Fehler

Jede Fehlerantwort der API hat diesen Körper:

Fehlerkörper
{
  "error": {
    "message": "Die Anfrage entspricht nicht dem Vertrag der sicherAI-API.",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request"
  }
}

Werte code aus, nicht message: message ist ein fester deutscher Text für Menschen. param nennt höchstens den Namen eines Felds, nie einen Wert. Fehler ohne automatische Wiederholung tragen den Kopf x-should-retry: false; die SDKs wiederholen sie dann nicht von sich aus. 401 trägt WWW-Authenticate: Bearer, 405 trägt Allow, 429 mit rate_limit_error trägt Retry-After in ganzen Sekunden, 503 kann Retry-After tragen.

Fehlercodes der API
codeHTTPtypeBedeutungAutomatisch wiederholen
invalid_request400invalid_request_errorDie Anfrage passt nicht zum Vertrag, etwa ungültiges JSON, ein leerer Text, ein Body bei GET /v1/models, ein ungültiger Idempotency-Key oder eine Anfrage über http.Nein. Anfrage korrigieren.
unsupported_parameter400invalid_request_errorEin Feld oder Wert wird nicht unterstützt, von der API oder von der Route des Modells. param nennt das Feld, wenn es zur Feldmatrix gehört.Nein. Anfrage korrigieren.
context_length_exceeded400invalid_request_errorDie Eingabe ist zu lang: zu viele Nachrichten, zu viel Text oder mehr Token, als das Modell oder eine Minute zulässt.Nein. Eingabe kürzen.
invalid_api_key401authentication_errorDer Schlüssel fehlt, ist falsch geformt, unbekannt, abgelaufen, widerrufen oder gesperrt.Nein. Schlüssel prüfen oder neu anlegen.
api_access_denied403permission_errorDer API-Zugang der Organisation ist nicht freigegeben oder gesperrt.Nein. Ein Inhaber oder Admin prüft den API-Zugang.
contract_required403permission_errorDer Organisation fehlen Vereinbarungen, die der API-Zugang voraussetzt.Nein. Ein Inhaber oder Admin klärt das in der App.
model_not_found404invalid_request_errorDie Modellkennung steht nicht in der Modellliste deines Schlüssels.Nein. Kennung aus GET /v1/models nehmen.
not_found404invalid_request_errorDiesen Pfad gibt es nicht, auch nicht mit Schrägstrich am Ende oder mit Query-Parametern.Nein. Pfad prüfen.
method_not_allowed405invalid_request_errorDie Methode passt nicht zum Pfad; der Kopf Allow nennt die erlaubte.Nein. Methode aus Allow nehmen.
idempotency_conflict409invalid_request_errorDer Idempotency-Key ließ sich für diese Anfrage nicht verwenden.Nein.
request_in_progress409invalid_request_errorEine Anfrage mit demselben Idempotency-Key läuft noch, oder ihr Ausgang ist offen.Nein.
request_already_processed409invalid_request_errorEine Anfrage mit demselben Idempotency-Key ist abgeschlossen; ihre Antwort kommt nicht erneut.Nein.
request_too_large413invalid_request_errorDer Body ist zu groß.Nein. Anfrage verkleinern.
rate_limit_exceeded429rate_limit_errorZu viele Anfragen oder Token im laufenden Minutenfenster.Ja, nach der Zeit in Retry-After.
concurrency_limit_exceeded429rate_limit_errorZu viele gleichzeitige Anfragen.Ja, nach der Zeit in Retry-After.
org_budget_exceeded429insufficient_quotaDas Nutzungsbudget der Organisation ist für den laufenden Zeitraum aufgebraucht.Nein.
api_budget_exceeded429insufficient_quotaDie API-Monatsgrenze der Organisation ist erreicht oder auf 0 gesetzt.Nein.
key_budget_exceeded429insufficient_quotaDie Monatsgrenze dieses Schlüssels ist erreicht oder auf 0 gesetzt.Nein.
short_window_exceeded429insufficient_quotaDas Kurzzeitbudget der Organisation ist ausgeschöpft; es wird mit dem gleitenden Zeitfenster wieder frei.Nein.
internal_error500server_errorInterner Fehler. Der Aufruf kann den Modellanbieter schon erreicht haben.Nein. Wenn überhaupt, dann einmal mit demselben Idempotency-Key.
upstream_error502server_errorDer Modellanbieter hat nicht erfolgreich geantwortet.Nein. Wenn überhaupt, dann einmal mit demselben Idempotency-Key.
service_unavailable503server_errorDie API nimmt vorübergehend keine Anfragen an; der Modellanbieter wurde noch nicht aufgerufen.Ja, später.
upstream_timeout504server_errorDer Modellanbieter hat nicht rechtzeitig geantwortet, oder die Höchstdauer der Anfrage ist erreicht.Nein. Wenn überhaupt, dann einmal mit demselben Idempotency-Key.

Die SDKs bilden die Statuscodes auf Fehlerklassen ab: 400 BadRequestError, 401 AuthenticationError, 403 PermissionDeniedError, 404 NotFoundError, 409 ConflictError, 429 RateLimitError, ab 500 InternalServerError. 405 und 413 haben keine eigene Klasse. Ohne Antwort melden sie einen Verbindungsfehler oder eine Zeitüberschreitung.

Außerhalb dieses Formats: Eine fehlerhafte Kopfzeile ergibt 400 Bad Request, zu große Kopfzeilen ergeben 431, beide als Text vom HTTP-Server. Die SDKs liefern dann code und type ohne Wert.

Wiederholungen und Idempotency-Key

  1. Schalte die automatischen Wiederholungen des SDKs aus: maxRetries: 0 in Node, max_retries=0 in Python. Sonst wiederholt das SDK nach Verbindungsfehlern und Zeitüberschreitungen von selbst, auch wenn die erste Anfrage den Modellanbieter schon erreicht hat. Ohne Idempotency-Key liefe der Aufruf dann zweimal.
  2. Wiederhole selbst nur bei 429 rate_limit_exceeded oder 429 concurrency_limit_exceeded nach der Zeit in Retry-After und bei 503 service_unavailable. Andere Fehler wiederholst du nicht automatisch.
  3. Der Kopf Idempotency-Key ist optional. Erlaubt sind 1 bis 255 Zeichen sichtbares ASCII ohne Komma, etwa eine UUID; andere Werte ergeben 400 invalid_request. Der Key gilt nur für deinen Schlüssel.
  4. Weitere POST-Anfragen mit demselben Key:
    • Der erste Aufruf läuft noch, oder sein Ausgang ist offen: 409 request_in_progress. Das gilt auch für zwei gleichzeitige Aufrufe.
    • Er ist abgeschlossen: 409 request_already_processed, bis eine feste Frist nach dem Abschluss verstrichen ist; danach ist der Key wieder frei.
    • Er scheiterte, bevor die Übergabe an den Modellanbieter begann: Der Key ist sofort wieder frei, und der neue Aufruf läuft.
    • 409 idempotency_conflict entsteht nur bei einem Wettlauf um den Key; auch dann nicht automatisch wiederholen.
    Ob der Body gleich ist, prüft die API nicht; die Antwort ist in beiden Fällen dieselbe 409.
  5. Die API liefert die Antwort des ersten Aufrufs nicht erneut aus. Ohne Key ist jeder POST ein eigener Aufruf.
  6. So sendest du den Key: in Node als Anfrageoption headers: { "Idempotency-Key": … } (die Option idempotencyKey sendet in openai 7.42.0 keinen Kopf), in Python als extra_headers={"Idempotency-Key": …}, mit curl als -H "Idempotency-Key: …".
  7. Bleibt eine Antwort aus, etwa nach einem Verbindungsabbruch, sende dieselbe Anfrage mit demselben Key einmal erneut. 409 request_in_progress oder 409 request_already_processed heißt dann: Der erste Aufruf ist angekommen, seine Antwort kommt aber nicht mehr.

Grenzen

Die Zahlen nennt nur die Leistungsbeschreibung. Hier stehen Art und Fehlerbild jeder Grenze, damit jede Zahl genau eine Quelle hat.

Grenzen der API mit Fehlerbild
GrenzeFehlerbild
Anfragen pro Minute, je Schlüssel und je Organisation; die Grenze der Organisation zählt den Chat in der App mit429 rate_limit_exceeded mit Retry-After
Token pro Minute, je Schlüssel und je Organisation; beide zählen nur Token der API429 rate_limit_exceeded mit Retry-After
Gleichzeitige Anfragen an POST /v1/chat/completions, je Schlüssel und je Organisation; der Chat in der App zählt nicht mit429 concurrency_limit_exceeded mit Retry-After
Abrufe von GET /v1/models pro Minute, je Schlüssel429 rate_limit_exceeded mit Retry-After
Abgewiesene Anfragen pro Minute, je Schlüssel: Wiederholt fehlerhafte Anfragen bremsen den Schlüssel429 rate_limit_exceeded mit Retry-After
Lastbremse vor der Anmeldung; sie kann auch durch Anfragen anderer Clients ausgelöst werden429 rate_limit_exceeded mit Retry-After
Größe des Bodys413 request_too_large
Zahl der Nachrichten und Länge aller Texte in UTF-16-Codeeinheiten400 context_length_exceeded
Eingabe-Token je Anfrage und Modell: Die API schätzt sie vor dem Aufruf nach oben ab, aus den UTF-8-Bytes aller Texte und Stoppfolgen und einem festen Zuschlag je Anfrage, Nachricht und Text. Zeichen mit mehreren Bytes, etwa Umlaute, zählen mehrfach400 context_length_exceeded
Token einer einzelnen Anfrage, also geschätzte Eingabe plus Ausgabegrenze, über der Grenze für Token pro Minute400 context_length_exceeded, denn Warten hilft hier nicht
Ausgabe-Token: Standardwert ohne max_completion_tokens und Höchstwertüber dem Höchstwert oder dem Deckel des Modells: 400 invalid_request
Länge des Felds user400 invalid_request mit param user
Höchstdauer einer AnfrageJSON: 504 upstream_timeout; Stream: Fehlerereignis upstream_timeout
Ablauf eines Schlüssels: Standard und Höchstwert in Tagen, oder ohne Ablaufnach dem Ablauf: 401 invalid_api_key
Duplikatschutz nach dem Abschluss einer Anfrage mit Idempotency-Key409 request_already_processed
Monatsgrenze des Schlüssels und API-Monatsgrenze der Organisation, beide in der App festgelegt; Nutzungsbudget und Kurzzeitbudget der Organisation429 insufficient_quota mit key_budget_exceeded, api_budget_exceeded, org_budget_exceeded oder short_window_exceeded, ohne Retry-After

Die Grenzen je Minute zählen in festen Minutenfenstern; Retry-After nennt ganze Sekunden. Die Monatsgrenzen gelten je Kalendermonat in UTC.

Schlüssel verwalten

Schlüssel verwalten Inhaber und Admins in der App unter Einstellungen › Organisation › API-Zugang: anlegen, Grenzen ändern, widerrufen und die API der Organisation sperren. Ohne Freigabe oder ohne API-Monatsgrenze bleibt die API der Organisation gesperrt (403 api_access_denied). Eine API-Monatsgrenze von 0 sperrt Anfragen an POST /v1/chat/completions (429 api_budget_exceeded).

  • Ablauf: ganze Tage oder „läuft nie ab“. Ohne Angabe gilt der Standardablauf; Standard und Höchstwert nennt die Leistungsbeschreibung. Nach dem Ablauf antwortet die API mit 401 invalid_api_key.
  • Monatsgrenze je Schlüssel: Eine Grenze von 0 sperrt seine Anfragen an POST /v1/chat/completions (429 key_budget_exceeded).
  • Erlaubte Modelle: optional; ohne Auswahl gilt keine zusätzliche Einschränkung.

Schlüssel ohne Ablauf bleiben gültig bis zum Widerruf, solange keiner der unten genannten Fälle sie sperrt. Erneuere sie regelmäßig: neuen Schlüssel anlegen, Anwendung umstellen, alten widerrufen. Einen Austausch an Ort und Stelle gibt es nicht.

Der volle Schlüssel erscheint nur einmal, direkt nach dem Anlegen. Danach zeigt die App nur das Präfix. Ist er verloren, widerrufe ihn und lege einen neuen an.

Ein Widerruf gilt einzeln für einen Schlüssel und wirkt ab der nächsten Anfrage. Ein laufender Stream endet bei der nächsten Prüfung mit einem Fehlerereignis.

Jeder Schlüssel ist dem Inhaber oder Admin zugeordnet, der ihn angelegt hat. Ohne Widerruf sperren diese Fälle seine Schlüssel für neue Anfragen mit 401 invalid_api_key: Die Person verlässt die Organisation, verliert die Rolle Inhaber oder Admin, ihr Konto wird gesperrt, oder die App verlangt von ihr, die E-Mail-Adresse zu bestätigen oder die Zustimmung nachzuholen, etwa nach einer neuen Fassung der AGB. Sperrt die Organisation ihre API, antworten alle ihre Schlüssel mit 403 api_access_denied.

Brauchst du einen Schlüssel nicht mehr, widerrufe ihn. Ist ein Schlüssel offengelegt, widerrufe ihn sofort; betrifft es mehrere Schlüssel, sperre die API der Organisation.

Versionen und Änderungen

  • Vertragsversion v1-2026-10-20: Sie umfasst Felder, Fehler und Grenzen der API.
  • Modellkatalog 2026-10-20: Die Version steigt, wenn ein Modell aufgenommen oder gestrichen wird.
  • Die Beispiele folgen openai 7.42.0 für Node und openai 3.37.0 für Python. Andere Versionen können sich anders verhalten, etwa bei Wiederholungen oder Umgebungsvariablen.

Abschaltungen von Modellen und Änderungen an Endpunkten stehen auf dieser Seite mit Datum und, soweit vorhanden, Ersatz, in der Regel mindestens 30 Tage vorher. Abschaltungen und Änderungen, die ein Modellanbieter vorgibt, sowie dringende Sicherheitskorrekturen kündigen wir hier so früh wie möglich an.

Angekündigte Änderungen

Derzeit keine.

Codebeispiele

Die Beispiele sind synthetisch. Sie lesen den Schlüssel nur aus SICHERAI_API_KEY, setzen die Base URL ausdrücklich und schalten automatische Wiederholungen aus. Ersetze <modell-id> durch eine Kennung aus GET /v1/models.

curl
# Schlüssel nur aus der Umgebung (SICHERAI_API_KEY), nie im Skript.

# 1 Modellliste
curl https://api.sicherai.com/v1/models \
  -H "Authorization: Bearer $SICHERAI_API_KEY"

# 2 Antwort als JSON. Den Idempotency-Key einmal je Vorgang erzeugen und bei
#   einer eigenen Wiederholung erneut senden.
IDEMPOTENCY_KEY=$(uuidgen)
curl https://api.sicherai.com/v1/chat/completions \
  -H "Authorization: Bearer $SICHERAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "model": "<modell-id>",
    "messages": [{"role": "user", "content": "Synthetischer Testtext: Bitte antworte mit OK."}]
  }'

# 3 Antwort als Stream (-N: ohne Puffer). Die Usage kommt als letzter Chunk vor
#   data: [DONE]; ohne data: [DONE] ist der Stream nicht vollständig.
curl -N https://api.sicherai.com/v1/chat/completions \
  -H "Authorization: Bearer $SICHERAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<modell-id>",
    "messages": [{"role": "user", "content": "Synthetischer Testtext: Zähle bis drei."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'
Python
# Python 3, openai 3.37.0 (pip install "openai==3.37.0"). Schlüssel nur aus der Umgebung.
import os
import uuid

from openai import APIError, OpenAI

api_key = os.environ.get("SICHERAI_API_KEY")
# Ohne eigenen Wert läse die Bibliothek OPENAI_API_KEY und sendete diesen Schlüssel an die sicherAI-API.
if not api_key:
    raise SystemExit("SICHERAI_API_KEY ist nicht gesetzt.")
# Die Bibliothek sendete diese Werte als Kopfzeilen mit; eine Zeile Authorization
# in OPENAI_CUSTOM_HEADERS ersetzte sogar den Schlüssel.
for name in ("OPENAI_ORG_ID", "OPENAI_PROJECT_ID", "OPENAI_CUSTOM_HEADERS"):
    if os.environ.get(name):
        raise SystemExit(f"{name} ist gesetzt.")

client = OpenAI(
    base_url="https://api.sicherai.com/v1",  # ohne base_url sendet die Bibliothek an api.openai.com
    api_key=api_key,
    max_retries=0,  # keine automatischen Wiederholungen
)

# 1 Modellliste: eine Seite; der Abruf verbraucht keine Token.
for model in client.models.list():
    print(model.id)

# 2 Antwort als JSON. Den Idempotency-Key einmal je Vorgang erzeugen;
#   eine eigene Wiederholung sendet denselben Wert.
idempotency_key = str(uuid.uuid4())
try:
    antwort = client.chat.completions.create(
        model="<modell-id>",
        messages=[{"role": "user", "content": "Synthetischer Testtext: Bitte antworte mit OK."}],
        extra_headers={"Idempotency-Key": idempotency_key},
    )
except APIError as fehler:
    # Vertragsfehler: code und, bei HTTP-Fehlern, die Kennung aus x-request-id.
    print(fehler.code, getattr(fehler, "request_id", None))
    raise
print(antwort.choices[0].message.content, antwort.usage.total_tokens)

# 3 Antwort als Stream. Mit include_usage kommt die Usage als letzter Chunk mit
#   choices: []; erst er zeigt, dass die Antwort vollständig ist.
stream = client.chat.completions.create(
    model="<modell-id>",
    messages=[{"role": "user", "content": "Synthetischer Testtext: Zähle bis drei."}],
    stream=True,
    stream_options={"include_usage": True},
)
usage = None
for teil in stream:
    if teil.choices:
        print(teil.choices[0].delta.content or "", end="")
    elif teil.usage:
        usage = teil.usage
if usage is None:
    raise RuntimeError("Der Stream endete ohne Usage und ist unvollständig.")
print()
print(usage.total_tokens, "Token")
Node
// Node 22 (ESM), openai 7.42.0 (npm install openai@7.42.0). Schlüssel nur aus der Umgebung.
import { randomUUID } from "node:crypto";
import OpenAI, { APIError } from "openai";

const apiKey = process.env.SICHERAI_API_KEY;
// Ohne eigenen Wert läse das SDK OPENAI_API_KEY und sendete diesen Schlüssel an die sicherAI-API.
if (!apiKey) throw new Error("SICHERAI_API_KEY ist nicht gesetzt.");
// Das SDK sendete diese Kopfzeilen mit; eine Zeile Authorization ersetzte den Schlüssel.
if (process.env.OPENAI_CUSTOM_HEADERS) throw new Error("OPENAI_CUSTOM_HEADERS ist gesetzt.");

const client = new OpenAI({
  baseURL: "https://api.sicherai.com/v1", // ohne baseURL sendet das SDK an api.openai.com
  apiKey,
  organization: null, // sonst sendete das SDK OPENAI_ORG_ID mit
  project: null, // sonst sendete das SDK OPENAI_PROJECT_ID mit
  maxRetries: 0, // keine automatischen Wiederholungen
});

// 1 Modellliste: eine Seite; der Abruf verbraucht keine Token.
const models = await client.models.list();
for (const model of models.data) console.log(model.id);

// 2 Antwort als JSON. Den Idempotency-Key setzt der Aufrufer als Kopfzeile;
//   eine eigene Wiederholung sendet denselben Wert.
const idempotencyKey = randomUUID();
try {
  const { data: completion, request_id: requestId } = await client.chat.completions
    .create(
      {
        model: "<modell-id>",
        messages: [{ role: "user", content: "Synthetischer Testtext: Bitte antworte mit OK." }],
      },
      { headers: { "Idempotency-Key": idempotencyKey } },
    )
    .withResponse();
  console.log(requestId, completion.choices[0]?.message.content, completion.usage?.total_tokens);
} catch (error) {
  // Vertragsfehler: HTTP-Status, code und die Kennung aus x-request-id.
  if (error instanceof APIError) console.error(error.status, error.code, error.requestID);
  throw error;
}

// 3 Antwort als Stream. Mit include_usage kommt die Usage als letzter Chunk mit
//   choices: []; erst er zeigt, dass die Antwort vollständig ist.
const controller = new AbortController(); // controller.abort() beendet den Stream
const stream = await client.chat.completions.create(
  {
    model: "<modell-id>",
    messages: [{ role: "user", content: "Synthetischer Testtext: Zähle bis drei." }],
    stream: true,
    stream_options: { include_usage: true },
  },
  { signal: controller.signal },
);
let usage = null;
for await (const chunk of stream) {
  if (chunk.choices.length > 0) process.stdout.write(chunk.choices[0]?.delta.content ?? "");
  else if (chunk.usage) usage = chunk.usage;
}
if (!usage) throw new Error("Der Stream endete ohne Usage und ist unvollständig.");
console.log("\n" + usage.total_tokens + " Token");