LLM-Anwendungen zu testen, ist eine leicht paradoxe Angelegenheit.
Tests sollen zuverlässig sein: dieselbe Eingabe, dasselbe Ergebnis, ein klarer Fehler, wenn etwas nicht funktioniert. Im Zentrum der Anwendung arbeitet jedoch ein Sprachmodell, das gerade deshalb nützlich ist, weil es nicht immer exakt dasselbe Ergebnis liefert.
Man kann für jeden Test die echte API aufrufen. Dann kostet die Test-Suite Geld, braucht Internet, wartet auf einen externen Dienst und bekommt im ungünstigsten Moment eine überraschend kreative Antwort.
Man kann das Modell lokal betreiben. Dann braucht der CI-Runner plötzlich mehr Arbeitsspeicher als manche Produktionsmaschine. Oder man stellt einen kleinen Tintenfisch daneben.
Cuttlefish ist ein eigenständiger, OpenAI-kompatibler Mockserver für Integrations- und End-to-End-Tests. Er hat acht Arme und zwei Tentakel, aber keine Modellgewichte, keine GPU und keinen besonderen Drang, Gedichte über Datenbanken zu verfassen.
Cuttlefish gibt sich gegenüber einer Anwendung als OpenAI-kompatibler Dienst aus. Hinter der Fassade arbeiten keine echten Gewichte, nur kleine, deterministische Szenarien.
Ein bestehender OpenAI-Client braucht dafür lediglich eine andere Basis-URL:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="cuttlefish",
)
response = client.chat.completions.create(
model="cuttlefish/eliza",
messages=[
{
"role": "user",
"content": "Unsere Tests funktionieren nur manchmal.",
}
],
)
print(response.choices[0].message.content)
Die Anwendung spricht weiterhin das vertraute Chat-Completions-Protokoll. Cuttlefish nimmt die Anfrage entgegen, validiert sie, wählt das passende Szenario und liefert eine Antwort in der erwarteten Form zurück. Das kann eine normale Chat-Antwort sein, ein Server-Sent-Events-Stream, ein Tool Call oder ein sorgfältig vorbereiteter Fehler.
Mitgeliefert werden unter anderem:
ELIZA spricht übrigens auch Deutsch und Französisch.
Sprachmodelle machen Software nicht untestbar. Sie verschieben nur die Grenze dessen, was sich zuverlässig kontrollieren lässt. Bei klassischer Software wird häufig geprüft, ob eine bestimmte Eingabe zu einer bestimmten Ausgabe führt. Bei einem Sprachmodell ist genau diese Ausgabe nicht immer stabil und soll es oft auch gar nicht sein.
Stabil bleiben müssen jedoch die Strukturen darum herum. Die Anwendung muss eine Antwort korrekt empfangen, einen Stream verarbeiten, einen Tool Call erkennen, einen Fehler anzeigen und nach einem Timeout einen sinnvollen nächsten Schritt wählen. Sie darf unterschiedliche Texte erhalten. Sie sollte deshalb nicht unterschiedlich zuverlässig funktionieren.
Die Antwort darf variieren. Das Verhalten der Anwendung nicht.
Cuttlefish macht also nicht das Sprachmodell berechenbar. Er sorgt dafür, dass die Software, die mit seiner Unberechenbarkeit umgehen muss, reproduzierbar getestet werden kann.
Eine einzelne Funktion zu ersetzen, reicht für Unit-Tests häufig aus. Sie beantwortet aber nicht die Fragen, die an den Grenzen einer LLM-Anwendung entstehen.
Verarbeitet der Client die tatsächliche JSON-Struktur korrekt? Funktioniert die Oberfläche bereits nach dem ersten Streaming-Chunk? Was geschieht, wenn statt einer Textantwort ein Tool Call kommt? Wird ein unterbrochener Stream erkannt? Greift die Wiederholungslogik bei einem HTTP-Fehler? Bleibt die Anwendung bedienbar, wenn das erste Token länger braucht?
An diesen Stellen wird nicht das Sprachmodell geprüft. Getestet wird alles, was darum herum entstanden ist:
Cuttlefish sitzt dafür an derselben Stelle wie später der echte Provider. Die Anwendung muss keinen besonderen Testpfad kennen. Sie spricht einfach mit einem anderen Server.
Das ist der entscheidende Unterschied zu einem Mock innerhalb des Anwendungscodes: Getestet wird nicht nur, ob eine Funktion aufgerufen wurde. Getestet wird, ob die beteiligten Komponenten über das tatsächliche Protokoll miteinander funktionieren.
Für wiederkehrende Abläufe lassen sich eigene Modelle in einer YAML-Datei definieren:
models:
cuttlefish/customer-refund:
strategy: scripted
config:
steps:
- exact: "Refund order ORD-123"
reply: "The refund has been queued."
- exact: "Look up order ORD-123"
tool_call:
name: lookup_order
arguments:
order_id: ORD-123
fallback: "No scripted response matched."
Der Modellname wird damit zum Szenario-Selektor. Eine Test-Suite kann gut lesbare Namen verwenden, ohne zusätzliche Steuerungs-APIs oder besondere Header einzuführen.
Die Szenarien sind bewusst zustandslos. Sie führen keine versteckten Gesprächszähler und teilen keinen veränderlichen Zustand zwischen Tests. Das macht parallele Testläufe erheblich weniger aufregend. Bei Test-Infrastruktur ist weniger Aufregung meistens ein Qualitätsmerkmal.
Auch die Konfiguration besteht ausschließlich aus Daten. Sie kann keinen Python-Code ausführen, keine Werkzeuge aufrufen und nicht auf das Netzwerk zugreifen. Was der Test bekommt, steht vorher fest. Das klingt zunächst nach einer Einschränkung. Für ein Werkzeug, dessen Hauptaufgabe Berechenbarkeit ist, handelt es sich eher um eine Stellenbeschreibung.
Der glückliche Pfad ist selten der Teil einer Anwendung, der nachts Probleme verursacht. Deshalb kann Cuttlefish auf Wunsch auch danebenbenehmen: HTTP-Fehler zurückgeben, Antworten verzögern, Payloads beschädigen oder einen Stream vorzeitig beenden.
Ein Rate Limit lässt sich beispielsweise über cuttlefish/error-429 reproduzieren, und zwar nicht ungefähr dann, wenn ein externer Provider gerade schlechte Laune hat, sondern bei jedem Testlauf.
Das ist besonders für Oberflächen und Orchestrierungslogik hilfreich. Ein Fehlerzustand, der sich zuverlässig auslösen lässt, wird eher sinnvoll gestaltet als einer, den man bisher nur aus einem Screenshot im Ticketsystem kennt.
Auch Ladezustände profitieren davon. Eine Oberfläche, die ausschließlich mit schnellen Antworten entwickelt wurde, ist häufig vor allem eines: optimistisch. Ein absichtlich langsames Szenario zeigt, ob ein Spinner tatsächlich erscheint, ob Bedienelemente korrekt gesperrt werden und ob Nutzerinnen und Nutzer erkennen, dass die Anwendung noch arbeitet.
In einer guten Test-Suite werden Fehler vorbereitet, nicht bloß ertragen.
Cuttlefish simuliert ein Protokoll, keine Intelligenz.Ob ein Prompt gute Ergebnisse liefert, ob ein Modell Fakten korrekt wiedergibt oder ob eine Ausgabe den richtigen Ton trifft, beantworten andere Werkzeuge: Evaluationsframeworks, Sicherheitsbenchmarks, am Ende immer der echte Provider selbst.
Auch die OpenAI-API bildet Mocktopus nur in Teilen ab. Der aktuelle Funktionsumfang konzentriert sich auf einen klar definierten Ausschnitt der Chat-Completions- und Models-Schnittstellen, mit reproduzierbar angenäherten statt providergetreu berechneten Tokenzahlen. Multimodale Ausgaben, Audio und Logprobs bleiben draußen.
Diese Begrenzung ist Absicht. Ein Testwerkzeug, das beginnt, die gesamte Welt nachzuspielen, wird dadurch selten besser. Sein Nutzen liegt in einem kleinen, verlässlich erfüllten Vertrag, samt der unangenehmen Klauseln darin.
Vor jeder Veröffentlichung sollte eine Anwendung deshalb weiterhin gegen das tatsächlich eingesetzte Modell geprüft werden. Mocktopus macht diesen Test nur seltener notwendig, ersetzen kann er ihn nicht.
Cuttlefish löst kein urbanstudio-spezifisches Problem.
Fast jedes Team, das Anwendungen mit LLM-Schnittstellen entwickelt, trifft früher oder später auf dieselben Fragen: Wie testet man Streaming? Wie reproduziert man Tool Calls? Wie läuft die Test-Suite offline? Wie verhindert man, dass eine externe API aus einem deterministischen Test einen Feldversuch macht?
Solche Infrastruktur gewinnt durch Öffentlichkeit.
Ein Mockserver ist nur dann wirklich vertrauenswürdig, wenn nachvollziehbar ist, was er annimmt, wie er Anfragen verarbeitet und welche Antworten er erzeugt. Gerade bei einem Werkzeug, dessen Hauptaufgabe Berechenbarkeit ist, wäre eine Blackbox eine seltsame Produktentscheidung.
Öffentliche Entwicklung bringt außerdem mehr reale Integrationen mit sich. Unterschiedliche SDKs setzen leicht unterschiedliche Schwerpunkte. Anwendungen verwenden Felder, Kombinationen und Streaming-Abläufe, die in der eigenen Test-Suite nie auftauchen.
Fehlerberichte und kleine Kompatibilitätsbeispiele helfen hier meist mehr als eine lange interne Featureliste. Und schließlich mögen wir Werkzeuge mit einem klaren Zweck. Wenn etwas klein, verständlich und für andere nützlich ist, muss daraus nicht zwangsläufig ein Produkt, ein Kundenkonto oder ein „Free Tier mit großzügigen 20 Tentakel-Minuten pro Monat" werden.
Cuttlefish steht deshalb unter der 0BSD-Lizenz. Sie erlaubt die Nutzung, Veränderung und Weitergabe des veröffentlichten Codes ohne eine Pflicht zur Namensnennung.
Mit anderen Worten: Nehmen Sie ihn mit.
Auch intern folgt Cuttlefish einem Prinzip, das uns bei größeren Plattformen beschäftigt: Zuständigkeiten sollten klar bleiben.
Eine Anfrage wird validiert und in einen unveränderlichen Kontext überführt. Eine Szenario-Strategie erzeugt daraus ein neutrales Ergebnis. Erst danach kümmern sich andere Teile um Tokenzählung, IDs, Zeitstempel, Verzögerungen, Fehler sowie die Ausgabe als JSON oder Stream.
Die Strategie selbst weiß nichts über HTTP und schläft nicht zwischen einzelnen Chunks. Der Streaming-Code ruft nicht heimlich ein zweites Mal die Strategie auf. Dadurch bleiben gestreamte und nicht gestreamte Antworten inhaltlich gleich. Zugleich lässt sich immer nachvollziehen, ob ein Fehler im vorbereiteten Szenario, in der API-Antwort oder beim Streaming entsteht.
Für einen kleinen Mockserver klingt das nach auffällig viel Architektur. Test-Infrastruktur braucht aber gerade diese innere Ordnung: Sie soll Fehler sichtbar machen, nicht neue Rätsel hinzufügen.
Cuttlefish lässt sich ohne dauerhafte Installation starten:
uvx cuttlefish serve
Alternativ lässt sich das Repository als Docker-Image erstellen und starten:
docker build -t cuttlefish.
docker run --rm -p 8000:8000 cuttlefish
Danach ist der Dienst unter http://localhost:8000/v1 erreichbar. Eine beliebige nicht leere API-Key-Zeichenfolge genügt, solange keine Bearer-Authentifizierung ausdrücklich konfiguriert wurde.
Quellcode, Dokumentation, Beispiele und Kompatibilitätsmatrix liegen auf GitHub: urbanstudioGmbH/cuttlefish
Cuttlefish ist klein, deterministisch und gelegentlich absichtlich unkooperativ.
Genau so, wie ein gutes Testwerkzeug sein sollte.