IT ▾
Uncensored Chatbot APIAccesso API diretto a un LLM senza censuraOttieni la chiave API

Uncensored Chatbot APIChat Web

Costruiamo una chat web in streaming (Proxy Express + JS puro)

In questo tutorial costruirai una piccola pagina di chat in circa 100 righe. Un piccolo server Express conserva la tua chiave segreta e inoltra le richieste a Uncensored Chatbot API, mentre un front-end in JavaScript puro legge la risposta in streaming parola per parola. Nessun framework, nessun passaggio di build e la tua chiave non tocca mai il browser.

Aggiornato

Punti chiave

  • Non chiamare mai l'API dal codice del browser; un endpoint server mantiene la chiave privata e ti permette di convalidare l'input.
  • Il proxy deve solo far passare gli eventi inviati dal server; la pagina analizza le righe di dati fino a [DONE].
  • Usa textContent durante lo streaming e sanitizza prima di renderizzare il markdown.
  • Limita la lunghezza della cronologia e max_tokens sul server così un utente non può spendere tutto il tuo saldo.

Cosa stiamo costruendo e perché un proxy

Immagina una singola pagina con una casella di testo e un registro scorrevole. Digiti una riga, la pagina invia la conversazione a /api/chat sul tuo server, e il server la inoltra al server remoto con la chiave reale allegata. La risposta arriva in streaming e ogni frammento appare istantaneamente. Daremo al bot una piccola personalità, un guardiano del faro di nome Wren, così la demo sembrerà viva.

Ti servono Node 18 o superiore (per fetch integrato) e una chiave API. Se non ne hai ancora una, ottieni il credito di prova gratuito dalla pagina chiave: i nuovi account ricevono $0,50 di credito per 7 giorni, senza bisogno di dettagli di pagamento. L'URL di base è https://api.uncensoredchatbotapi.com/v1 e l'ID del modello è uncensored.

Perché ti serve il proxy

È allettante incollare la chiave nel codice front-end e chiamare l'API direttamente. Non farlo. Tutto ciò che viene inviato a un browser può essere letto da chiunque apra gli strumenti di sviluppo e una chiave rubata significa un saldo svuotato. Un proxy risolve questo problema e ti offre tre vantaggi aggiuntivi.

  • Segretezza. La chiave vive in una variabile d'ambiente sul server.
  • Controllo. Decidi tu il prompt di sistema, la lunghezza della cronologia e max_tokens, così gli utenti non possono sovrascriverli.
  • Un luogo per le regole. Limiti per utente, controlli dell'età e policy di logging appartengono tutti a questo livello. La guida alla sicurezza si basa su questo stesso endpoint.

Passo 1: configura il progetto

Crea una cartella, inizializzala come progetto ES-module e installa Express. Tutto il resto è integrato.

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

Finirai con due cose: server.js alla radice e una cartella public che contiene la pagina e il suo script.

Passo 2: scrivi il proxy Express

Il server serve file statici ed espone un solo endpoint POST. Leggilo in tre parti. Prima sanitizza la cronologia in arrivo: solo i ruoli user e assistant passano, ogni messaggio è tagliato a 4.000 caratteri e si mantengono solo gli ultimi 30 turni. Secondo, aggiunge il tuo messaggio di sistema, che il browser non può mai cambiare. Terzo, chiama l'endpoint upstream con stream: true e trasmette i byte direttamente indietro.

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

Due scelte valgono la pena di essere spiegate. Passare i byte grezzi significa che non devi capire il formato degli eventi sul server. E restituire lo stato del server remoto per i errori permette alla pagina di reagire in modo sensato; ad esempio, un 402 significa che il tuo saldo è vuoto e un 429 significa che qualcuno sta andando troppo veloce.

Passo 3: la pagina e il lettore di stream

Ora il front-end. Salva questo come public/index.html; è deliberatamente spoglio così puoi ridecorarlo più tardi.

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

Successivamente, public/chat.js. La parte interessante è il ciclo di lettura. I blocchi di rete non rispettano i limiti di riga, quindi manteniamo un buffer, dividiamo per newline e tratteniamo l'ultima riga parziale finché non arrivano più dati. Ogni riga completa che inizia con data: è JSON, tranne il marcatore finale [DONE].

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

Nota l'array history. L'API non conserva memoria tra le chiamate, quindi la pagina reinvia l'intera conversazione ogni volta e il server la taglia. Avvia l'app e aprila in un browser:

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

Una nota sul rendering del markdown

I modelli di chat amano gli asterischi, le liste e le occasionali barriere di codice. La nostra demo mostra testo grezzo, che è sicuro. Quando vuoi un output bello, renderizza il markdown con una libreria, ma segui due regole. Esegui l'HTML attraverso un sanitizer prima di inserirlo con innerHTML, poiché un modello, o un utente che lo inganna, può emettere tag e attributi che non hai intenzione. E renderizza incrementalmente con cura: riprocessare l'intera risposta su ogni frammento va bene per messaggi brevi, ma il markdown incompleto può sfarfallare, quindi alcuni blocchi di codice mostrano testo grezzo durante lo streaming e passano all'output formattato quando lo streaming termina.

La formattazione del gioco di ruolo è una stranezza separata. Molti personaggi racchiudono le azioni tra asterischi, come *aggiusta la lampada*. Decidi se la tua app stili quelli come corsivi, e indica al personaggio nel prompt di sistema quale convenzione seguire. La nostra guida al design della persona mostra il wording del prompt per quello.

Passo 4: test di fumo dell'endpoint

Prima di dare la colpa al browser, prova il proxy direttamente. Se il terminale funziona, il server è a posto e qualsiasi bug rimanente vive nella pagina.

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

Il flag -N disabilita il buffering di curl, quindi dovresti vedere le righe data: apparire gradualmente, terminando con data: [DONE]. Se ottieni un errore JSON invece, leggi il suo stato: 401 significa che la chiave nel tuo ambiente è sbagliata, 402 significa che il saldo è vuoto, e un 404 significa che l'URL del server remoto ha un errore di battitura.

SintomoCausa probabileSoluzione
La pagina mostra il messaggio non disponibileIl server ha restituito un codice di stato diverso da 200Esegui il test curl e leggi lo status
Il testo appare solo alla fineUno strato proxy sta facendo buffering alla rispostaDisabilita il buffering per l'endpoint
Caratteri corrottiDecoder usato senza stream: trueMantieni l'opzione impostata su TextDecoder.decode
La risposta si interrompe a metà fraseraggiunto max_tokensAumenta il limite sul server

Con quelle quattro correzioni puoi diagnosticare quasi ogni problema di primo avvio in meno di un minuto. Tieni il comando curl negli appunti; è anche un utile controllo di salute dopo che avrai modificato il proxy in seguito.

Piccoli dettagli che lo fanno sembrare completo

Una chat in streaming è già gradevole, ma pochi dettagli separano una demo da un prodotto a cui si torna.

  • Disabilita il pulsante di invio mentre arriva una risposta. I doppi invii creano cronologie interlacciate che confondono sia l'utente che il modello.
  • Aggiungi un pulsante di arresto. Crea un AbortController, passa il suo segnale a fetch e chiama abort() al click. Salva tutto il testo arrivato finora come turno dell'assistente.
  • Persisti il registro. Mantieni la cronologia in sessionStorage così un refresh non cancella la conversazione, e offri un pulsante svuota chat che lo svuota.
  • Scorrimento automatico sensato. Segui il fondo solo quando l'utente è già vicino ad esso; altrimenti lascia che leggano le righe più vecchie in pace.
  • Mostra un errore gentile. Sostituisci la bolla vuota con un link di retry che reinvia l'ultimo messaggio dell'utente.

Ognuna di queste è una decina di righe di JavaScript puro, quindi resisti alla tentazione di ricorrere a un framework finché la tua interfaccia non ha davvero bisogno di componenti, routing o stato condiviso.

Rafforzamento e prossimi passi

Ora hai una chat funzionante. Prima che arrivino gli utenti reali, aggiungi alcune salvaguardie in server.js. Limita le richieste per IP o sessione, perché una singola chiave è consentita 300 richieste al minuto in totale, e un singolo utente entusiasta potrebbe usarle tutte. Gestisci gli errori del server remoto esplicitamente: un 503 con upstream_busy merita un pulsante di retry amichevole, e un 403 content_blocked merita un messaggio chiaro piuttosto che una bolla vuota. Mantieni max_tokens modesto; 500 è più che sufficiente per la chat, mentre il massimo consentito è 32.000 per richiesta.

Poi pensa al costo. Come illustrazione, supponi che ogni turno invii 1.200 token di prompt e riceva 250 token di completamento. Questo è circa $0,0003 per l'input e $0,00025 per l'output, circa $0,00055 a turno. Questi conteggi di token sono assunzioni, quindi misura i tuoi con il blocco usage. La documentazione copre quel campo, e la guida ai LLM ospitati senza censura spiega cosa aspettarsi da questo tipo di servizio.

Domande e risposte

Posso chiamare l'API direttamente dal browser?

Tecnicamente sì, ma esporresti la tua chiave a ogni visitatore. Usa una route del server come quella in questo tutorial così la chiave rimane in una variabile d'ambiente.

Il server deve analizzare lo stream?

No. Può inoltrare i byte invariati e la pagina legge le righe di dati. Analizza sul server solo se vuoi registrare il testo o filtrare l'output.

Perché la mia risposta arriva tutta insieme?

C'è un buffering in corso. Controlla che la richiesta imposti stream a true e che qualsiasi proxy inverso o strato di compressione non stia trattenendo la risposta.

Come dovrei dare memoria al bot?

Reinvia la conversazione in messaggi a ogni chiamata, tagliando i turni più vecchi quando cresce. La finestra di contesto di 100.000 token è condivisa con la risposta.

La tua chiave è a un modulo di distanza

Crea un account, copia la chiave, cambia l'URL di base. È tutta la configurazione.

Ottieni chiave APILeggi la documentazione