Ein Klick auf „Generieren“, dann mehrere Minuten Wartezeit: Bei einer Anwendung für KI-Musik liegt die eigentliche Integrationsarbeit zwischen dem Absenden einer Idee und dem ersten abspielbaren Track.
Was zeigt der Client währenddessen an? Wann darf er aufhören, den Status abzufragen? Und was passiert, wenn die Verbindung abbricht, obwohl der Auftrag bereits angenommen wurde?
Bei unserem Projekt KlangKI – KI-Songs auf Deutsch erstellen begegnen uns diese Fragen sowohl im Webprodukt als auch bei der Anbindung von KI-Assistenten. Dieser Beitrag betrachtet den öffentlich dokumentierten API-Vertrag und unseren offenen TypeScript-MCP-Adapter. Im Mittelpunkt steht die Client-Seite des Ablaufs; interne Modell- oder Queue-Implementierungen werden dafür nicht vorausgesetzt.
1. Auftrag und Ergebnis getrennt behandeln
Die Generierung dauert normalerweise etwa drei bis fünf Minuten. Der Client bekommt beim Start eine Aufgaben-ID und fragt das Ergebnis später separat ab.
Web-Client oder KI-Assistent | | Generierung anfordern v KlangKI API ----> taskId ^ | Status mit taskId abfragen | Client erhält Status und Tracks | v audioUrl im Player verwenden Die wichtigen Schnittstellen sind:
| Operation | Methode und Route |
|---|---|
| Musikgenerierung starten | POST /api/music/generate |
| Aufgabenstatus lesen | GET /api/music/task/status/{taskId} |
| Aufgabenhistorie lesen | GET /api/music/my-music |
Für eine Beschreibung wird beispielsweise dieser Request-Body gesendet:
{ "formData": { "type": "text", "prompt": "Deutscher Indie-Pop über einen Neuanfang in Berlin, warme Gitarren, optimistische Stimmung", "isInstrumental": false } } Nach erfolgreicher Übermittlung sollte ein Client die zurückgegebene taskId aufbewahren. Damit lässt sich die Statusabfrage nach einer Unterbrechung wieder aufnehmen. Ein Seitenwechsel muss dann nicht automatisch zu einem neuen Generierungsauftrag führen.
API-Schlüssel gehören dabei in eine vertrauenswürdige Serverumgebung oder in die lokale Konfiguration des MCP-Prozesses. Ein geheimer Account-Schlüssel gehört nicht in ausgelieferten Browsercode.
2. HTTP-Erfolg und fachlichen Erfolg unterscheiden
Die API verwendet eine Antwort-Hülle mit code, message und data. Ein fachlicher Fehler kann auch mit HTTP 200 ankommen. Deshalb reicht die Prüfung von response.ok allein nicht aus.
Der Adapter prüft beide Ebenen. Hier ein gekürzter Ausschnitt aus der Antwortbehandlung, nachdem die JSON-Struktur bereits validiert wurde:
if (!response.ok || envelope.code !== "SUCCESS") { throw new ApiError( envelope.code === "SUCCESS" ? `HTTP_${response.status}` : envelope.code, typeof envelope.message === "string" ? envelope.message : `API error (HTTP ${response.status}).`, isRecord(envelope.data) ? envelope.data : undefined, ); } Das ist besonders bei Fehlern wie fehlenden Credits wichtig. Ein Client soll diesen Fall verständlich anzeigen können und eventuell mitgelieferte Informationen behalten. Der vollständige Adapter behandelt außerdem ungültiges JSON, fehlende Antwortfelder und Netzwerkfehler.
3. Polling nach einer Serverentscheidung beenden
Eine verbreitete Versuchung ist, im Client eine eigene Liste aller abschließenden Statuswerte zu pflegen. Ändert sich der Vertrag, können verschiedene Clients dann unterschiedlich entscheiden, ob eine Aufgabe noch läuft.
Unser Status-Endpunkt liefert dafür isTerminal. Der Client verwendet dieses Feld, um das Polling zu beenden. Für die Darstellung bleiben status, vorhandene Tracks und ein möglicher errorCode relevant.
isTerminal: true bedeutet, dass der Vorgang abgeschlossen ist. Es bedeutet nicht automatisch, dass ein Song erfolgreich erzeugt wurde.
Für die MCP-Nutzung empfehlen wir ungefähr 15 bis 30 Sekunden Abstand zwischen Statusabfragen. Ein Client sollte jeweils die vorherige Abfrage abwarten, damit bei langsamen Antworten keine überlappenden Requests entstehen.
Einzelne Tracks können bereits verfügbar sein, bevor der gesamte Auftrag abgeschlossen ist. Deshalb sind zwei Entscheidungen getrennt zu treffen:
- Gibt es bereits einen Track mit einer abspielbaren
audioUrl? - Ist die Aufgabe insgesamt abgeschlossen, sodass keine weiteren Statusabfragen nötig sind?
Das ermöglicht eine frühere Wiedergabe, ohne den restlichen Auftrag aus dem Blick zu verlieren. Ob und wie Teilergebnisse angezeigt werden, ist eine Entscheidung der jeweiligen Oberfläche.
4. Ein Timeout ist ein unbekanntes Ergebnis
Der kritischste Fall entsteht beim Starten einer Generierung:
Client sendet POST | Server nimmt den Auftrag an | Antwort erreicht den Client nicht | Client sieht einen Timeout Der Auftrag kann zu diesem Zeitpunkt bereits existieren und Credits verbrauchen. Ein automatisches Wiederholen des POST-Requests könnte einen zweiten kostenpflichtigen Auftrag anlegen.
Unser Adapter wiederholt HTTP-Anfragen deshalb nicht automatisch. Bei einer unsicheren Musikübermittlung fordert er dazu auf, zuerst die Aufgabenhistorie mit kisong_list_tasks oder im Webkonto zu prüfen.
Für eine Oberfläche würde ich diesen Fall ausdrücklich als „Übermittlung unklar“ darstellen. Eine passende Meldung wäre:
Die Verbindung wurde unterbrochen. Dein Auftrag könnte bereits gestartet sein. Prüfe zuerst deine letzten Generierungen, bevor du einen neuen Auftrag sendest.
Die Historie ist dabei eine Hilfe zur Klärung, keine Garantie für eine automatische Zuordnung: Wenn jemand mehrere ähnliche Aufträge kurz hintereinander startet, kann die Zuordnung ohne zusätzliche Kennung mehrdeutig sein.
Für eine zukünftige API-Erweiterung wäre ein serverseitig unterstützter Idempotenzschlüssel eine Möglichkeit, Wiederholungen eindeutig demselben Auftrag zuzuordnen. Der hier beschriebene Adapter setzt eine solche Funktion nicht voraus.
5. Abbrechen der Abfrage und Abbrechen des Auftrags trennen
Ein lokales AbortSignal kann einen laufenden HTTP-Request beenden. Daraus folgt jedoch nicht, dass die Musikgenerierung auf dem Server gestoppt wurde.
Im Adapter werden das konfigurierte Zeitlimit und das Abbruchsignal des MCP-Requests kombiniert:
const timeout = AbortSignal.timeout(this.config.timeoutMs); const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout; Eine Schaltfläche „Nicht weiter warten“ sollte daher genau diese lokale Wirkung vermitteln. Ohne einen bestätigten serverseitigen Abbruch sollte sie nicht versprechen, den Auftrag oder seine Abrechnung rückgängig zu machen.
6. Dieselben Grenzen für KI-Assistenten sichtbar machen
Beim Model Context Protocol (MCP) erhält ein Assistent Werkzeuge für einzelne Operationen. Unser Adapter trennt beispielsweise:
-
kisong_get_credits: Musik-Credits prüfen. -
kisong_generate_song: einen Generierungsauftrag starten. -
kisong_get_task: den Fortschritt eines bekannten Auftrags lesen. -
kisong_list_tasks: die Aufgabenhistorie zur Klärung heranziehen.
Generierungswerkzeuge sind mit idempotentHint: false beschrieben. Status- und Historienwerkzeuge haben Hinweise auf lesende, idempotente Nutzung. Solche Metadaten helfen dem Client bei der Einordnung; sie ersetzen weder Berechtigungsprüfungen noch die eigentliche Fehlerbehandlung.
Auch die Werkzeugbeschreibungen gehören zum Integrationsdesign: Sie nennen den Credit-Verbrauch, die asynchrone Aufgaben-ID und die Regel, nach einem Timeout nicht automatisch erneut zu generieren.
Die vollständigen Eingabegrenzen und Endpunkte stehen im API-Vertrag des Adapters. Das Repository enthält außerdem Tests mit einer lokalen Mock-API für Validierung, fachliche Fehler und das Ausbleiben automatischer Wiederholungen.
Was ich daraus für die Oberfläche ableite
Für eine brauchbare Oberfläche würde ich mindestens zwischen „wird übermittelt“, „Generierung läuft“, „Übermittlung unklar“ und einem abschließenden Ergebnis unterscheiden. Verfügbare Tracks können unabhängig davon angezeigt werden.
Ein Fortschrittsbalken mit erfundenen Prozentwerten hilft dabei wenig. Eine gespeicherte Aufgaben-ID, ein verständlicher Status und ein Weg zurück zur Historie helfen unmittelbar, wenn etwas schiefgeht.
Wer den Anwendungsfall ausprobieren möchte, findet auf KlangKI die Generierung aus Beschreibungen und eigenen Songtexten. Der offene MCP-Adapter zeigt dazu die konkrete API-Anbindung in TypeScript.
Wie behandelt ihr in euren Anwendungen den Fall, dass ein kostenpflichtiger Auftrag möglicherweise angenommen wurde, die Antwort aber verloren geht?