Das AI Gateway ist mehr als ein Load Balancer. Es ist dein Kontrollpunkt für alle LLM-Requests. Hier entscheidest du, welches Model benutzt wird, wie viel darf es kosten, welche Fallbacks greifen, wer darf anfragen.
Die 4 Kernaufgaben eines AI Gateways
Request
↓
┌─────────────────────────────────────┐
│ 1. AUTHENTIFIZIERUNG │ Who are you?
│ API Keys validieren │
├─────────────────────────────────────┤
│ 2. RATE LIMITING │ How much can you use?
│ Tokens/min, Requests/min pro Key │
├─────────────────────────────────────┤
│ 3. SMART ROUTING │ Which model?
│ Cost vs Speed vs Quality │
├─────────────────────────────────────┤
│ 4. MONITORING & CACHING │ Did we see this before?
│ Token Counting, Semantic Cache │
└─────────────────────────────────────┘
↓
LLM Backends (vLLM, OpenAI, Anthropic, ...)
1. Authentifizierung und API Key Management
API Key Structure
import secrets
import hashlib
from datetime import datetime, timedelta
class APIKeyManager:
def __init__(self, db):
self.db = db
def create_key(self, user_id: str, name: str, quota_tokens: int):
"""Erzeuge einen API Key mit Token-Quota"""
key_bytes = secrets.token_bytes(32)
key_hex = key_bytes.hex()
key_hash = hashlib.sha256(key_hex.encode()).hexdigest()
self.db.execute("""
INSERT INTO api_keys
(user_id, name, hash, quota_tokens, created_at, valid_until)
VALUES (?, ?, ?, ?, ?, ?)
""", (
user_id, name, key_hash, quota_tokens,
datetime.now(), datetime.now() + timedelta(days=365)
))
return key_hex # Nur einmal zurückgeben!
def validate_key(self, key_hex: str) -> dict:
"""Validiere API Key und hole User-Limits"""
key_hash = hashlib.sha256(key_hex.encode()).hexdigest()
result = self.db.execute("""
SELECT user_id, quota_tokens, tokens_used, valid_until
FROM api_keys WHERE hash = ? AND valid_until > now()
""", (key_hash,))
if not result:
return None
row = result[0]
if row['tokens_used'] >= row['quota_tokens']:
return None # Quota exceeded
return {
'user_id': row['user_id'],
'tokens_remaining': row['quota_tokens'] - row['tokens_used'],
'expires_at': row['valid_until']
}
Gateway Middleware (FastAPI)
from fastapi import FastAPI, Header, HTTPException, status
from functools import wraps
app = FastAPI()
key_manager = APIKeyManager(db)
@app.middleware("http")
async def authenticate_request(request, call_next):
"""Middleware: Validiere API Key bei jedem Request"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing or invalid Authorization header"
)
api_key = auth_header[7:] # Remove "Bearer "
user_info = key_manager.validate_key(api_key)
if not user_info:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key or quota exceeded"
)
# Speichere User-Info für Request-Handler
request.state.user_id = user_info['user_id']
request.state.tokens_remaining = user_info['tokens_remaining']
response = await call_next(request)
return response
2. Rate Limiting: Token-Aware vs Request-Based
Naive Rate Limiting zählt Requests. Intelligent Rate Limiting zählt Tokens.
Das Problem mit Request-basiert
User A: 1000 Token Request → 1 Request
User B: 10 Token Request → 1 Request
Beide verbrauchen 1 Request-Slot, aber A verbraucht 100x mehr Tokens!
Token-basiertes Rate Limiting
from datetime import datetime, timedelta
from collections import defaultdict
class TokenRateLimiter:
def __init__(self, tokens_per_minute=100000):
self.limit = tokens_per_minute
self.user_buckets = defaultdict(lambda: {
"tokens": tokens_per_minute,
"last_refill": datetime.now()
})
def check_and_consume(self, user_id: str, tokens_needed: int) -> bool:
"""Prüfe ob User noch Tokens hat und verbrauche sie"""
bucket = self.user_buckets[user_id]
# Token Refill (Leaky Bucket)
now = datetime.now()
seconds_since_refill = (now - bucket["last_refill"]).total_seconds()
refill_rate = self.limit / 60 # Tokens pro Sekunde
bucket["tokens"] = min(
self.limit,
bucket["tokens"] + (seconds_since_refill * refill_rate)
)
bucket["last_refill"] = now
# Prüfe ob genug Tokens vorhanden
if bucket["tokens"] < tokens_needed:
return False
# Verbrauche
bucket["tokens"] -= tokens_needed
return True
def get_status(self, user_id: str) -> dict:
"""Zeige Token-Status für User"""
bucket = self.user_buckets[user_id]
return {
"tokens_available": int(bucket["tokens"]),
"tokens_per_minute_limit": self.limit,
"reset_in_seconds": 60
}
limiter = TokenRateLimiter(tokens_per_minute=100000)
@app.post("/v1/completions")
async def completions(request: CompletionRequest):
user_id = request.state.user_id
estimated_tokens = (
len(request.prompt.split()) * 1.3 + # Input
request.max_tokens * 1.3 # Output worst case
)
if not limiter.check_and_consume(user_id, estimated_tokens):
status = limiter.get_status(user_id)
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Rate limit exceeded. {status['tokens_available']:.0f} tokens available next minute."
)
# Jetzt inference...
3. Intelligentes Routing
Das Gateway routet nicht einfach auf den "nächsten Server" — es macht strategische Entscheidungen:
Routing-Strategien
from enum import Enum
from dataclasses import dataclass
class RoutingStrategy(Enum):
SPEED = "minimize_latency"
COST = "minimize_cost"
QUALITY = "maximize_quality"
AVAILABLE = "first_available"
@dataclass
class ModelEndpoint:
name: str
url: str
model: str
latency_p95: float # milliseconds
cost_per_1k_tokens: float
quality_score: float # 0-1
availability: float # 0-1 (uptime percentage)
class GatewayRouter:
def __init__(self):
self.endpoints = {
"llama-7b": ModelEndpoint(
name="llama-7b",
url="http://gpu-1:8000",
model="meta-llama/Llama-2-7b-hf",
latency_p95=15,
cost_per_1k_tokens=0.0001,
quality_score=0.7,
availability=0.995
),
"llama-70b": ModelEndpoint(
name="llama-70b",
url="http://gpu-2:8000",
model="meta-llama/Llama-2-70b-hf",
latency_p95=45,
cost_per_1k_tokens=0.0005,
quality_score=0.95,
availability=0.99
),
"gpt-4": ModelEndpoint(
name="gpt-4",
url="https://api.openai.com/v1",
model="gpt-4",
latency_p95=200,
cost_per_1k_tokens=0.03,
quality_score=1.0,
availability=0.999
)
}
def select_model(
self,
request_tokens: int,
strategy: RoutingStrategy,
budget_remaining: float,
latency_budget_ms: float
) -> ModelEndpoint:
"""Wähle das beste Model für Request"""
# Filter: Was ist verfügbar?
available = [
ep for ep in self.endpoints.values()
if ep.availability > 0.95
]
if strategy == RoutingStrategy.SPEED:
# Latenz minimieren
selected = min(available, key=lambda ep: ep.latency_p95)
elif strategy == RoutingStrategy.COST:
# Kosten minimieren
request_cost = (request_tokens / 1000) * min(
ep.cost_per_1k_tokens for ep in available
)
if request_cost > budget_remaining:
# Fallback auf billiges Model
selected = min(available, key=lambda ep: ep.cost_per_1k_tokens)
else:
selected = min(available, key=lambda ep: ep.cost_per_1k_tokens)
elif strategy == RoutingStrategy.QUALITY:
# Qualität maximieren
selected = max(available, key=lambda ep: ep.quality_score)
else: # AVAILABLE
selected = available[0]
# Latenz-Check
if selected.latency_p95 > latency_budget_ms:
# Fallback auf schnelleres Model
selected = min(
(ep for ep in available if ep.latency_p95 <= latency_budget_ms),
key=lambda ep: ep.latency_p95,
default=available[0]
)
return selected
router = GatewayRouter()
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest):
"""Intelligentes Routing basierend auf Request"""
# Bestimme Strategie
if request.get("prefer_fast"):
strategy = RoutingStrategy.SPEED
elif request.get("budget_tokens"):
strategy = RoutingStrategy.COST
else:
strategy = RoutingStrategy.QUALITY
# Schätze Token
prompt_tokens = len(request.messages[-1]["content"].split()) * 1.3
estimated_request_tokens = prompt_tokens + request.get("max_tokens", 512)
# Berechne verfügbares Budget
user_tokens_remaining = request.state.tokens_remaining
budget_cost = user_tokens_remaining * 0.001 # Assume avg cost
# Wähle Model
selected = router.select_model(
request_tokens=estimated_request_tokens,
strategy=strategy,
budget_remaining=budget_cost,
latency_budget_ms=500
)
print(f"Routing to {selected.name} (strategy: {strategy})")
# Forward Request
response = await forward_request(selected.url, request)
return response
Fallback Chains
class FallbackChain:
def __init__(self, chain: list[str]):
"""
chain = ["gpt-4", "claude-3-opus", "llama-70b"]
Versuche der Reihe nach
"""
self.chain = chain
async def execute(self, request: dict):
"""Führe aus, fallback wenn fehlschlägt"""
errors = []
for model_name in self.chain:
endpoint = router.endpoints[model_name]
try:
response = await call_endpoint(endpoint, request, timeout=5)
return response
except TimeoutError:
errors.append(f"{model_name}: timeout")
continue
except Exception as e:
errors.append(f"{model_name}: {str(e)}")
continue
# Alle Fallbacks fehlgeschlagen
raise Exception(f"All fallbacks failed: {errors}")
# Beispiel: High Quality, aber mit Fallback
chain = FallbackChain(["gpt-4", "claude-3-opus", "llama-70b"])
response = await chain.execute({"messages": [...]})
4. Kostentracking auf Gateway-Ebene
from dataclasses import dataclass
from datetime import datetime
@dataclass
class CostEvent:
user_id: str
model: str
prompt_tokens: int
completion_tokens: int
cost_usd: float
timestamp: datetime
request_id: str
class CostTracker:
def __init__(self, db):
self.db = db
def log_completion(
self,
user_id: str,
model: str,
prompt_tokens: int,
completion_tokens: int
):
"""Log API Call mit Kosten"""
total_tokens = prompt_tokens + completion_tokens
# Kosten aus Modell-Config
pricing = {
"gpt-4": {"prompt": 0.03, "completion": 0.06},
"gpt-3.5": {"prompt": 0.001, "completion": 0.002},
"llama-70b": {"prompt": 0.0005, "completion": 0.0008},
}
if model not in pricing:
cost_usd = 0 # Local models
else:
cost_usd = (
(prompt_tokens / 1000) * pricing[model]["prompt"] +
(completion_tokens / 1000) * pricing[model]["completion"]
)
event = CostEvent(
user_id=user_id,
model=model,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cost_usd=cost_usd,
timestamp=datetime.now(),
request_id=generate_request_id()
)
# Speichere in Datenbank
self.db.execute("""
INSERT INTO cost_events
(user_id, model, prompt_tokens, completion_tokens, cost_usd, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
""", (
event.user_id, event.model, event.prompt_tokens,
event.completion_tokens, event.cost_usd, event.timestamp
))
return event
def get_user_costs(self, user_id: str, days=30):
"""Aggregiere Kosten für Benutzer"""
result = self.db.execute("""
SELECT
model,
SUM(prompt_tokens) as total_prompt,
SUM(completion_tokens) as total_completion,
SUM(cost_usd) as total_cost,
COUNT(*) as request_count
FROM cost_events
WHERE user_id = ?
AND timestamp > datetime('now', '-{} days')
GROUP BY model
""".format(days), (user_id,))
return {row['model']: {
'prompt_tokens': row['total_prompt'],
'completion_tokens': row['total_completion'],
'cost_usd': row['total_cost'],
'requests': row['request_count']
} for row in result}
def get_dashboard(self, user_id: str):
"""Dashboard: Kosten heute, diese Woche, dieser Monat"""
today = self.db.execute("""
SELECT SUM(cost_usd) as cost FROM cost_events
WHERE user_id = ? AND date(timestamp) = date('now')
""", (user_id,))[0]['cost'] or 0
this_week = self.db.execute("""
SELECT SUM(cost_usd) as cost FROM cost_events
WHERE user_id = ? AND timestamp > datetime('now', '-7 days')
""", (user_id,))[0]['cost'] or 0
this_month = self.db.execute("""
SELECT SUM(cost_usd) as cost FROM cost_events
WHERE user_id = ? AND timestamp > datetime('now', '-30 days')
""", (user_id,))[0]['cost'] or 0
return {
'today': today,
'this_week': this_week,
'this_month': this_month
}
tracker = CostTracker(db)
@app.get("/api/costs/dashboard")
async def cost_dashboard(request):
user_id = request.state.user_id
dashboard = tracker.get_dashboard(user_id)
return dashboard
5. Semantic Caching (GPTCache Pattern)
Nicht nur Responses cachen, sondern Cache nach Semantik (Bedeutung):
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class SemanticCache:
def __init__(self, embedding_model, similarity_threshold=0.95):
"""
Cache-Einträge nicht exakt matchen, sondern semantisch ähnlich finden
"""
self.embedding_model = embedding_model # z.B. all-MiniLM
self.threshold = similarity_threshold
self.cache = [] # List of (embedding, prompt, response)
def embed(self, text: str) -> np.array:
"""Embedde einen Text"""
embedding = self.embedding_model.encode(text)
return embedding / np.linalg.norm(embedding) # Normalize
def get(self, prompt: str) -> str | None:
"""Suche ähnliche Prompts im Cache"""
query_embedding = self.embed(prompt)
for cached_embedding, cached_prompt, cached_response in self.cache:
# Kosinus-Ähnlichkeit
similarity = float(
cosine_similarity(
query_embedding.reshape(1, -1),
cached_embedding.reshape(1, -1)
)[0][0]
)
if similarity >= self.threshold:
print(f"Cache HIT: {similarity:.3f} similar to '{cached_prompt[:50]}'")
return cached_response
return None
def put(self, prompt: str, response: str):
"""Speichere Prompt-Response Pair"""
embedding = self.embed(prompt)
self.cache.append((embedding, prompt, response))
# Beispiel: Nutzen im Gateway
cache = SemanticCache(embedding_model)
@app.post("/v1/completions")
async def completions(request: CompletionRequest):
# Versuche aus Cache zu holen
cached = cache.get(request.prompt)
if cached:
return {"response": cached, "cached": True}
# Nicht gecacht → LLM aufrufen
response = await router.execute(request)
# Speichere in Cache
cache.put(request.prompt, response.text)
return {"response": response.text, "cached": False}
6. Token Counting auf Gateway-Ebene
Token Counting passiert IMMER am Gateway — bevor du das teuren LLM rufst:
import tiktoken
class TokenCounter:
def __init__(self):
# Verschiedene Encoding für verschiedene Models
self.encoders = {
"gpt-4": tiktoken.encoding_for_model("gpt-4"),
"gpt-3.5-turbo": tiktoken.encoding_for_model("gpt-3.5-turbo"),
"cl100k_base": tiktoken.get_encoding("cl100k_base"),
}
def count_tokens(self, text: str, model: str = "gpt-4") -> int:
"""Zähle Tokens für Text"""
encoder = self.encoders.get(model, self.encoders["cl100k_base"])
return len(encoder.encode(text))
def count_messages(self, messages: list[dict], model: str = "gpt-4") -> int:
"""Zähle Tokens für Message Array (mit Overhead)"""
encoder = self.encoders.get(model, self.encoders["cl100k_base"])
total = 0
# OpenAI Overhead: ~4 Tokens pro Message
total += 4 * len(messages)
for msg in messages:
total += len(encoder.encode(msg.get("content", "")))
if "name" in msg:
total += len(encoder.encode(msg["name"]))
total += 1 # Name prefix overhead
return total
counter = TokenCounter()
@app.post("/api/tokenize")
async def tokenize(request: dict):
"""Endpoint: Zeige Token-Count VOR dem API Call"""
model = request.get("model", "gpt-4")
tokens = counter.count_messages(request.get("messages", []), model)
return {
"tokens": tokens,
"estimated_cost_usd": (tokens / 1000) * 0.03 # gpt-4 pricing
}
# Vor jedem Request: Token zählen
@app.post("/v1/completions")
async def completions(request: CompletionRequest):
model = request.get("model", "gpt-4")
tokens = counter.count_messages(request.messages, model)
# Log für Cost-Tracking
print(f"Request tokens: {tokens}")
# Übergebe Estimate an Router
selected = router.select_model(tokens, strategy)
...
7. Praktisches Gateway-Beispiel mit LiteLLM
LiteLLM abstrahiert mehrere LLM-Provider:
from litellm import completion
import litellm
# Fallback Chain konfigurieren
litellm.set_verbose(True)
litellm.model_list = [
{
"model_name": "my-gpt4",
"litellm_params": {
"model": "gpt-4",
"api_key": os.getenv("OPENAI_API_KEY")
}
},
{
"model_name": "my-gpt4",
"litellm_params": {
"model": "claude-3-opus-20240229",
"api_key": os.getenv("ANTHROPIC_API_KEY")
}
},
{
"model_name": "my-gpt4",
"litellm_params": {
"model": "llama2-70b",
"api_base": "http://localhost:8000/v1",
}
}
]
@app.post("/v1/chat/completions")
async def unified_chat(request: dict):
"""Ein Endpoint, mehrere Backends"""
try:
response = completion(
model="my-gpt4",
messages=request["messages"],
max_tokens=request.get("max_tokens", 1000),
temperature=request.get("temperature", 0.7),
num_retries=2, # Fallback automatisch
)
return response
except Exception as e:
return {"error": str(e)}
Zusammenfassung: AI Gateway Pattern
| Komponente | Aufgabe | Tool/Technik |
|---|---|---|
| Auth | API Keys validieren | JWT, API Key Hash |
| Rate Limiting | Token-basiert drosseln | Leaky Bucket, Redis |
| Routing | Intelligente Model-Wahl | Cost/Speed/Quality Score |
| Fallback | Versuch nächstes Model | Chain of Responsibility |
| Caching | Semantische Duplikate | Embeddings + Cosine Sim |
| Token Counting | Vorher zählen, nicht hinterher | tiktoken, LiteLLM |
| Cost Tracking | Geld zählen | SQL Events, Dashboard |
Das Gateway ist deine Kontrollinstanz. Alles geht da durch. Deshalb lohnt sich hier jede Millisekunde Optimierung.
Quellen und Links
- LiteLLM Docs — Multi-Provider LLM Abstraction
- Kong API Gateway — Production API Gateway
- tiktoken — Token Counting für OpenAI Models
- GPTCache — Semantic Caching für LLMs
- OpenAI Rate Limits — Offizielle Limits
- Traefik AI Gateway Pattern — Gateway Pattern
- Redis for Rate Limiting — Implementation Guide
