# Der Modell-Pförtner: Eine kleine KI wählt die richtige große

**Bauplan aus dem Werkzeugkasten von stevennoack.de · Stand: Juli 2026**

Wenn du mehrere KI-Modelle zur Auswahl hast, kennst du das Problem: Vor jeder
Frage überlegst du, welches Modell dran ist. Dieser Bauplan nimmt dir das ab.
Ein schnelles, günstiges Modell liest deine Anfrage, ordnet sie einer Schublade
zu (Code, Recherche, Strategie, Kreatives, Aktuelles, Alltag) und reicht sie an
deinen Spezialisten weiter. Im Modell-Menü heißt das Ganze dann einfach „Auto".

Bei mir läuft das seit Ende Juli 2026 im Alltag. Die Zuordnungstabelle unten ist
meine; deine wird anders aussehen, und genau dafür ist sie ein simples
Wörterbuch im Code.

## Was du brauchst

- **Open WebUI** (die Chat-Oberfläche, läuft lokal per Docker)
- Einen **OpenAI-kompatiblen Endpunkt** mit deinen Modellen dahinter. Bei mir
  ist das LiteLLM auf `http://litellm:4000/v1`, es geht aber jeder Dienst, der
  `/chat/completions` spricht.
- Zehn Minuten.

## Der Code

Das ist eine Open-WebUI-„Function" vom Typ Pipe. Speichere sie als
`auto-router.py` oder füge sie direkt im Admin-Bereich ein.

```python
"""
title: Auto (Router)
description: Wählt das Modell anhand der Anfrage. Ein kleines Modell sortiert, Spezialisten antworten.
version: 0.1.0
"""

import json
import os

import requests
from pydantic import BaseModel, Field

# Schublade -> Zielmodell. HIER passt du an, wer bei dir was bekommt.
ROUTING = {
    "code": "gpt-5.6-sol",
    "recherche": "gemini-3.1-pro-low",
    "strategie": "claude-fable-5",
    "kreativ": "kimi-k3",
    "aktuell": "grok-4.5",
    "schnell": "gpt-5.4-mini",
}
DEFAULT_MODEL = "claude-fable-5"  # Auffangnetz bei Unsicherheit
CLASSIFIER_MODEL = "claude-haiku-4-5-20251001"  # der Pförtner: klein und schnell

# Die komplette Anweisung steht in der NUTZER-Nachricht, nicht als System-Prompt.
# Grund: Manche Proxys und Brücken injizieren eigene System-Prompts, in denen
# deine System-Anweisung untergeht. In der Nutzer-Rolle gehört der Platz dir.
CLASSIFIER_PROMPT = (
    "Du bist ein Anfrage-Router. Ordne die folgende Anfrage GENAU EINER "
    "Kategorie zu:\n"
    "- code: Programmieren, Debugging, Skripte, Technik-Konfiguration\n"
    "- recherche: Analysieren/Zusammenfassen langer Texte, Faktenrecherche\n"
    "- strategie: Geschäfts- oder Projektentscheidungen, Beratung, Abwägungen\n"
    "- kreativ: Geschichten, Brainstorming, Wortspiele, künstlerische Ideen\n"
    "- aktuell: Tagesgeschehen, Nachrichten, Meinungen zu laufenden Ereignissen\n"
    "- schnell: kurze Faktenfragen, Alltagshilfe, Übersetzungen\n"
    "Wenn nichts eindeutig passt: strategie. Deine gesamte Antwort ist NUR das "
    "eine Kategorie-Wort, klein geschrieben. Beantworte die Anfrage NICHT.\n\n"
    "Anfrage: "
)


class Pipe:
    class Valves(BaseModel):
        base_url: str = Field(
            default="http://litellm:4000/v1",
            description="OpenAI-kompatible Basis-URL deines Modell-Endpunkts",
        )
        api_key: str = Field(
            default="", description="Leer = OPENAI_API_KEY aus der Container-Umgebung"
        )
        zeige_routing: bool = Field(
            default=True, description="Gewähltes Modell als erste Zeile der Antwort nennen"
        )

    def __init__(self):
        self.valves = self.Valves()

    def _headers(self):
        key = self.valves.api_key or os.getenv("OPENAI_API_KEY", "")
        return {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}

    def _letzte_nutzerfrage(self, messages):
        for m in reversed(messages):
            if m.get("role") != "user":
                continue
            content = m.get("content", "")
            if isinstance(content, list):  # multimodal: nur die Textteile
                content = " ".join(
                    p.get("text", "") for p in content if isinstance(p, dict)
                )
            return content
        return ""

    def _klassifiziere(self, frage):
        r = requests.post(
            f"{self.valves.base_url}/chat/completions",
            headers=self._headers(),
            json={
                "model": CLASSIFIER_MODEL,
                "messages": [
                    {"role": "user", "content": CLASSIFIER_PROMPT + frage[:4000]},
                ],
                "max_tokens": 10,
                "temperature": 0,
            },
            timeout=25,
        )
        r.raise_for_status()
        wort = r.json()["choices"][0]["message"]["content"].strip().lower()
        for kategorie in ROUTING:
            if kategorie in wort:
                return kategorie
        return None

    def pipe(self, body: dict):
        messages = body.get("messages", [])
        try:
            kategorie = self._klassifiziere(self._letzte_nutzerfrage(messages))
            model = ROUTING.get(kategorie, DEFAULT_MODEL)
        except Exception:
            kategorie, model = None, DEFAULT_MODEL

        if self.valves.zeige_routing:
            yield f"`→ {model}" + (f" ({kategorie})" if kategorie else " (Auffangnetz)") + "`\n\n"

        with requests.post(
            f"{self.valves.base_url}/chat/completions",
            headers=self._headers(),
            json={"model": model, "messages": messages, "stream": True},
            stream=True,
            timeout=600,
        ) as r:
            r.raise_for_status()
            for line in r.iter_lines():
                if not line.startswith(b"data: "):
                    continue
                data = line[len(b"data: "):]
                if data == b"[DONE]":
                    break
                try:
                    delta = json.loads(data)["choices"][0].get("delta", {})
                except (json.JSONDecodeError, IndexError, KeyError):
                    continue
                if delta.get("content"):
                    yield delta["content"]
```

## Einbauen

1. Open WebUI → Profilbild → **Admin Panel** → **Functions** → **+**
2. Code einfügen, speichern, die Function **aktivieren** (Schalter).
3. Seite neu laden. Im Modell-Menü steht jetzt „Auto (Router)".

Die erste Zeile jeder Antwort verrät, wohin geroutet wurde, zum Beispiel
`→ kimi-k3 (kreativ)`. So siehst du dem Pförtner bei der Arbeit zu und merkst
schnell, welche Schublade du umhängen willst.

## Drei Stolperfallen, die mich Zeit gekostet haben

1. **System-Prompts können verschluckt werden.** Läuft dein Endpunkt über eine
   Brücke oder einen Proxy, der eigene System-Prompts injiziert, ignoriert der
   Pförtner deine System-Anweisung und beantwortet die Frage, statt sie zu
   sortieren. Erkennbar an absurd hohen prompt_tokens. Lösung steht schon im
   Code: Die Anweisung gehört komplett in die Nutzer-Nachricht.
2. **Hintergrund-Jobs laufen sonst auch durch den Router.** Open WebUI erzeugt
   Titel, Folgefragen und Schlagworte mit dem gerade gewählten Modell, also mit
   „Auto". Stell unter Admin → Einstellungen → Oberfläche ein festes, kleines
   **Aufgaben-Modell** ein, sonst klassifiziert der Pförtner ernsthaft die
   Anweisung „erzeuge einen Titel".
3. **Denk-Modelle brauchen Luft.** Modelle mit eingebautem Nachdenken
   verbrauchen erst Tokens fürs Denken. Ist das Antwort-Budget zu klein,
   kommt eine leere Antwort zurück, ohne Fehlermeldung.

---
Aus der Werkstatt von Steven Noack · stevennoack.de · Fragen und Ergänzungen
gern per Mail an kontakt@stevennoack.de
