Discord-бот на Claude или GPT: минимальный пример

Двадцать строк кода и один API-ключ — и в Discord-канале уже отвечает Claude или GPT. Разбираем рабочий пример и типичные ошибки под нагрузкой.

4 мин · · Обновлено · разработка

Discord-бот на своём ключе — это по сути один обработчик события и один HTTP-запрос. Сложность появляется не в коде бота, а в том, что Discord ждёт быстрый ответ, а модель иногда думает секундами, и в том, что канал с активным чатом легко упирается в лимит расхода именно вашего ключа (общего rate limit на самом шлюзе нет).

Минимальный бот на discord.js

bash
// bot.js
import { Client, GatewayIntentBits, Events } from "discord.js";

const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
});

client.on(Events.MessageCreate, async (message) => {
  if (message.author.bot || !message.content.startsWith("!ask ")) return;
  const question = message.content.slice(5);

  await message.channel.sendTyping();

  try {

Обратите внимание на message.channel.sendTyping() — индикатор «печатает» держит канал живым, пока модель формирует ответ, и это особенно заметно на длинных ответах или если модель рассуждает перед ответом (extended thinking), когда первый токен приходит не мгновенно.

Anthropic-схема, если нужен именно Claude

Discord.js не завязан на формат тела запроса — так же легко вызвать /v1/messages:

bash
const r = await fetch("https://api.zukko.pro/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
    "Content-Type": "application/json",
    "anthropic-version": "2023-06-01"
  },
  body: JSON.stringify({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: question }]
  })
});
const data = await r.json();
const reply = data.content[0].text;

Разница только в форме тела и в том, что ответ лежит в content[0].text, а не в choices[0].message.content.

Лимит ключа — реальная проблема активного сервера

В большом Discord-сервере всплеск сообщений в !ask за минуту — обычное дело, и здесь легко упереться не в лимит модели, а в лимит собственного ключа: 429 с кодом key_daily_limit_reached или key_monthly_limit_reached означает, что дневной (сбрасывается в 00:00 UTC) или месячный лимит расхода, выставленный на странице ключа в кабинете, исчерпан. Своего rate limit у шлюза нет — прочие 429 приходят от провайдера модели. Ключевой лимит стоит ловить отдельно и показывать пользователям bot-специфичное сообщение, а не «сервис недоступен»:

bash
if (r.status === 429) {
  const err = await r.json();
  if (err.error?.code === "key_daily_limit_reached") {
    return message.reply("Бот исчерпал дневной лимит запросов, попробуйте завтра.");
  }
  return message.reply("Модель временно перегружена у провайдера, подождите немного.");
}

402 — баланс закончился, а не бот сломался

bash
{"error": {"message": "Недостаточно средств на балансе", "code": "insufficient_balance"}}

Проверка баланса происходит раньше, чем запрос уходит к модели, так что упавшая попытка ничего не стоит. Для бота это значит, что можно смело показывать пользователю честное сообщение про баланс вместо общей ошибки — это не баг в коде, а сигнал пополнить счёт в https://zukko.pro/app.

Не спамьте одинаковыми запросами — используйте кэш

Если несколько человек в канале задают один и тот же частый вопрос (правила сервера, FAQ), есть смысл включить кэш ответов заголовком X-Zukko-Cache: true (температура 0 тоже включает кэш, но только у моделей, которые принимают этот параметр, — не у Claude). Запросы одного пользователя с полностью совпадающим телом кэшируются до 7 суток для чата, повторный ответ приходит мгновенно и бесплатно, а в ответе будет заголовок X-Zukko-Cache: hit — удобно логировать его, чтобы видеть, сколько ответов бот вообще не платит.

Idempotency-Key на случай дублей

Discord иногда доставляет одно и то же событие дважды при сетевых задержках на своей стороне. Если бот реагирует на событие, а не только на прямой вызов пользователя, стоит добавить Idempotency-Key на основе id сообщения — тогда случайный дубль события не спишет деньги повторно:

bash
headers: {
  "Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
  "Content-Type": "application/json",
  "Idempotency-Key": `discord-${message.id}`
}

Разные модели для разных серверов на одном боте

Если один бот стоит в нескольких серверах, удобно не хардкодить модель, а хранить её в конфиге на сервер и подтягивать список актуальных id без своего кэша:

bash
const models = await fetch("https://api.zukko.pro/v1/models").then(r => r.json());

Эндпоинт публичный, ключ не нужен, так что можно смело вызывать его при старте бота и валидировать конфиг каждого сервера — если администратор вписал в настройки устаревший или неверный id модели, бот заметит это ещё до первого запроса пользователя, а не выдаст model_not_found посреди диалога.

Ограничение доступа по ролям и по ключу одновременно

Лимит на уровне ключа (дневной и месячный расход в долларах, список разрешённых моделей) настраивается на странице ключа в кабинете и защищает от перерасхода на уровне всего бота. Внутри самого бота стоит дополнительно ограничивать команду !ask по ролям Discord — это не заменяет лимит ключа, а работает вместе с ним: лимит ключа — это потолок расхода в деньгах, а роли — это то, кто вообще может до него достучаться. Если бот стоит в нескольких серверах с одним ключом, разумно завести по ключу на крупный сервер, чтобы лимит одного сообщества не блокировал остальных при всплеске активности.

Если бот не отвечает вообще

Прежде чем разбирать код, стоит проверить связку в изоляции — тем же запросом, что использует бот, но напрямую через curl с тем же ключом. Если он проходит, проблема в самом боте (не тот intent, обрезанный токен из .env, не тот путь); если нет — ответ сервера назовёт причину: неверный ключ, пустой баланс или превышенный лимит. Отдельно стоит проверить публичный GET https://api.zukko.pro/api/status — там JSON со статусом моделей; если нужная модель недоступна, дело не в боте вообще.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.

Discord-бот на Claude или GPT API — рабочий код · Zukko