Module 5: OpenRouter - Introduction

Mini-Project: Smart Chatbot with OpenRouter

Project overview

Final project of Module 5: An intelligent chatbot that auto-selects the optimal model based on the query, with robust fallback and cost optimization.

Time: 60 minutes
Difficulty: Medium-High


🎯 Objective

Build a production-ready chatbot with:

  • ✅ Model auto-selection (cheap → expensive)
  • ✅ Fallback chain (3+ models)
  • ✅ Detailed cost tracking
  • ✅ Performance metrics
  • ✅ User tier support (free/pro/enterprise)

💻 Complete Code

#!/usr/bin/env python3
"""
Smart Chatbot with OpenRouter
Module 5 - Final Project
"""

import os
import json
import time
from datetime import datetime
from pathlib import Path
from typing import List, Dict
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

# ============================================================================
# CONFIGURATION
# ============================================================================

Path("conversations").mkdir(exist_ok=True)
Path("logs").mkdir(exist_ok=True)

# ============================================================================
# SMART ROUTER
# ============================================================================

class SmartChatbot:
    """Intelligent chatbot with OpenRouter."""
    
    def __init__(self, user_tier: str = "free"):
        self.client = OpenAI(
            base_url="https://openrouter.ai/api/v1",
            api_key=os.getenv("OPENROUTER_API_KEY")
        )
        
        self.user_tier = user_tier
        self.messages = [
            {"role": "system", "content": "You are a helpful assistant."}
        ]
        
        self.conversation_id = datetime.now().strftime("%Y%m%d_%H%M%S")
        self.total_cost = 0.0
        self.query_count = 0
        self.model_usage = {}
        
        # Budget per tier
        self.daily_budgets = {
            "free": 0.10,
            "pro": 1.00,
            "enterprise": 100.00
        }
    
    def classify_query(self, prompt: str) -> str:
        """Classify the query type."""
        prompt_lower = prompt.lower()
        
        if any(w in prompt_lower for w in ["código", "code", "function", "python"]):
            return "code"
        elif len(prompt) > 300:
            return "complex"
        else:
            return "simple"
    
    def select_model(self, query_type: str) -> List[str]:
        """
        Select model(s) based on type and tier.
        Returns a list (primary + fallbacks).
        """
        
        # Budget check
        budget = self.daily_budgets[self.user_tier]
        remaining = budget - self.total_cost
        
        # If near the limit → only cheap models
        if remaining < budget * 0.2:
            return [
                "mistralai/mixtral-8x7b-instruct",
                "anthropic/claude-3-haiku"
            ]
        
        # Routing by tier and type
        if self.user_tier == "free":
            return [
                "mistralai/mixtral-8x7b-instruct",
                "anthropic/claude-3-haiku"
            ]
        
        elif self.user_tier == "pro":
            if query_type == "code":
                return [
                    "mistralai/codestral-latest",
                    "openai/gpt-3.5-turbo",
                    "mistralai/mixtral-8x7b-instruct"
                ]
            else:
                return [
                    "openai/gpt-3.5-turbo",
                    "anthropic/claude-3-haiku",
                    "mistralai/mixtral-8x7b-instruct"
                ]
        
        else:  # enterprise
            if query_type == "complex":
                return [
                    "openai/gpt-4-turbo",
                    "anthropic/claude-3-opus",
                    "openai/gpt-3.5-turbo"
                ]
            elif query_type == "code":
                return [
                    "openai/gpt-4-turbo",
                    "mistralai/codestral-latest"
                ]
            else:
                return [
                    "openai/gpt-3.5-turbo",
                    "anthropic/claude-3-haiku"
                ]
    
    def chat(self, user_message: str) -> str:
        """Chat with auto-selection and fallback."""
        
        self.messages.append({"role": "user", "content": user_message})
        
        # Classify query
        query_type = self.classify_query(user_message)
        
        # Select models (primary + fallbacks)
        models = self.select_model(query_type)
        
        # Try models with fallback
        for i, model in enumerate(models):
            try:
                start = time.time()
                
                response = self.client.chat.completions.create(
                    model=model,
                    messages=self.messages,
                    timeout=10.0
                )
                
                latency = time.time() - start
                
                # Extract response
                assistant_message = response.choices[0].message.content
                self.messages.append({"role": "assistant", "content": assistant_message})
                
                # Track metrics
                self._track_usage(model, response.usage, latency, query_type)
                
                # Success message
                fallback_msg = f" (fallback #{i})" if i > 0 else ""
                print(f"✅ {model}{fallback_msg} | {latency:.2f}s | ${self._estimate_cost(response.usage):.6f}")
                
                return assistant_message
            
            except Exception as e:
                print(f"⚠️ {model} failed: {e}")
                if i == len(models) - 1:
                    return "Error: All models exhausted. Please try again."
                continue
        
        return "Error: Unexpected failure"
    
    def _estimate_cost(self, usage) -> float:
        """Estimate cost based on tokens."""
        # Simplified: assume avg $0.50/1M
        return (usage.total_tokens / 1_000_000) * 0.50
    
    def _track_usage(self, model: str, usage, latency: float, query_type: str):
        """Track metrics."""
        
        cost = self._estimate_cost(usage)
        
        self.total_cost += cost
        self.query_count += 1
        
        if model not in self.model_usage:
            self.model_usage[model] = {
                "count": 0,
                "total_tokens": 0,
                "total_cost": 0.0,
                "total_latency": 0.0
            }
        
        self.model_usage[model]["count"] += 1
        self.model_usage[model]["total_tokens"] += usage.total_tokens
        self.model_usage[model]["total_cost"] += cost
        self.model_usage[model]["total_latency"] += latency
    
    def show_stats(self):
        """Show the statistics."""
        print("\n" + "="*60)
        print("📊 STATISTICS")
        print("="*60)
        print(f"Tier: {self.user_tier}")
        print(f"Queries: {self.query_count}")
        print(f"Total cost: ${self.total_cost:.6f}")
        print(f"Budget used: {(self.total_cost/self.daily_budgets[self.user_tier])*100:.1f}%")
        
        print("\nBy model:")
        for model, stats in self.model_usage.items():
            avg_latency = stats["total_latency"] / stats["count"]
            print(f"\n  {model}:")
            print(f"    Queries: {stats['count']}")
            print(f"    Tokens: {stats['total_tokens']:,}")
            print(f"    Cost: ${stats['total_cost']:.6f}")
            print(f"    Avg latency: {avg_latency:.2f}s")
        
        print("="*60 + "\n")
    
    def save_conversation(self):
        """Save the conversation."""
        filepath = f"conversations/conversation_{self.conversation_id}.json"
        
        data = {
            "conversation_id": self.conversation_id,
            "timestamp": datetime.now().isoformat(),
            "user_tier": self.user_tier,
            "messages": self.messages[1:],  # Skip system
            "total_cost": self.total_cost,
            "query_count": self.query_count,
            "model_usage": self.model_usage
        }
        
        with open(filepath, "w") as f:
            json.dump(data, f, indent=2)
        
        print(f"💾 Saved: {filepath}")

# ============================================================================
# MAIN CLI
# ============================================================================

def main():
    """Main CLI."""
    
    print("\n" + "="*60)
    print("🤖 SMART CHATBOT (OpenRouter)")
    print("="*60)
    
    # Select tier
    print("\nSelect tier:")
    print("  1. Free ($0.10/day budget)")
    print("  2. Pro ($1.00/day budget)")
    print("  3. Enterprise (unlimited)")
    
    tier_choice = input("\nTier (1-3): ").strip()
    tier_map = {"1": "free", "2": "pro", "3": "enterprise"}
    tier = tier_map.get(tier_choice, "free")
    
    print(f"\n✅ Tier: {tier}")
    print("\nCommands:")
    print("  - 'stats' → Show statistics")
    print("  - 'salir' → Exit")
    print("\n" + "="*60 + "\n")
    
    bot = SmartChatbot(user_tier=tier)
    
    while True:
        try:
            user_input = input("You: ").strip()
            
            if user_input.lower() in ["salir", "exit", "quit"]:
                print("\n👋 Bye!\n")
                bot.show_stats()
                bot.save_conversation()
                break
            
            if user_input.lower() == "stats":
                bot.show_stats()
                continue
            
            if not user_input:
                continue
            
            response = bot.chat(user_input)
            print(f"\nBot: {response}\n")
        
        except KeyboardInterrupt:
            print("\n\n👋 Interrupted\n")
            bot.show_stats()
            bot.save_conversation()
            break

if __name__ == "__main__":
    main()

🚀 Usage

python smart_chatbot.py

Example conversation:

Select tier:
  1. Free ($0.10/day budget)
  2. Pro ($1.00/day budget)
  3. Enterprise (unlimited)

Tier (1-3): 2

✅ Tier: pro

You: Hi
✅ openai/gpt-3.5-turbo | 1.5s | $0.000025
Bot: Hi! How can I help you today?

You: Write a Python function for fibonacci
✅ mistralai/codestral-latest | 2.1s | $0.000120
Bot: def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

You: stats

============================================================
📊 STATISTICS
============================================================
Tier: pro
Queries: 2
Total cost: $0.000145
Budget used: 0.0%

By model:

  openai/gpt-3.5-turbo:
    Queries: 1
    Tokens: 50
    Cost: $0.000025
    Avg latency: 1.50s

  mistralai/codestral-latest:
    Queries: 1
    Tokens: 240
    Cost: $0.000120
    Avg latency: 2.10s
============================================================

You: salir

👋 Bye!

[Stats displayed again]
💾 Saved: conversations/conversation_20240215_103045.json

✅ Rubric

Functionality (40 pts):

  • (10) Auto-selection works
  • (10) Fallback works
  • (10) Accurate cost tracking
  • (10) Tier support (free/pro/enterprise)

Optimization (30 pts):

  • (10) Budget limits respected
  • (10) Cheap models for simple queries
  • (10) Logical fallback chain

Production-ready (30 pts):

  • (10) Robust error handling
  • (10) Complete stats
  • (10) Conversations saved

Total: ___/100


✅ Module 5 Summary

What you mastered:

  • ✅ OpenRouter API (100+ models)
  • ✅ Cost optimization strategies
  • ✅ Robust fallback
  • ✅ Intelligent auto-selection
  • ✅ Multi-tier support

Advantages vs a single provider:

  • 50-95% savings (cheap models)
  • High availability (fallback)
  • No vendor lock-in
  • Flexibility

➡️ Next Modules

Module 6: Modal (Serverless) Module 7: Technical Comparison (Benchmarks) Module 8: Unified Client (Final project)


Congratulations! You completed Module 5. 🎉