NL ▾
Uncensored Chatbot APIDirecte API-toegang tot één ongecensureerd LLMAPI-sleutel aanvragen

Uncensored Chatbot APIWeb Chat

Bouw een streaming webchat (Express Proxy + Plain JS)

In deze tutorial bouw je een klein chatpagina in ongeveer 100 regels. Een kleine Express-server houdt je geheime sleutel vast en relayt verzoeken naar de Uncensored Chatbot API, terwijl een eenvoudige JavaScript-frontend het gestreamde antwoord woord voor woord leest. Geen framework, geen buildstap, en je sleutel komt nooit in de browser terecht.

Bijgewerkt

Belangrijkste punten

  • Roep de API nooit aan vanuit browsercode; een serverroute houdt de sleutel privé en stelt je in staat input te valideren.
  • De proxy hoeft alleen server-verzonden gebeurtenissen door te geven; de pagina parseert gegevensregels tot [DONE].
  • Gebruik textContent tijdens streaming en sanitiseer voordat je markdown render.
  • Beperk de lengte van de geschiedenis en max_tokens op de server zodat één gebruiker niet je hele saldo kan opgebruiken.

Wat we bouwen, en waarom een proxy

Stel je een enkele pagina voor met een tekstvak en een scrollend log. Je typt een regel, de pagina post het gesprek naar /api/chat op je eigen server, en jouw server stuurt het door naar de upstream met de echte sleutel erbij. Het antwoord stroomt terug en elk fragment verschijnt op het moment dat het aankomt. We geven de bot een klein persoonlijkheid, een vuurtorenwachter genaamd Wren, zodat de demo levendig aanvoelt.

Je hebt Node 18 of nieuwer nodig (voor ingebouwde fetch) en een API-sleutel. Heb je er nog geen? Pak dan de gratis proeftegoed van de sleutelpagina: nieuwe accounts krijgen $0,50 tegoed voor 7 dagen, zonder betalingsgegevens. De basis-URL is https://api.uncensoredchatbotapi.com/v1 en de model id is uncensored.

Waarom je de proxy nodig hebt

Het is verleidelijk om de sleutel in frontend-code te plakken en de API rechtstreeks aan te roepen. Doe dat niet. Alles wat naar een browser wordt gestuurd, kan door iedereen worden gelezen die de ontwikkelaarshulpmiddelen opent, en een gelekte sleutel betekent een leeg saldo. Een proxy lost dit op en geeft je drie extra voordelen.

  • Vertrouwelijkheid. De sleutel staat in een omgevingsvariabele op de server.
  • Controle. Jij bepaalt het systeem prompt, de lengte van de geschiedenis en max_tokens, zodat gebruikers ze niet kunnen overschrijven.
  • Een plek voor regels. Per-gebruiker limieten, leeftijdsgrenzen en loggingbeleid horen allemaal in deze laag. De veiligheidsgids bouwt voort op precies deze route.

Stap 1: project instellen

Maak een map, initialiseer deze als een ES-module-project en installeer Express. Alles anders is ingebouwd.

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

Je eindigt met twee dingen: server.js in de root en een public map met de pagina en het script.

Stap 2: schrijf de Express proxy

De server serveert statische bestanden en exposeert één POST-route. Lees het in drie delen. Eerst sanitizeert het de binnenkomende history: alleen user en assistant rollen slagen, elk bericht wordt bijgesneden tot 4.000 tekens, en alleen de laatste 30 beurten worden bewaard. Ten tweede voegt het je eigen systeembericht toe, dat de browser nooit kan wijzigen. Ten derde roept het de upstream endpoint aan met stream: true en pipeert het de bytes direct terug.

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"));

Twee keuzes zijn het uitleggen waard. De ruwe bytes doorgeven betekent dat je het gebeurtenisformaat op de server helemaal niet hoeft te begrijpen. En het teruggeven van de upstream status voor fouten stelt de pagina in staat verstandig te reageren; bijvoorbeeld, een 402 betekent dat je saldo leeg is en een 429 betekent dat iemand te snel gaat.

Stap 3: de pagina en de stream reader

Nu de front-end. Sla dit op als public/index.html; het is bewust kaal zodat je het later kunt stylen.

<!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>

Volgens public/chat.js. Het interessante deel is de leescyclus. Netwerkfragmenten houden zich niet aan regelgrenzen, dus we houden een buffer bij, splitsen op nieuwe regels en houden de laatste gedeeltelijke regel vast tot er meer data binnenkomt. Elke volledige regel die begint met data: is JSON, behalve de laatste [DONE] marker.

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 });
});

Merk de history array op. De API houdt geen geheugen bij tussen aanroepen, dus de pagina stuurt de hele conversatie opnieuw elke keer, en de server knipt het. Start de app en open het in een browser:

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

Een opmerking over markdown rendering

Chatmodellen houden van sterretjes, lijsten en af en toe een code fence. Onze demo toont ruwe tekst, wat veilig is. Wanneer je mooie output wilt, render markdown met een library, maar volg twee regels. Voer de HTML door een sanitizer voordat je het invoegt met innerHTML, omdat een model, of een gebruiker die het bedriegt, tags en attributen kan genereren die je niet bedoelde. En render incrementeel met zorg: het opnieuw parseren van het hele antwoord bij elk fragment is prima voor korte berichten, maar halve markdown kan flikkeren, dus sommige builders tonen ruwe tekst tijdens streaming en wisselen naar opgemaakte output wanneer de stream eindigt.

Rollenspel-opmaak is een aparte eigenaardigheid. Veel karakters omhullen acties met sterretjes, zoals *past de lamp aan*. Beslis of je app deze als cursief weergeeft en geef het karakter in de systeemprompt aan welke conventie het moet volgen. Onze persona-ontwerp handleiding toont prompt wording daarvoor.

Stap 4: smoke-test de route

Blameer de browser niet voordat je de proxy direct test. Als de terminal werkt, is de server in orde en zit de resterende bug in de pagina.

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

De -N vlag schakelt curl's eigen buffering uit, dus je zou geleidelijk data: regels moeten zien verschijnen, eindigend met data: [DONE]. Als je een JSON fout krijgt, lees dan de status: 401 betekent dat de sleutel in je omgeving verkeerd is, 402 betekent dat het saldo leeg is, en een 404 betekent dat de upstream URL een typefout heeft.

SymptoomWaarschijnlijke oorzaakOplossing
Pagina toont het onbeschikbare berichtServer retourneerde een niet-200 statusVoer de curl test uit en lees de status
Tekst verschijnt alleen aan het eindeEen proxy-laag buffer het antwoordSchakel buffering uit voor de route
Beschadigde tekensDecoder gebruikt zonder stream: trueHoud de optie ingesteld op TextDecoder.decode
Antwoord valt midden in een zin afmax_tokens bereiktVerhoog het plafond op de server

Met die vier oplossingen kun je bijna elk eerste-start probleem binnen een minuut diagnosticeren. Houd de curl command in je notities; het is ook een handige health check nadat je de proxy later wijzigt.

Kleine details die het af maken

Een chat die streamt is al prettig, maar een paar details maken het verschil tussen een demo en een product waar mensen naartoe terugkeren.

  • Schakel de verzendknop uit terwijl een antwoord aankomt. Dubbele verzendingen creëren verstrengelde geschiedenissen die zowel de gebruiker als het model verwarren.
  • Voeg een stopknop toe. Maak een AbortController aan, geef het signaal door aan fetch en roep abort() aan bij klikken. Bewaar de tekst die tot dan toe is ontvangen als de assistant-turn.
  • Bewaar de logboek. Bewaar de geschiedenis in sessionStorage zodat een verversen het gesprek niet wist, en bied een knop 'geschiedenis wissen' die deze leegmaakt.
  • Scroll automatisch op een verstandige manier. Volg de onderkant alleen wanneer de gebruiker daar al dichtbij is; laat ze anders rustig oudere regels lezen.
  • Toon een zachte foutmelding. Vervang de lege ballon door een herstelkoppeling die het laatste gebruikersbericht opnieuw verzendt.

Elk van deze is een dozijn regels eenvoudige JavaScript, dus weerhoud je ervan een framework te gebruiken totdat je interface echt componenten, routing of gedeelde state nodig heeft.

Versteviging en vervolgstappen

Je hebt nu een werkende chat. Voeg voordat echte gebruikers binnenkomen een paar beschermingsmaatregelen toe in server.js. Beperk verzoeken per IP of sessie, omdat een enkele sleutel in totaal 300 verzoeken per minuut toestaat, en één enthousiaste gebruiker zou alles kunnen opbruiken. Verwerk upstream-fouten expliciet: een 503 met upstream_busy verdient een vriendelijke herstelknop, en een 403 content_blocked verdient een duidelijke boodschap in plaats van een lege ballon. Houd max_tokens bescheiden; 500 is meer dan genoeg voor chat, terwijl het toegestane maximum 32.000 per verzoek is.

Denk dan na over kosten. Als illustratie, neem aan dat elke wending 1.200 prompt tokens verstuurt en 250 completion tokens ontvangt. Dat is ongeveer $0,0003 voor input en $0,00025 voor output, ongeveer $0,00055 per wending. Die token counts zijn aannames, dus meet je eigen met de usage chunk. De docs behandelen dat veld, en de gids voor gehoste ongecensureerde LLMs legt uit wat je kunt verwachten van dit soort service.

Vragen en antwoorden

Kan ik de API rechtstreeks vanuit de browser aanroepen?

Technisch gezien wel, maar dan maak je je sleutel bloot aan elke bezoeker. Gebruik een server route zoals in deze tutorial, zodat de sleutel in een environment variable blijft.

Moet de server de stream parseren?

Nee. Het kan de bytes ongewijzigd doorsturen, en de pagina leest de data-regels. Parseer op de server alleen als je tekst wilt loggen of output wilt filteren.

Waarom komt mijn antwoord ineens in één keer?

Iets ertussen bufferd. Controleer of het verzoek stream op true zet en of een reverse proxy of compressielaag de response niet vasthoudt.

Hoe geef ik de bot geheugen?

Stuur de conversatie bij elke aanroep opnieuw als berichten, en knip de oudste wendingen als deze te lang wordt. Het contextvenster van 100.000 tokens wordt gedeeld met het antwoord.

Je sleutel is nog maar één formulier verwijderd

Maak een account aan, kopieer de sleutel, pas de base URL aan. Dat is de hele setup.

API-sleutel ophalenLees de docs