You've made it. 98 episodes. Tokens to transformers. Training to inference. Embeddings to voice assistants.
Now let's put it all together.
Building an AI SaaS isn't just calling the OpenAI API. It's auth, billing, rate limiting, model routing, observability, scaling, and the 3 AM page when your GPU cluster runs out of memory.
This is the architecture you need. Everything we've learned, in one system.
The Architecture Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CLIENT LAYER โ
โ React/Vue App โ React Native โ API Clients (Python, JS, etc) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GATEWAY LAYER โ
โ Auth (JWT) โ Rate Limit โ API Gateway โ Router โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SERVICE LAYER โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ Auth โ โ Billing โ โ Request โ โ Admin โ โ
โ โ Service โ โ Service โ โ Service โ โ Service โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AI ORCHESTRATION โ
โ Cache (Semantic) โ Model Router โ Fallback Handler โ
โ โ โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โ
โ โ OpenAI โ โ Claude โ โ Self- โ โ Other โ โ
โ โ โ โ โ โ Hosted โ โ Providers โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DATA & OBSERVABILITY โ
โ Postgres (Users) โ Redis (Cache) โ ClickHouse (Analytics) โ
โ โ โ โ โ
โ S3 (Logs/Exports) Vector DB Grafana/Datadog โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Component 1: Authentication & Authorization
See Episode 54 for why auth matters for data privacy.
JWT-Based Auth
python
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.InvalidTokenError:
raise HTTPException(401, "Invalid token")@app.post("/v1/chat")
async def chat(request: ChatRequest, user: dict = Depends(verify_token)):
# user["sub"] = user_id
# user["plan"] = subscription tier
passAPI Key Management
python
import secrets
def generate_api_key():
return f"sk-{secrets.token_urlsafe(32)}"Store hashed version in DB
api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()๐ง
Component 2: Rate Limiting & Quotas
See Episode 84 for gateway patterns.
Tiered Rate Limits
python
PLAN_LIMITS = {
"free": {"rpm": 10, "tpm": 1000, "daily_cost": 0.10},
"pro": {"rpm": 100, "tpm": 10000, "daily_cost": 5.00},
"enterprise": {"rpm": 1000, "tpm": 100000, "daily_cost": 100.00}}
async def check_rate_limit(user_id: str, plan: str):
limits = PLAN_LIMITS[plan]# Check requests per minute
current_rpm = await redis.get(f"rpm:{user_id}")
if int(current_rpm or 0) >= limits["rpm"]:
raise HTTPException(429, "Rate limit exceeded")# Check daily spend
daily_spend = await redis.get(f"spend:{user_id}:{today()}")
if float(daily_spend or 0) >= limits["daily_cost"]:
raise HTTPException(429, "Daily quota exceeded")Token Bucket in Redis
lua
-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("GET", key)
if not current then
redis.call("SET", key, 1, "EX", window)
return 1end
if tonumber(current) < limit then
redis.call("INCR", key)
return tonumber(current) + 1else
return -1end
๐
Component 3: The AI Gateway
This is the heart of your system. See Episode 84.
Unified API
python
from fastapi import FastAPI
import litellm
app = FastAPI()@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
# 1. Check cache (Episode 83)
cached = await check_semantic_cache(request.messages)
if cached:
return {"choices": [{"message": {"content": cached}}], "cached": True}# 2. Route to appropriate model
model = route_model(request.messages, request.model)# 3. Call with fallback
try:
response = await call_with_fallback(model, request)
except Exception as e:
raise HTTPException(503, f"All providers failed: {e}")# 4. Store in cache
await store_in_semantic_cache(request.messages, response)# 5. Log usage
await log_usage(request.user_id, model, response.usage)return responseSemantic Caching
See Episode 83.
python
from sentence_transformers import SentenceTransformer
import qdrant_client
embedder = SentenceTransformer('all-MiniLM-L6-v2')
qdrant = qdrant_client.QdrantClient(host="localhost", port=6333)
async def check_semantic_cache(messages: list, threshold=0.92):
query = messages[-1]["content"] # Last user message
embedding = embedder.encode(query)
results = qdrant.search(
collection_name="response_cache",
query_vector=embedding.tolist(),
limit=1
)
if results and results[0].score > threshold:
return results[0].payload["response"]
return NoneComponent 4: Model Routing
See Episode 55 for cost optimization strategies.
Smart Router
python
def route_model(messages: list, requested_model: str = None) -> str:
# User specified
if requested_model:
return requested_model
# Auto-route based on content
content = messages[-1]["content"]
# Simple tasks โ Cheap model
if is_simple_task(content):
return "claude-3.5-haiku"
# Code tasks โ Best coding model
if is_code_task(content):
return "claude-3.5-sonnet"
# Long context โ Gemini
if estimate_tokens(messages) > 100000:
return "gemini-1.5-pro"
# Default
return "gpt-4-turbo"
def is_simple_task(text: str) -> bool:
simple_keywords = ["what is", "define", "explain simple"]
return any(kw in text.lower() for kw in simple_keywords)Component 5: Billing System
Track every token, bill accurately.
Usage Tracking
python
from decimal import Decimal
async def log_usage(user_id: str, model: str, usage: dict):
cost = calculate_cost(model, usage["prompt_tokens"], usage["completion_tokens"])# Store in ClickHouse for analytics
await clickhouse.insert("usage_logs", {
"user_id": user_id,
"model": model,
"input_tokens": usage["prompt_tokens"],
"output_tokens": usage["completion_tokens"],
"cost_usd": cost,
"timestamp": datetime.utcnow()
})# Update Redis for real-time quota
await redis.incrbyfloat(f"spend:{user_id}:{today()}", cost)MODEL_PRICING = {
"gpt-4": {"input": 30/1_000_000, "output": 60/1_000_000},
"claude-3.5-haiku": {"input": 0.25/1_000_000, "output": 1.25/1_000_000},}
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> Decimal:
pricing = MODEL_PRICING.get(model, {"input": 0, "output": 0})
return Decimal(
input_tokens * pricing["input"] +
output_tokens * pricing["output"]
)๐ฌ
Component 6: Observability
See Episode 56 for full observability setup.
Structured Logging
python
import structlog
logger = structlog.get_logger()async def log_request(request_id: str, user_id: str, model: str, latency: float):
logger.info(
"ai_request_completed",
request_id=request_id,
user_id=user_id,
model=model,
latency_ms=latency,
environment="production"
)Metrics
python
from prometheus_client import Counter, Histogram, Gauge
requests_total = Counter('ai_requests_total', 'Total requests', ['model', 'status'])
request_duration = Histogram('ai_request_duration_seconds', 'Request duration', ['model'])
active_requests = Gauge('ai_active_requests', 'Active requests')@app.post("/v1/chat/completions")
async def chat(request: Request):
active_requests.inc()
start = time.time()try:
response = await process(request)
requests_total.labels(model=request.model, status="success").inc()
return response
except Exception as e:
requests_total.labels(model=request.model, status="error").inc()
raise
finally:
request_duration.labels(model=request.model).observe(time.time() - start)
active_requests.dec()๐ก๏ธ
Component 7: Scaling Infrastructure
Horizontal Scaling
yaml
docker-compose.yml
version: '3.8'
services:
api:
build: .
replicas: 3
environment:
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgresql://...redis:
image: redis:7-alpinepostgres:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/dataKubernetes Deployment
yaml
k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-api
spec:
replicas: 5
selector:
matchLabels:
app: ai-apitemplate:
spec:
containers:
- name: api
image: your-registry/ai-api:latest
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
env:
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: api-secrets
key: openai-key๐ฆ
Component 8: Security & Compliance
See Episode 54 for privacy considerations.
Data Handling
python
PII detection before sending to LLM
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
def sanitize_prompt(text: str) -> str:
results = analyzer.analyze(text=text, language='en')
if results:
anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
return anonymized.text
return textRequest Validation
python
from pydantic import BaseModel, validator
class ChatRequest(BaseModel):
messages: list
model: str
max_tokens: int = 150@validator('max_tokens')
def limit_tokens(cls, v):
if v > 4096:
raise ValueError('max_tokens cannot exceed 4096')
return v@validator('messages')
def limit_context(cls, v):
if len(v) > 50:
raise ValueError('Too many messages in context')
return v๐
The Complete Request Flow
- Client sends POST /v1/chat/completions
โ- JWT verification
โ- Rate limit check (Redis)
โ- Semantic cache check (Qdrant)
โ [cache miss]- Model routing
โ- Request validation (PII, limits)
โ- Call LLM API (with fallback)
โ- Store in semantic cache
โ- Log usage (ClickHouse)
โ- Metrics (Prometheus)
โ- Return response
๐
Cost Structure at Scale
Component Monthly Cost (1M requests) Notes
LLM APIs $10,000-50,000 Biggest cost by far
| Compute (API servers) | $500-2,000 | 3-5 instances |
|---|---|---|
| Redis (cache) | $200-500 | Managed Redis |
| Postgres | $100-300 | User data |
| Vector DB | $300-800 | Semantic cache |
| Observability | $200-500 | Datadog/Grafana |
| Total | $11,300-54,100 | Varies by model mix |
๐๏ธ
What We've Built
This architecture combines everything from the series:
Tokens (Episode 1) โ We count and limit themTraining vs Inference (Episode 4) โ We only do inference
Embeddings (Episode 15) โ We cache by meaningVector Search (Episode 17) โ Fast semantic cache lookup
Cost Optimization (Episode 55) โ Smart routing, caching
Monitoring (Episode 56) โ Full observabilitySemantic Caching (Episode 83) โ 40-70% cost savings
AI Gateway (Episode 84) โ Unified API
Data Privacy (Episode 54) โ PII handling, compliance