
How to Build an AI-Powered Discord Bot with Multiple LLMs
How to Build an AI-Powered Discord Bot with Multiple LLMs#
Discord bots powered by AI are transforming community engagement. This comprehensive guide shows you how to build a production-ready Discord bot that supports multiple AI models (GPT-5, Claude, Gemini, and more) with slash commands, conversation context, and cost optimization.
What You'll Build#
By the end of this tutorial, you'll have a Discord bot that:
- ✅ Supports 10+ AI models (GPT-5, Claude, Gemini, DeepSeek, etc.)
- ✅ Uses slash commands for easy interaction
- ✅ Maintains conversation context per channel
- ✅ Handles rate limiting and errors gracefully
- ✅ Costs 30-50% less than using official APIs
- ✅ Includes admin controls and usage tracking
Live demo: /ask gpt-5 What is quantum computing?
Prerequisites#
- Python 3.9+ installed
- Discord account and server
- Crazyrouter API key (or OpenAI/Anthropic keys)
- Basic Python knowledge
Step 1: Set Up Discord Bot#
1.1 Create Discord Application#
- Go to Discord Developer Portal
- Click "New Application"
- Name your bot (e.g., "AI Assistant")
- Go to "Bot" tab → "Add Bot"
- Enable these intents:
- Message Content Intent
- Server Members Intent
- Presence Intent
1.2 Get Bot Token#
- Under "Bot" tab, click "Reset Token"
- Copy the token (save it securely)
- Never share this token publicly
1.3 Invite Bot to Server#
- Go to "OAuth2" → "URL Generator"
- Select scopes:
bot,applications.commands - Select permissions:
- Send Messages
- Read Message History
- Use Slash Commands
- Embed Links
- Copy the generated URL and open in browser
- Select your server and authorize
Step 2: Install Dependencies#
# Create project directory
mkdir ai-discord-bot
cd ai-discord-bot
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install discord.py openai python-dotenv aiohttp
Step 3: Project Structure#
ai-discord-bot/
├── .env # Environment variables
├── bot.py # Main bot code
├── models.py # AI model configurations
├── context_manager.py # Conversation context
├── utils.py # Helper functions
└── requirements.txt # Dependencies
Step 4: Configuration#
Create .env file:
# Discord
DISCORD_TOKEN=your_discord_bot_token
# Crazyrouter (recommended - supports all models)
CRAZYROUTER_API_KEY=your_crazyrouter_key
# Or use individual providers
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
GOOGLE_API_KEY=your_google_key
# Bot settings
MAX_CONTEXT_MESSAGES=10
DEFAULT_MODEL=gpt-5-mini
ADMIN_USER_IDS=123456789,987654321
Step 5: Model Configuration#
Create models.py:
"""AI model configurations and pricing"""
MODELS = {
# OpenAI Models
"gpt-5": {
"name": "gpt-5.2",
"display": "GPT-5.2",
"provider": "openai",
"max_tokens": 4096,
"cost_per_1k": 0.015, # Input cost
"description": "Most capable GPT model"
},
"gpt-5-mini": {
"name": "gpt-5-mini",
"display": "GPT-5 Mini",
"provider": "openai",
"max_tokens": 4096,
"cost_per_1k": 0.001,
"description": "Fast and affordable"
},
# Anthropic Models
"claude": {
"name": "claude-opus-4-6-20260101",
"display": "Claude Opus 4.6",
"provider": "anthropic",
"max_tokens": 4096,
"cost_per_1k": 0.015,
"description": "Best reasoning and analysis"
},
"claude-sonnet": {
"name": "claude-sonnet-4-5-20250929",
"display": "Claude Sonnet 4.5",
"provider": "anthropic",
"max_tokens": 4096,
"cost_per_1k": 0.003,
"description": "Balanced performance"
},
# Google Models
"gemini": {
"name": "gemini-2.5-pro",
"display": "Gemini 2.5 Pro",
"provider": "google",
"max_tokens": 8192,
"cost_per_1k": 0.00125,
"description": "Long context, multimodal"
},
"gemini-flash": {
"name": "gemini-2.5-flash",
"display": "Gemini 2.5 Flash",
"provider": "google",
"max_tokens": 8192,
"cost_per_1k": 0.0001,
"description": "Fastest, cheapest"
},
# Other Models
"deepseek": {
"name": "deepseek-v3.2",
"display": "DeepSeek V3.2",
"provider": "deepseek",
"max_tokens": 4096,
"cost_per_1k": 0.00027,
"description": "Budget-friendly, good quality"
},
"grok": {
"name": "grok-4.1-fast",
"display": "Grok 4.1 Fast",
"provider": "xai",
"max_tokens": 4096,
"cost_per_1k": 0.005,
"description": "Fast, witty responses"
}
}
def get_model_list():
"""Return formatted model list for Discord embed"""
return "\n".join([
f"**{key}**: {config['display']} - {config['description']}"
for key, config in MODELS.items()
])
def get_model_config(model_key):
"""Get model configuration by key"""
return MODELS.get(model_key, MODELS["gpt-5-mini"])
Step 6: Context Manager#
Create context_manager.py:
"""Manage conversation context per Discord channel"""
from collections import defaultdict, deque
import os
MAX_CONTEXT = int(os.getenv("MAX_CONTEXT_MESSAGES", 10))
class ContextManager:
def __init__(self):
# Store context per channel: {channel_id: deque([messages])}
self.contexts = defaultdict(lambda: deque(maxlen=MAX_CONTEXT))
def add_message(self, channel_id, role, content):
"""Add message to channel context"""
self.contexts[channel_id].append({
"role": role,
"content": content
})
def get_context(self, channel_id):
"""Get conversation context for channel"""
return list(self.contexts[channel_id])
def clear_context(self, channel_id):
"""Clear context for channel"""
self.contexts[channel_id].clear()
def get_context_size(self, channel_id):
"""Get number of messages in context"""
return len(self.contexts[channel_id])
# Global context manager
context_manager = ContextManager()
Step 7: Main Bot Code#
Create bot.py:
"""AI-powered Discord bot with multiple LLM support"""
import discord
from discord import app_commands
from discord.ext import commands
import openai
import os
from dotenv import load_dotenv
import asyncio
from models import MODELS, get_model_list, get_model_config
from context_manager import context_manager
# Load environment variables
load_dotenv()
# Initialize OpenAI client (works with Crazyrouter)
client = openai.OpenAI(
api_key=os.getenv("CRAZYROUTER_API_KEY"),
base_url="https://api.crazyrouter.com/v1"
)
# Bot setup
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
# Admin user IDs
ADMIN_IDS = [int(id) for id in os.getenv("ADMIN_USER_IDS", "").split(",") if id]
@bot.event
async def on_ready():
"""Bot startup"""
print(f"✅ {bot.user} is online!")
print(f"📊 Connected to {len(bot.guilds)} servers")
# Sync slash commands
try:
synced = await bot.tree.sync()
print(f"✅ Synced {len(synced)} slash commands")
except Exception as e:
print(f"❌ Failed to sync commands: {e}")
@bot.tree.command(name="ask", description="Ask AI a question")
@app_commands.describe(
model="AI model to use",
prompt="Your question or prompt"
)
@app_commands.choices(model=[
app_commands.Choice(name=config["display"], value=key)
for key, config in list(MODELS.items())[:25] # Discord limit: 25 choices
])
async def ask(interaction: discord.Interaction, model: str, prompt: str):
"""Main AI chat command"""
await interaction.response.defer(thinking=True)
try:
# Get model config
model_config = get_model_config(model)
# Get conversation context
channel_id = interaction.channel_id
context = context_manager.get_context(channel_id)
# Add user message to context
context_manager.add_message(channel_id, "user", prompt)
# Build messages for API
messages = context + [{"role": "user", "content": prompt}]
# Call AI API
response = await asyncio.to_thread(
client.chat.completions.create,
model=model_config["name"],
messages=messages,
max_tokens=model_config["max_tokens"],
temperature=0.7
)
# Extract response
ai_response = response.choices[0].message.content
# Add AI response to context
context_manager.add_message(channel_id, "assistant", ai_response)
# Create embed
embed = discord.Embed(
title=f"🤖 {model_config['display']}",
description=ai_response[:4000], # Discord limit
color=discord.Color.blue()
)
embed.set_footer(text=f"Context: {context_manager.get_context_size(channel_id)} messages")
await interaction.followup.send(embed=embed)
except Exception as e:
error_embed = discord.Embed(
title="❌ Error",
description=f"Failed to get AI response: {str(e)}",
color=discord.Color.red()
)
await interaction.followup.send(embed=error_embed, ephemeral=True)
@bot.tree.command(name="models", description="List available AI models")
async def models(interaction: discord.Interaction):
"""Show available models"""
embed = discord.Embed(
title="🤖 Available AI Models",
description=get_model_list(),
color=discord.Color.green()
)
embed.set_footer(text="Use /ask <model> <prompt> to chat")
await interaction.response.send_message(embed=embed)
@bot.tree.command(name="clear", description="Clear conversation context")
async def clear(interaction: discord.Interaction):
"""Clear channel context"""
channel_id = interaction.channel_id
context_manager.clear_context(channel_id)
embed = discord.Embed(
title="🧹 Context Cleared",
description="Conversation history has been reset.",
color=discord.Color.orange()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@bot.tree.command(name="context", description="Show current conversation context")
async def show_context(interaction: discord.Interaction):
"""Display current context"""
channel_id = interaction.channel_id
context = context_manager.get_context(channel_id)
if not context:
await interaction.response.send_message("No context yet. Start chatting!", ephemeral=True)
return
context_text = "\n\n".join([
f"**{msg['role'].title()}**: {msg['content'][:100]}..."
for msg in context
])
embed = discord.Embed(
title=f"💬 Conversation Context ({len(context)} messages)",
description=context_text[:4000],
color=discord.Color.purple()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@bot.tree.command(name="stats", description="Show bot usage statistics")
async def stats(interaction: discord.Interaction):
"""Show bot stats (admin only)"""
if interaction.user.id not in ADMIN_IDS:
await interaction.response.send_message("❌ Admin only command", ephemeral=True)
return
total_contexts = len(context_manager.contexts)
total_messages = sum(len(ctx) for ctx in context_manager.contexts.values())
embed = discord.Embed(
title="📊 Bot Statistics",
color=discord.Color.gold()
)
embed.add_field(name="Servers", value=len(bot.guilds), inline=True)
embed.add_field(name="Active Channels", value=total_contexts, inline=True)
embed.add_field(name="Total Messages", value=total_messages, inline=True)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Run bot
if __name__ == "__main__":
token = os.getenv("DISCORD_TOKEN")
if not token:
print("❌ DISCORD_TOKEN not found in .env")
exit(1)
bot.run(token)
Step 8: Run Your Bot#
# Activate virtual environment
source venv/bin/activate
# Run bot
python bot.py
You should see:
✅ AI Assistant#1234 is online!
📊 Connected to 1 servers
✅ Synced 5 slash commands
Step 9: Test Your Bot#
In Discord, try these commands:
/models
# Shows all available AI models
/ask gpt-5-mini What is quantum computing?
# Ask GPT-5 Mini a question
/ask claude Explain relativity in simple terms
# Ask Claude Opus
/context
# View conversation history
/clear
# Reset conversation context
/stats
# View bot statistics (admin only)
Advanced Features#
Feature 1: Image Generation#
Add to bot.py:
@bot.tree.command(name="imagine", description="Generate an image with AI")
@app_commands.describe(prompt="Image description")
async def imagine(interaction: discord.Interaction, prompt: str):
"""Generate image with DALL-E"""
await interaction.response.defer(thinking=True)
try:
response = await asyncio.to_thread(
client.images.generate,
model="dall-e-3",
prompt=prompt,
size="1024x1024",
n=1
)
image_url = response.data[0].url
embed = discord.Embed(
title="🎨 Generated Image",
description=prompt,
color=discord.Color.purple()
)
embed.set_image(url=image_url)
await interaction.followup.send(embed=embed)
except Exception as e:
await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
Feature 2: Model Comparison#
@bot.tree.command(name="compare", description="Compare responses from multiple models")
@app_commands.describe(prompt="Question to ask all models")
async def compare(interaction: discord.Interaction, prompt: str):
"""Compare responses from GPT, Claude, and Gemini"""
await interaction.response.defer(thinking=True)
models_to_compare = ["gpt-5-mini", "claude-sonnet", "gemini-flash"]
responses = {}
for model_key in models_to_compare:
try:
model_config = get_model_config(model_key)
response = await asyncio.to_thread(
client.chat.completions.create,
model=model_config["name"],
messages=[{"role": "user", "content": prompt}],
max_tokens=500
)
responses[model_config["display"]] = response.choices[0].message.content[:500]
except Exception as e:
responses[model_config["display"]] = f"Error: {e}"
embed = discord.Embed(
title="🔍 Model Comparison",
description=f"**Prompt**: {prompt}",
color=discord.Color.teal()
)
for model_name, response_text in responses.items():
embed.add_field(
name=model_name,
value=response_text[:1024],
inline=False
)
await interaction.followup.send(embed=embed)
Feature 3: Usage Tracking#
import json
from datetime import datetime
class UsageTracker:
def __init__(self):
self.usage = {}
def track(self, user_id, model, tokens):
"""Track API usage"""
if user_id not in self.usage:
self.usage[user_id] = []
self.usage[user_id].append({
"model": model,
"tokens": tokens,
"timestamp": datetime.now().isoformat()
})
def get_user_usage(self, user_id):
"""Get usage for user"""
return self.usage.get(user_id, [])
def save(self, filename="usage.json"):
"""Save usage to file"""
with open(filename, "w") as f:
json.dump(self.usage, f, indent=2)
usage_tracker = UsageTracker()
Cost Optimization#
Strategy 1: Use Cheaper Models for Simple Tasks#
def select_model_by_complexity(prompt):
"""Auto-select model based on prompt complexity"""
if len(prompt) < 50:
return "gemini-flash" # $0.0001/1K tokens
elif any(word in prompt.lower() for word in ["code", "debug", "program"]):
return "gpt-5-mini" # $0.001/1K tokens
elif any(word in prompt.lower() for word in ["analyze", "research", "explain"]):
return "claude-sonnet" # $0.003/1K tokens
else:
return "gpt-5-mini" # Default
Strategy 2: Limit Context Window#
# In .env
MAX_CONTEXT_MESSAGES=5 # Reduce from 10 to save tokens
Strategy 3: Use Crazyrouter#
Crazyrouter provides 30% savings automatically:
| Model | Official | Crazyrouter | Savings |
|---|---|---|---|
| GPT-5 | $15/1M | $10.50/1M | 30% |
| Claude Opus | $15/1M | $10.50/1M | 30% |
| Gemini Pro | $1.25/1M | $0.875/1M | 30% |
Monthly savings (10K requests, 500 tokens avg):
- Official APIs: ~$75
- Crazyrouter: ~$52.50
- Savings: $22.50/month
Deployment#
Deploy to Railway#
- Create
Procfile:
worker: python bot.py
- Create
requirements.txt:
discord.py==2.3.2
openai==1.12.0
python-dotenv==1.0.0
aiohttp==3.9.3
- Push to GitHub
- Connect to Railway
- Add environment variables
- Deploy!
Deploy to Heroku#
heroku create your-bot-name
heroku config:set DISCORD_TOKEN=your_token
heroku config:set CRAZYROUTER_API_KEY=your_key
git push heroku main
Troubleshooting#
Bot doesn't respond to slash commands#
- Check bot has "applications.commands" scope
- Run
await bot.tree.sync()in on_ready - Wait 1 hour for Discord to propagate commands
"Missing Access" error#
- Ensure bot has these permissions:
- Send Messages
- Read Message History
- Use Slash Commands
- Embed Links
Rate limit errors#
- Implement exponential backoff
- Use Crazyrouter for higher rate limits
- Add request queuing
Context not working#
- Check
MAX_CONTEXT_MESSAGESin .env - Verify channel_id is consistent
- Clear context with
/clearand retry
Best Practices#
- Always defer responses for AI calls (they take time)
- Use ephemeral messages for errors and admin commands
- Implement rate limiting per user to prevent abuse
- Log all API calls for debugging and cost tracking
- Use embeds for better formatting
- Handle errors gracefully with user-friendly messages
- Test in private server before public deployment
Frequently Asked Questions#
Can I use this bot commercially?#
Yes, but ensure you comply with Discord's Terms of Service and each AI provider's usage policies.
How much does it cost to run?#
With Crazyrouter and 1,000 requests/day (500 tokens avg):
- Monthly cost: ~$15-30
- Per request: ~$0.0005-0.001
Can I add more models?#
Yes! Add to models.py and the bot automatically supports them. Crazyrouter provides 300+ models.
How do I prevent spam?#
Implement per-user rate limiting:
from collections import defaultdict
import time
user_last_request = defaultdict(float)
COOLDOWN = 5 # seconds
@bot.tree.command(name="ask")
async def ask(interaction, model, prompt):
user_id = interaction.user.id
now = time.time()
if now - user_last_request[user_id] < COOLDOWN:
await interaction.response.send_message(
f"⏳ Please wait {COOLDOWN}s between requests",
ephemeral=True
)
return
user_last_request[user_id] = now
# ... rest of command
Can I use this with other AI providers?#
Yes! Just change the base_url and api_key in the OpenAI client initialization.
Conclusion#
You now have a production-ready Discord bot that:
- Supports 10+ AI models
- Maintains conversation context
- Costs 30-50% less with Crazyrouter
- Handles errors gracefully
- Includes admin controls
Next steps:
- Deploy to Railway/Heroku
- Add custom commands for your community
- Implement usage limits and tracking
- Add image generation and other features
Get your Crazyrouter API key at crazyrouter.com to access all models with one key and save 30% on costs.
Happy building! 🤖


