DE ▾
Uncensored Chatbot APIDirekter API-Zugriff auf ein unzensiertes LLMAPI-Schlüssel erhalten

Uncensored Chatbot APIWeb-Chat

Bauen wir einen Streaming-Web-Chat (Express-Proxy + reines JS)

In diesem Tutorial baust du eine kleine Chat-Seite in etwa 100 Zeilen. Ein kleiner Express-Server hält deinen geheimen Schlüssel und leitet Anfragen an die Uncensored Chatbot API weiter, während ein einfaches JavaScript-Frontend die gestreamte Antwort wortweise liest. Kein Framework, kein Build-Schritt, und dein Schlüssel berührt nie den Browser.

Aktualisiert

Wichtige Punkte

  • Rufe die API niemals aus Browser-Code auf; ein Server-Endpunkt hält den Schlüssel privat und ermöglicht die Eingabevalidierung.
  • Der Proxy muss nur die Server-Sent-Events durchreichen; die Seite parst Datenzeilen bis [DONE].
  • Verwende textContent während des Streamings und sanitize vor dem Rendern von Markdown.
  • Begrenze die Verlaufslänge und max_tokens auf dem Server, damit ein Benutzer nicht dein gesamtes Guthaben ausgeben kann.

Was wir bauen und warum ein Proxy

Stell dir eine einzelne Seite mit einem Textfeld und einem scrollbaren Protokoll vor. Du gibst eine Zeile ein, die Seite sendet die Konversation an /api/chat auf deinem eigenen Server, und dein Server leitet sie mit angehängtem echten Schlüssel nach oben weiter. Die Antwort wird gestreamt, und jeder Fragment erscheint im Moment seiner Ankunft. Wir geben dem Bot eine kleine Persönlichkeit, einen Leuchtturmwärter namens Wren, damit die Demo lebendig wirkt.

Du benötigst Node 18 oder neuer (für das eingebaute fetch) und einen API-Schlüssel. Falls du noch keinen hast, hole dir das kostenlose Testguthaben auf der Schlüssel-Seite: Neue Konten erhalten 7 Tage lang $0,50 Guthaben, keine Zahlungsdetails erforderlich. Die Base URL ist https://api.uncensoredchatbotapi.com/v1 und die Modell-ID ist uncensored.

Warum du den Proxy benötigst

Es ist verlockend, den Schlüssel in Frontend-Code einzufügen und die API direkt aufzurufen. Bitte tu das nicht. Alles, was an einen Browser ausgeliefert wird, kann von jedem gelesen werden, der die Entwickler-Tools öffnet, und ein geöffneter Schlüssel bedeutet ein leeres Guthaben. Ein Proxy behebt dies und bietet dir drei zusätzliche Vorteile.

  • Vertraulichkeit. Der Schlüssel liegt in einer Umgebungsvariable auf dem Server.
  • Kontrolle. Du entscheidest über den System-Prompt, die Verlaufslänge und max_tokens, sodass Benutzer sie nicht überschreiben können.
  • Ein Platz für Regeln. Pro-Nutzer-Limits, Altersfreigaben und die Protokollierungsrichtlinie gehören in diese Schicht. Die Sicherheitsanleitung baut auf genau dieser Route auf.

Schritt 1: Projekt einrichten

Erstelle einen Ordner, initialisiere ihn als ES-Modul-Projekt und installiere Express. Alles andere ist eingebaut.

mkdir lantern-chat && cd lantern-chat
npm init -y
npm pkg set type=module
npm install express
mkdir public

Du wirst am Ende zwei Dinge haben: server.js im Root-Verzeichnis und einen public-Ordner, der die Seite und ihr Skript enthält.

Schritt 2: Den Express-Proxy schreiben

Der Server stellt statische Dateien bereit und bietet eine einzige POST-Route. Lies sie in drei Teilen. Erstens bereinigt er die eingehende Historie: Nur die Rollen user und assistant werden durchgelassen, jede Nachricht wird auf 4.000 Zeichen gekürzt, und nur die letzten 30 Abschnitte werden behalten. Zweitens fügt er deine eigene Systemnachricht hinzu, die der Browser niemals ändern kann. Drittens ruft er den Endpunkt mit stream: true auf und leitet die Bytes direkt weiter.

import express from "express";

const app = express();
app.use(express.json({ limit: "256kb" }));
app.use(express.static("public"));

const UPSTREAM = "https://api.uncensoredchatbotapi.com/v1/chat/completions";
const SYSTEM = "You are Wren, a dry-witted night-shift lighthouse keeper. Stay in character.";

app.post("/api/chat", async (req, res) => {
  const history = Array.isArray(req.body.messages) ? req.body.messages : [];
  // Keep only well-formed turns and cap the history we forward.
  const turns = history
    .filter((m) => ["user", "assistant"].includes(m.role) && typeof m.content === "string")
    .slice(-30)
    .map((m) => ({ role: m.role, content: m.content.slice(0, 4000) }));

  if (turns.length === 0) return res.status(400).json({ error: "no messages" });

  const upstream = await fetch(UPSTREAM, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "uncensored",
      messages: [{ role: "system", content: SYSTEM }, ...turns],
      stream: true,
      max_tokens: 500,
      temperature: 0.8,
    }),
  });

  if (!upstream.ok) {
    const info = await upstream.json().catch(() => ({}));
    return res.status(upstream.status).json(info);
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  for await (const chunk of upstream.body) res.write(chunk);
  res.end();
});

app.listen(3000, () => console.log("http://localhost:3000"));

Zwei Optionen sind es wert, erklärt zu werden. Die Rohdaten durchzuleiten bedeutet, dass du das Ereignisformat auf dem Server überhaupt nicht verstehen musst. Und die Rückgabe des Upstream-Status bei Fehlern ermöglicht es der Seite, sinnvoll zu reagieren; zum Beispiel bedeutet 402, dass dein Guthaben leer ist, und 429 bedeutet, dass jemand zu schnell ist.

Schritt 3: Die Seite und der Stream-Reader

Jetzt das Frontend. Speichere dies als public/index.html; es ist bewusst kahl gehalten, damit du es später neu gestalten kannst.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Lantern Chat</title>
  <style>
    body { font: 16px system-ui; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
    #log { min-height: 320px; border: 1px solid #ccc; padding: 1rem; white-space: pre-wrap; }
    .me { color: #234; font-weight: 600; }
    form { display: flex; gap: .5rem; margin-top: 1rem; }
    input { flex: 1; padding: .6rem; }
  </style>
</head>
<body>
  <h1>Lantern Chat</h1>
  <div id="log"></div>
  <form id="form">
    <input id="text" autocomplete="off" placeholder="Say something...">
    <button>Send</button>
  </form>
  <script src="chat.js"></script>
</body>
</html>

Als Nächstes public/chat.js. Der interessante Teil ist die Leseschleife. Netzwerkchunks respektieren keine Zeilengrenzen, daher behältst du einen buffer, splittest nach Zeilenumbrüchen und hältst die letzte unvollständige Zeile zurück, bis weitere Daten eintreffen. Jede vollständige Zeile, die mit data: beginnt, ist JSON, mit Ausnahme des abschließenden [DONE]-Markers.

const log = document.getElementById("log");
const form = document.getElementById("form");
const input = document.getElementById("text");
const history = [];

function addLine(cls, text) {
  const div = document.createElement("div");
  div.className = cls;
  div.textContent = text;          // textContent, never innerHTML, for untrusted text
  log.appendChild(div);
  return div;
}

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  input.value = "";
  history.push({ role: "user", content: text });
  addLine("me", "You: " + text);
  const bubble = addLine("bot", "");

  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages: history }),
  });
  if (!res.ok) {
    bubble.textContent = "(the chat is unavailable right now)";
    history.pop();
    return;
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "", reply = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();            // keep a partial line for the next read
    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const data = line.slice(6).trim();
      if (data === "[DONE]") continue;
      const json = JSON.parse(data);
      const piece = json.choices?.[0]?.delta?.content;
      if (piece) {
        reply += piece;
        bubble.textContent = reply;
      }
    }
  }
  history.push({ role: "assistant", content: reply });
});

Beachte das history-Array. Die API speichert keinen Zustand zwischen den Aufrufen, daher sendet die Seite die gesamte Konversation jedes Mal erneut, und der Server kürzt sie. Starte die App und öffne sie in einem Browser:

export API_KEY="paste-your-key-here"
node server.js

Ein Hinweis zum Markdown-Rendern

Chat-Modelle lieben Sternchen, Listen und gelegentliche Code-Fences. Unsere Demo zeigt Rohtext, was sicher ist. Wenn du schöne Ausgaben möchtest, render Markdown mit einer Bibliothek, aber befolge zwei Regeln. Führe HTML durch einen Sanitizer, bevor du es mit innerHTML einfügst, da ein Modell oder ein Benutzer, der es austrickst, Tags und Attribute erzeugen kann, die du nicht beabsichtigt hast. Und render inkrementell mit Sorgfalt: Das erneute Parsen der gesamten Antwort bei jedem Fragment ist für kurze Nachrichten in Ordnung, aber unvollständiges Markdown kann flackern, daher zeigen einige Builder Rohtext während des Streamings an und wechseln bei Streamende zu formatierter Ausgabe.

Rollenspiel-Formatierung ist eine separate Eigenheit. Viele Zeichen wickeln Aktionen in Sternchen, wie *passt die Lampe an*. Entscheide, ob deine App diese als Kursiv formatiert, und teile dem Charakter im System-Prompt mit, welcher Konvention er folgen soll. Unsere Anleitung zur Persona-Design zeigt Prompt-Wortwahl dafür.

Schritt 4: Route kurz testen

Bevor du den Browser beschuldigst, frage den Proxy direkt ab. Wenn das Terminal funktioniert, ist der Server in Ordnung und jeder verbleibende Fehler liegt in der Seite.

curl -N http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Wren, is the fog coming in?"}]}'

Die -N-Flagge deaktiviert das eigene Puffern von curl, also solltest du sehen, wie data:-Zeilen allmählich erscheinen, endend mit data: [DONE]. Wenn du stattdessen einen JSON-Fehler erhältst, lies seinen Status: 401 bedeutet, dass der Schlüssel in deiner Umgebung falsch ist, 402 bedeutet, dass das Guthaben leer ist, und 404 bedeutet, dass die Upstream-URL einen Tippfehler enthält.

SymptomWahrscheinliche UrsacheLösung
Seite zeigt die UnverfügbarkeitsmeldungServer gab einen Nicht-200-Status zurückFühre den curl-Test aus und lies den Status
Text erscheint nur am EndeEine Proxy-Schicht puffert die AntwortPuffern für die Route deaktivieren
Verzerrte ZeichenDecoder verwendet ohne stream: trueHalte die Option auf TextDecoder.decode eingestellt
Antwort bricht mitten im Satz abmax_tokens erreichtErhöhe das Limit auf dem Server

Mit diesen vier Korrekturen kannst du fast jedes Erststartproblem innerhalb einer Minute diagnostizieren. Behalte den curl-Befehl in deinen Notizen; er ist auch ein praktischer Health-Check, nachdem du den Proxy später geändert hast.

Kleine Feinheiten, die es fertig aussehen lassen

Ein Chat mit Streaming ist bereits angenehm, aber ein paar Details trennen eine Demo von einem Produkt, zu dem man zurückkehrt.

  • Deaktiviere die Senden-Schaltfläche, während eine Antwort eintrifft. Doppelte Absendungen erzeugen verschachtelte Historien, die sowohl den Benutzer als auch das Modell verwirren.
  • Füge eine Stopp-Schaltfläche hinzu. Erstelle einen AbortController, übergebe sein Signal an fetch und rufe abort() beim Klicken auf. Speichere den bisher eingetroffenen Text als Antwort des Assistenten.
  • Persistiere das Protokoll. Behalte die Historie in sessionStorage, damit ein Neuladen die Konversation nicht löscht, und biete eine Schaltfläche zum Leeren an, die sie leert.
  • Automatisch sinnvoll scrollen. Folge nur dem Ende, wenn der Benutzer bereits nahe daran ist; lasse ihn sonst ältere Zeilen in Ruhe lesen.
  • Zeige eine sanfte Fehlermeldung. Ersetze die leere Blase durch einen Wiederholungslink, der die letzte Benutzernachricht erneut sendet.

Jede dieser Maßnahmen besteht aus einem Dutzend Zeilen reinem JavaScript, also widerstehe dem Drang, ein Framework zu verwenden, bis deine Oberfläche wirklich Komponenten, Routing oder geteilten Zustand benötigt.

Absicherung und nächste Schritte

Du hast nun einen funktionierenden Chat. Bevor echte Benutzer kommen, füge einige Schutzmaßnahmen in server.js hinzu. Begrenze Anfragen pro IP oder Sitzung, da ein einzelner Schlüssel insgesamt 300 Anfragen pro Minute erlaubt sind und ein eifriger Benutzer das gesamte Kontingent aufbrauchen könnte. Behandle Upstream-Fehler explizit: Ein 503 mit upstream_busy verdient einen freundlichen Retry-Button, und ein 403 content_blocked verdient eine klare Nachricht statt einer leeren Blase. Halte max_tokens bescheiden; 500 reichen für Chat völlig aus, während das erlaubte Maximum 32.000 pro Anfrage beträgt.

Denke dann über die Kosten nach. Als Beispiel nehmen wir an, dass jede Anfrage 1.200 Prompt-Token sendet und 250 Completion-Token empfängt. Das sind etwa $0,0003 für Input und $0,00025 für Output, grob $0,00055 pro Anfrage. Diese Token-Anzahlen sind Annahmen, also miss deine eigenen mit dem usage-Chunk. Die docs behandeln dieses Feld, und der Guide zu gehosteten unzensierten LLMs erklärt, was du von dieser Art von Dienst erwarten kannst.

Fragen und Antworten

Kann ich die API direkt vom Browser aus aufrufen?

Technisch ja, aber du würdest deinen Schlüssel jedem Besucher offenlegen. Nutze einen Server-Endpunkt wie in diesem Tutorial, damit der Schlüssel in einer Umgebungsvariable bleibt.

Muss der Server den Stream parsen?

Nein. Es kann die Bytes unverändert weiterleiten, und die Seite liest die Datenzeilen. Nur auf dem Server parsen, wenn du Text protokollieren oder Ausgabe filtern möchtest.

Warum kommt meine Antwort auf einmal an?

Etwas dazwischen puffert. Prüfe, dass die Anfrage stream auf true setzt und dass kein Reverse-Proxy oder Komprimierungs-Layer die Antwort zurückhält.

Wie gebe ich dem Bot Gedächtnis?

Sende die Konversation in Nachrichten bei jedem Aufrf erneut und kürze die ältesten Abschnitte, wenn sie wächst. Das 100.000-Token-Kontextfenster wird mit der Antwort geteilt.

Dein Schlüssel ist nur ein Formular entfernt

Erstelle ein Konto, kopiere den Schlüssel, ändere die Base URL. Das ist die gesamte Einrichtung.

API-Schlüssel erhaltenLies die Dokumentation