Login
Back to Blog
How to Build an AI-Powered Discord Bot with Multiple LLMs

How to Build an AI-Powered Discord Bot with Multiple LLMs

C
Crazyrouter Team
March 12, 2026
8 viewsEnglishTutorial
Share:

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#

  1. Go to Discord Developer Portal
  2. Click "New Application"
  3. Name your bot (e.g., "AI Assistant")
  4. Go to "Bot" tab → "Add Bot"
  5. Enable these intents:
    • Message Content Intent
    • Server Members Intent
    • Presence Intent

1.2 Get Bot Token#

  1. Under "Bot" tab, click "Reset Token"
  2. Copy the token (save it securely)
  3. Never share this token publicly

1.3 Invite Bot to Server#

  1. Go to "OAuth2" → "URL Generator"
  2. Select scopes: bot, applications.commands
  3. Select permissions:
    • Send Messages
    • Read Message History
    • Use Slash Commands
    • Embed Links
  4. Copy the generated URL and open in browser
  5. Select your server and authorize

Step 2: Install Dependencies#

bash
# 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#

code
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:

bash
# 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:

python
"""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:

python
"""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:

python
"""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#

bash
# Activate virtual environment
source venv/bin/activate

# Run bot
python bot.py

You should see:

code
✅ AI Assistant#1234 is online!
📊 Connected to 1 servers
✅ Synced 5 slash commands

Step 9: Test Your Bot#

In Discord, try these commands:

code
/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:

python
@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#

python
@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#

python
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#

python
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#

python
# In .env
MAX_CONTEXT_MESSAGES=5  # Reduce from 10 to save tokens

Strategy 3: Use Crazyrouter#

Crazyrouter provides 30% savings automatically:

ModelOfficialCrazyrouterSavings
GPT-5$15/1M$10.50/1M30%
Claude Opus$15/1M$10.50/1M30%
Gemini Pro$1.25/1M$0.875/1M30%

Monthly savings (10K requests, 500 tokens avg):

  • Official APIs: ~$75
  • Crazyrouter: ~$52.50
  • Savings: $22.50/month

Deployment#

Deploy to Railway#

  1. Create Procfile:
code
worker: python bot.py
  1. Create requirements.txt:
code
discord.py==2.3.2
openai==1.12.0
python-dotenv==1.0.0
aiohttp==3.9.3
  1. Push to GitHub
  2. Connect to Railway
  3. Add environment variables
  4. Deploy!

Deploy to Heroku#

bash
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#

  1. Check bot has "applications.commands" scope
  2. Run await bot.tree.sync() in on_ready
  3. Wait 1 hour for Discord to propagate commands

"Missing Access" error#

  1. Ensure bot has these permissions:
    • Send Messages
    • Read Message History
    • Use Slash Commands
    • Embed Links

Rate limit errors#

  1. Implement exponential backoff
  2. Use Crazyrouter for higher rate limits
  3. Add request queuing

Context not working#

  1. Check MAX_CONTEXT_MESSAGES in .env
  2. Verify channel_id is consistent
  3. Clear context with /clear and retry

Best Practices#

  1. Always defer responses for AI calls (they take time)
  2. Use ephemeral messages for errors and admin commands
  3. Implement rate limiting per user to prevent abuse
  4. Log all API calls for debugging and cost tracking
  5. Use embeds for better formatting
  6. Handle errors gracefully with user-friendly messages
  7. 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:

python
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:

  1. Deploy to Railway/Heroku
  2. Add custom commands for your community
  3. Implement usage limits and tracking
  4. 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! 🤖

Related Articles