Lo que estamos construyendo
Dos bots pequeños, un cerebro compartido. Un archivo llamado llm.py posee todo lo que toca la API: la personalidad, el limitador de velocidad, la lógica de reintentos. Un bot de Discord y un bot de Telegram luego solo traducen eventos de la plataforma en una lista de mensajes y viceversa. Añade una tercera plataforma más tarde y solo escribirás el adaptador.
Instala las dependencias con pip install httpx discord.py python-telegram-bot. Exporta API_KEY desde tu página de cuenta, además de DISCORD_TOKEN y/o TELEGRAM_TOKEN desde la configuración de bot de cada plataforma. Ambos bots asumen Python 3.10 o superior.
El formato de la petición es la forma estándar de chat-completions de OpenAI, descrita en la guía rápida multilingüe. Aquí no necesitas un SDK.
El núcleo compartido: personalidad, limitador y reintentos
Pon la parte peligrosa en un solo lugar. El módulo a continuación mantiene una ventana deslizante de marcas de tiempo de peticiones y espera cuando se acerca a 240 llamadas por minuto, un margen deliberado bajo los 300 por minuto permitidos por clave. Un semáforo limita la concurrencia a cuatro para que un canal ocupado no dispare veinte peticiones a la vez.
# llm.py - shared by both bots
import asyncio
import os
import time
from collections import deque
import httpx
URL = "https://api.unrestrictedaiapi.com/v1/chat/completions"
PERSONA = "You are Pip, a sardonic bar-room storyteller. Keep replies under 150 words."
_stamps = deque() # timestamps of recent calls
_gate = asyncio.Semaphore(4) # at most 4 requests in flight
_client = httpx.AsyncClient(timeout=60.0)
async def _wait_for_slot(limit=240, window=60.0):
# Stay below the 300 requests/minute per-key limit with headroom.
while True:
now = time.monotonic()
while _stamps and now - _stamps[0] > window:
_stamps.popleft()
if len(_stamps) < limit:
_stamps.append(now)
return
await asyncio.sleep(window - (now - _stamps[0]) + 0.05)
async def chat(history):
"""history: list of {"role","content"} dicts, oldest first."""
messages = [{"role": "system", "content": PERSONA}] + list(history)
async with _gate:
for attempt in range(3):
await _wait_for_slot()
r = await _client.post(
URL,
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "uncensored", "messages": messages, "max_tokens": 400},
)
if r.status_code in (429, 503):
await asyncio.sleep(2 ** attempt)
continue
if r.status_code == 403:
return "I can't continue with that."
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "The service is busy. Try again in a moment."Revisa el manejo de estados. Un 429 o 503 espera 1, 2 y luego 4 segundos e intenta de nuevo. Un 403 significa que el filtro de contenido se activó, así que el bot responde con una línea neutral en lugar de reintentar. Cualquier otro error lanza una excepción, que la biblioteca de tu plataforma registrará. Si ves un 402, tu saldo está vacío o la prueba terminó; recarga el saldo prepago.
El adaptador de Discord
Los bots de Discord necesitan que la intención de contenido de mensaje esté activada en el portal de desarrolladores y también en el código, o msg.content llega vacío. El bot solo responde cuando se le menciona, lo que evita que lea cada mensaje en un servidor ocupado.
# discord_bot.py
import os
import time
from collections import defaultdict, deque
import discord
from llm import chat
intents = discord.Intents.default()
intents.message_content = True # also enable it in the developer portal
bot = discord.Client(intents=intents)
HISTORY_TURNS = 10 # user+assistant messages kept per channel
history = defaultdict(lambda: deque(maxlen=HISTORY_TURNS))
last_use = {} # user id -> last request time
COOLDOWN = 5.0 # seconds between requests per user
def chunks(text, size=1900):
return [text[i:i + size] for i in range(0, len(text), size)] or [""]
@bot.event
async def on_message(msg: discord.Message):
if msg.author.bot or bot.user not in msg.mentions:
return
# Age gate: guild text channels must be flagged NSFW. No DMs.
if not isinstance(msg.channel, discord.TextChannel) or not msg.channel.is_nsfw():
await msg.reply("I only chat in channels marked age-restricted (NSFW).")
return
now = time.monotonic()
if now - last_use.get(msg.author.id, 0) < COOLDOWN:
await msg.add_reaction("\u23f3")
return
last_use[msg.author.id] = now
text = msg.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not text:
return
h = history[msg.channel.id]
h.append({"role": "user", "content": f"{msg.author.display_name}: {text}"})
async with msg.channel.typing():
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for part in chunks(answer):
await msg.channel.send(part)
bot.run(os.environ["DISCORD_TOKEN"])El historial es un deque con maxlen=10 indexado por id de canal, así que cada canal es su propia conversación y los turnos antiguos caen automáticamente. Anteceder cada línea del usuario con el nombre de visualización del hablante permite al modelo distinguir a las personas en una sala compartida. Diez mensajes es un valor conservador; la ventana de contexto es de 100.000 tokens, así que puedes aumentarlo mucho antes de que importe.
El adaptador de Telegram
python-telegram-bot v20 y versiones posteriores es asíncrono, lo que se integra naturalmente con el núcleo compartido. chat_data es un diccionario por chat que la biblioteca mantiene por ti, así que el historial y el periodo de enfriamiento no necesitan código de almacenamiento adicional.
# telegram_bot.py (python-telegram-bot v20+)
import os
import time
from collections import deque
from telegram import Update
from telegram.ext import (ApplicationBuilder, CommandHandler, ContextTypes,
MessageHandler, filters)
from llm import chat
async def adult(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.chat_data["adult"] = True
await update.message.reply_text("Noted. Say something.")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"This bot produces adult fiction. Send /adult only if you are 18 or older."
)
async def talk(update: Update, context: ContextTypes.DEFAULT_TYPE):
cd = context.chat_data
if not cd.get("adult"):
await update.message.reply_text("Send /adult to confirm you are 18+.")
return
if time.monotonic() - cd.get("last", 0) < 4:
return # per-chat cooldown
cd["last"] = time.monotonic()
h = cd.setdefault("history", deque(maxlen=10))
h.append({"role": "user", "content": update.message.text})
await context.bot.send_chat_action(update.effective_chat.id, "typing")
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for i in range(0, len(answer), 4000): # Telegram limit is 4096 chars
await update.message.reply_text(answer[i:i + 4000])
app = ApplicationBuilder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("adult", adult))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, talk))
app.run_polling()run_polling() es la forma más sencilla de iniciar y no necesita una URL pública. Ten en cuenta que chat_data vive en memoria, así que un reinicio lo borra. Si necesitas que el historial sobreviva a reinicios, guarda el contenido del deque tú mismo en un archivo o base de datos.
Control de edad y la regla del canal NSFW
Esta API es solo para adultos, y cualquier cosa que construyas sobre ella hereda esa responsabilidad. Trata el control de edad como una función, no como un pensamiento secundario.
- Discord: responde solo en canales de la comunidad marcados como +18. La comprobación
channel.is_nsfw()no tiene coste y el bot rechaza en otros lugares, incluidos los mensajes directos. - Telegram: requiere una confirmación explícita
/adultpor chat antes de cualquier generación, y establece la regla en el texto de bienvenida. - Ambos: no ofrezcas el bot en espacios dirigidos a jóvenes, y elimina el acceso rápidamente si te enteras de que un usuario tiene menos de 18 años.
Un comando de confirmación es un filtro, no una prueba de edad. El contenido sexual que involucra a menores siempre es bloqueado por la API con un 403, incluyendo ficción y roleplay, y tu bot nunca debería intentar burlar esa respuesta.
Ejecutarlo y probarlo sin multitud
Inicia cada bot en su propia terminal con python discord_bot.py o python telegram_bot.py. Para la primera ejecución, crea un servidor de prueba privado o un chat solo contigo. Recorre una lista de verificación corta y capturarás casi todos los errores de principiante.
- Menciona al bot en un canal no NSFW y confirma que se niega. Esto prueba que el control funciona.
- Menciónalo en un canal marcado y envía dos mensajes rápidamente. El segundo debería obtener la reacción de reloj de arena, lo que prueba que el enfriamiento funciona.
- Mantén una conversación de cuatro mensajes, luego refiérete al primer mensaje. Si el bot recuerda, el historial está conectado correctamente.
- Establece temporalmente la clave de API a un valor incorrecto y confirma que el error aparece en tus registros en lugar de desaparecer silenciosamente.
- Pega un bloque largo de texto y observa cómo la respuesta se divide en varios mensajes bajo el límite de longitud de la plataforma.
Si las respuestas parecen genéricas, el problema suele ser la persona en lugar del código. Cambia una frase de PERSONA, reinicia y compara. Como la persona vive en el módulo compartido, un cambio afecta a ambos bots a la vez, lo cual es precisamente por lo que la estructura vale la pena a pesar del archivo adicional.
Cuando estés listo para desplegar, cualquier administrador de procesos siempre activo servirá. Los bots mantienen una conexión de larga duración con su plataforma y hacen llamadas HTTPS salientes cortas a la API, así que no necesitan puertos entrantes y casi ninguna memoria. Añade una política de reinicio para que un fallo no deje a tu comunidad hablando con un bot silencioso.
Límites de peticiones, tamaño del historial y coste
Tres perillas controlan cómo se comporta el bot bajo carga. El enfriamiento por usuario evita que una persona monopolice la clave. La ventana global protege el techo de 300 peticiones por minuto. El semáforo suaviza los picos. Si ejecutas varios procesos de bot en una clave, comparten ese techo, así que reduce el limit de cada proceso en consecuencia.
Estima el gasto antes de invitar a mucha gente. Suposiciones: cada llamada envía aproximadamente 1200 tokens de entrada (persona más diez turnos) y devuelve 250 tokens. Una llamada cuesta 1200 x $0,25 / 1.000.000 + 250 x $1,00 / 1.000.000 = $0,0003 + $0,00025 = $0,00055. Un servidor que produzca 2000 respuestas al día gastaría aproximadamente $1,10 al día bajo esas suposiciones. Tus números serán diferentes, así que registra el campo usage durante una semana y recalcula.
Para ajustar la personalidad en sí, lee la guía de prompts. Los precios se listan en la página de precios.
Endurecimiento antes de compartir el bot
Mantén los tokens fuera del control de código fuente; léelos desde el entorno como se muestra. Limita la longitud del mensaje antes de enviarlo a la API para que un pegado enorme no queme tu saldo. Registra errores pero no el texto del mensaje, ya que tus usuarios esperarían razonablemente que sus chats se mantengan privados. Añade un comando /reset que borra el deque de un canal, que es la solución más rápida cuando una conversación se desvía. Finalmente, pon una nota visible en el perfil del bot diciendo que escribe ficción para adultos y que sus respuestas son generadas.
Una elección de diseño más merece una frase: los bots esperan la respuesta completa en lugar de hacer streaming. Editar un mensaje de chat token por token choca directamente con los límites de edición de la plataforma, y para respuestas limitadas a unas 150 palabras la espera es corta. Si más tarde quieres escritura en vivo en tu propia interfaz web, activa stream: true allí, donde controlas el transporte. Hasta entonces, el indicador de escritura que ya envías es la forma más barata de mostrar que el bot está funcionando, y mantiene el código lo suficientemente corto como para leerlo en una sola sesión y auditarlo antes de invitar a alguien.