Volle Kontrolle über LLMs: Mein Setup mit Ollama, LiteLLM und Claude Code

Wie ich lokale und Cloud-LLMs mit voller Kontrolle über Kosten und Modelle nutze - vom Gaming-PC bis Kubernetes. Ein DevOps-Ansatz für DSGVO-konforme KI-Infrastruktur.

Martin Stagl 5 Min. Lesezeit
LLM AI Self-Hosting DSGVO

Volle Kontrolle über LLMs: Mein Setup mit Ollama, LiteLLM und Claude Code

Volle Kontrolle über LLMs: Mein Setup mit Ollama, LiteLLM und Claude Code

Als DevOps Engineer war es mir wichtig, volle Kontrolle über meine LLM-Nutzung zu haben - sowohl bei den Modellen als auch bei den Kosten. In diesem Artikel zeige ich, wie ich lokale und Cloud-LLMs kombiniere und dabei ein professionelles Setup mit Cost Tracking aufbaue.

Hardware: RTX 3090 als LLM-Powerhouse

Mein Setup basiert auf einer NVIDIA RTX 3090 mit 24GB VRAM. Das ist genug für:

  • Kleinere Llama-Modelle (z.B. Llama 3.1 8B)
  • Mistral-Modelle
  • Fokus auf europäische LLMs (wichtig für DSGVO-Compliance!)

Ollama: Lokale LLMs auf der Gaming-GPU

Ollama läuft direkt auf meinem Gaming-PC und managed die lokalen Modelle.

Docker Compose Setup (Lokal)

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama-data:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    environment:
      - NVIDIA_VISIBLE_DEVICES=0
      - OLLAMA_HOST=0.0.0.0:11434
    restart: unless-stopped

volumes:
  ollama-data:
    driver: local

Kubernetes Deployment

Für Production läuft Ollama in meinem On-Premises Kubernetes-Cluster:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: ai-services
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
      - name: ollama
        image: ollama/ollama:latest
        ports:
        - containerPort: 11434
          name: http
        env:
        - name: OLLAMA_HOST
          value: "0.0.0.0:11434"
        resources:
          requests:
            nvidia.com/gpu: 1
            memory: "8Gi"
            cpu: "2"
          limits:
            nvidia.com/gpu: 1
            memory: "16Gi"
            cpu: "4"
        volumeMounts:
        - name: ollama-models
          mountPath: /root/.ollama
      nodeSelector:
        nvidia.com/gpu.present: "true"
      volumes:
      - name: ollama-models
        persistentVolumeClaim:
          claimName: ollama-models-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: ollama
  namespace: ai-services
spec:
  selector:
    app: ollama
  ports:
  - port: 11434
    targetPort: 11434
    name: http
  type: ClusterIP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ollama-models-pvc
  namespace: ai-services
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
  storageClassName: local-path

Modelle laden

# Lokal
docker exec -it ollama ollama pull llama3.1:8b
docker exec -it ollama ollama pull mistral:7b

# Kubernetes
kubectl exec -it deployment/ollama -n ai-services -- ollama pull llama3.1:8b
kubectl exec -it deployment/ollama -n ai-services -- ollama pull mistral:7b

LiteLLM: Der zentrale Proxy für alle Modelle

LiteLLM ist das Herzstück meines Setups. Es bietet:

  • Einheitliche API für alle LLM-Provider
  • Cost Tracking
  • Rate Limiting
  • Load Balancing
  • Model Fallbacks

Docker Compose Setup (Lokal)

version: '3.8'

services:
  litellm:
    image: ghcr.io/berriai/litellm:main-latest
    container_name: litellm
    ports:
      - "4000:4000"
    volumes:
      - ./litellm-config.yaml:/app/config.yaml
      - litellm-db:/app/database
    environment:
      - DATABASE_URL=sqlite:////app/database/litellm.db
      - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
    command: --config /app/config.yaml --detailed_debug
    restart: unless-stopped
    depends_on:
      - ollama

volumes:
  litellm-db:
    driver: local

LiteLLM Konfiguration

# litellm-config.yaml
model_list:
  # Ollama Models (lokal)
  - model_name: llama3.1
    litellm_params:
      model: ollama/llama3.1:8b
      api_base: http://ollama:11434
      
  - model_name: mistral
    litellm_params:
      model: ollama/mistral:7b
      api_base: http://ollama:11434
  
  # Claude Models (Cloud)
  - model_name: claude-sonnet
    litellm_params:
      model: claude-sonnet-4-20250514
      api_key: os.environ/ANTHROPIC_API_KEY
      
  - model_name: claude-opus
    litellm_params:
      model: claude-opus-4-20250514
      api_key: os.environ/ANTHROPIC_API_KEY

# Cost Tracking aktivieren
litellm_settings:
  success_callback: ["langfuse"]
  database_url: "sqlite:////app/database/litellm.db"
  store_model_in_db: true
  
# Rate Limiting
router_settings:
  routing_strategy: least-busy
  num_retries: 3
  timeout: 600
  
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: "sqlite:////app/database/litellm.db"

Kubernetes Deployment für LiteLLM

apiVersion: v1
kind: ConfigMap
metadata:
  name: litellm-config
  namespace: ai-services
data:
  config.yaml: |
    model_list:
      - model_name: llama3.1
        litellm_params:
          model: ollama/llama3.1:8b
          api_base: http://ollama.ai-services.svc.cluster.local:11434
          
      - model_name: mistral
        litellm_params:
          model: ollama/mistral:7b
          api_base: http://ollama.ai-services.svc.cluster.local:11434
      
      - model_name: claude-sonnet
        litellm_params:
          model: claude-sonnet-4-20250514
          api_key: os.environ/ANTHROPIC_API_KEY
          
    litellm_settings:
      success_callback: ["langfuse"]
      database_url: "postgresql://litellm:password@postgres:5432/litellm"
      store_model_in_db: true
      
    general_settings:
      master_key: os.environ/LITELLM_MASTER_KEY
      database_url: "postgresql://litellm:password@postgres:5432/litellm"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: litellm
  namespace: ai-services
spec:
  replicas: 2
  selector:
    matchLabels:
      app: litellm
  template:
    metadata:
      labels:
        app: litellm
    spec:
      containers:
      - name: litellm
        image: ghcr.io/berriai/litellm:main-latest
        ports:
        - containerPort: 4000
          name: http
        env:
        - name: LITELLM_MASTER_KEY
          valueFrom:
            secretKeyRef:
              name: litellm-secrets
              key: master-key
        - name: ANTHROPIC_API_KEY
          valueFrom:
            secretKeyRef:
              name: litellm-secrets
              key: anthropic-api-key
        - name: DATABASE_URL
          value: "postgresql://litellm:[email protected]:5432/litellm"
        volumeMounts:
        - name: config
          mountPath: /app/config.yaml
          subPath: config.yaml
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"
        livenessProbe:
          httpGet:
            path: /health
            port: 4000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 4000
          initialDelaySeconds: 10
          periodSeconds: 5
      volumes:
      - name: config
        configMap:
          name: litellm-config
---
apiVersion: v1
kind: Service
metadata:
  name: litellm
  namespace: ai-services
spec:
  selector:
    app: litellm
  ports:
  - port: 4000
    targetPort: 4000
    name: http
  type: ClusterIP
---
apiVersion: v1
kind: Secret
metadata:
  name: litellm-secrets
  namespace: ai-services
type: Opaque
stringData:
  master-key: "sk-your-master-key-here"
  anthropic-api-key: "sk-ant-your-key-here"

LiteLLM Ingress (optional)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: litellm-ingress
  namespace: ai-services
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - llm.yourdomain.com
    secretName: litellm-tls
  rules:
  - host: llm.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: litellm
            port:
              number: 4000

Coding CLIs: Von Claude Code zu Open Code

Claude Code CLI

Claude Code war mein erster Versuch, hat aber ein Flickering-Problem, das mich gestört hat:

# Installation
npm install -g @anthropic-ai/claude-code

# Verwendung
export ANTHROPIC_API_KEY="sk-ant-..."
claude-code

Pros:

  • Offizielle Anthropic Integration
  • Gute Code-Completion

Cons:

  • Flickering im Terminal
  • Keine Persistenz

Open Code (aktuell in Verwendung)

# Installation
npm install -g opencode

# Verwendung mit LiteLLM
export OPENAI_API_BASE="http://localhost:4000"
export OPENAI_API_KEY="sk-your-litellm-key"

opencode --model claude-sonnet

Pros:

  • Kein Flickering
  • Funktioniert mit LiteLLM

Cons:

  • Historie geht verloren bei /exit
  • Nicht perfekt

pi-mono: Österreichisches OSS-Tool (als nächstes zu testen)

pi-mono ist ein vielversprechendes Tool aus Österreich:

# Installation
git clone https://github.com/badlogic/pi-mono.git
cd pi-mono
npm install

# Setup
cp .env.example .env
# Configure your LiteLLM endpoint

# Run
npm start

Warum interessant:

  • Aus Österreich (wichtig für DSGVO) -Besseres Session Management
  • Focus auf Developer Experience

OpenWebUI: Web-Interface für alle Modelle

OpenWebUI bietet eine ChatGPT-ähnliche Oberfläche für alle Modelle:

Docker Compose

version: '3.8'

services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "3000:8080"
    volumes:
      - open-webui-data:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - OPENAI_API_BASE_URL=http://litellm:4000/v1
      - OPENAI_API_KEY=sk-your-litellm-key
    restart: unless-stopped
    depends_on:
      - ollama
      - litellm

volumes:
  open-webui-data:
    driver: local

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: open-webui
  namespace: ai-services
spec:
  replicas: 1
  selector:
    matchLabels:
      app: open-webui
  template:
    metadata:
      labels:
        app: open-webui
    spec:
      containers:
      - name: open-webui
        image: ghcr.io/open-webui/open-webui:main
        ports:
        - containerPort: 8080
          name: http
        env:
        - name: OLLAMA_BASE_URL
          value: "http://ollama.ai-services.svc.cluster.local:11434"
        - name: OPENAI_API_BASE_URL
          value: "http://litellm.ai-services.svc.cluster.local:4000/v1"
        - name: OPENAI_API_KEY
          valueFrom:
            secretKeyRef:
              name: litellm-secrets
              key: master-key
        volumeMounts:
        - name: data
          mountPath: /app/backend/data
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "500m"
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: open-webui-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: open-webui
  namespace: ai-services
spec:
  selector:
    app: open-webui
  ports:
  - port: 8080
    targetPort: 8080
    name: http
  type: ClusterIP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: open-webui-pvc
  namespace: ai-services
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: local-path

Cost Tracking mit LiteLLM

LiteLLM trackt automatisch alle API-Calls:

# Lokale Datenbank abfragen
sqlite3 /path/to/litellm.db

# Kosten pro Modell
SELECT 
  model,
  COUNT(*) as calls,
  SUM(response_cost) as total_cost,
  AVG(response_cost) as avg_cost
FROM litellm_logs
WHERE created_at > datetime('now', '-30 days')
GROUP BY model;

# Top User nach Kosten
SELECT 
  user,
  COUNT(*) as calls,
  SUM(response_cost) as total_cost
FROM litellm_logs
WHERE created_at > datetime('now', '-30 days')
GROUP BY user
ORDER BY total_cost DESC
LIMIT 10;

Kubernetes: Complete Stack Deployment

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ai-services
---
# Complete deployment script
apiVersion: v1
kind: ConfigMap
metadata:
  name: deployment-script
  namespace: ai-services
data:
  deploy.sh: |
    #!/bin/bash
    set -e
    
    echo "🚀 Deploying AI Services Stack..."
    
    # Apply in correct order
    kubectl apply -f namespace.yaml
    kubectl apply -f secrets.yaml
    kubectl apply -f postgres.yaml
    kubectl wait --for=condition=ready pod -l app=postgres -n ai-services --timeout=120s
    
    kubectl apply -f ollama-deployment.yaml
    kubectl wait --for=condition=ready pod -l app=ollama -n ai-services --timeout=120s
    
    kubectl apply -f litellm-deployment.yaml
    kubectl wait --for=condition=ready pod -l app=litellm -n ai-services --timeout=120s
    
    kubectl apply -f open-webui-deployment.yaml
    kubectl wait --for=condition=ready pod -l app=open-webui -n ai-services --timeout=120s
    
    echo "Deployment complete!"
    echo ""
    echo "Access your services:"
    echo "LiteLLM: http://$(kubectl get svc litellm -n ai-services -o jsonpath='{.status.loadBalancer.ingress[0].ip}'):4000"
    echo "OpenWebUI: http://$(kubectl get svc open-webui -n ai-services -o jsonpath='{.status.loadBalancer.ingress[0].ip}'):8080"

Monitoring Setup

# prometheus-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: litellm-monitor
  namespace: ai-services
spec:
  selector:
    matchLabels:
      app: litellm
  endpoints:
  - port: http
    path: /metrics
    interval: 30s

Kosten-Vergleich

SetupMonatliche KostenProsCons
Pure Cloud (Claude Opus)~€150-300Keine Hardware, Sofort verfügbarHohe laufende Kosten, Vendor Lock-in
Hybrid (Mein Setup)~€30-50Flexibilität, Cost ControlInitiale Hardware-Kosten
Pure Self-Hosted~€20-30Niedrigste KostenLimitiert auf kleinere Modelle

Annahme: 1000 API-Calls/Monat mit gemischter Nutzung

Nächste Schritte

  1. pi-mono testen - Besseres Session Management
  2. Langfuse Integration - Erweiterte Analytics
  3. Model Fine-Tuning - Eigene Modelle auf Ollama
  4. Auto-Scaling - Kubernetes HPA für LiteLLM

Fazit

Die RTX 3090 zahlt sich nach ~6 Monaten gegenüber reiner Cloud-Nutzung aus, und ich habe die Freiheit, jederzeit neue Modelle zu testen - besonders wichtig für europäische LLMs!

Resources


Fragen oder Anregungen? Feel free to reach out!

Share: