Kimi K3.1 Pro bricht lange Antworten kommentarlos ab
Wer Kimi K3.1 Pro über einen OpenAI-kompatiblen Client nutzt, erhält bei langen Generierungen plötzlich abgeschnittene Antworten. Die Anfrage gilt als erfolgreich und der Stream schließt sauber, ohne dass der Code einen Fehler meldet.

Sie richten einen bestehenden OpenAI-kompatiblen Client auf Kimi K3.1 Pro aus und er funktioniert beim ersten Versuch. Dann kommt eine lange Generierung mitten im Satz abgeschnitten zurück, und nichts in Ihrem Code hat sich beschwert. Die Anfrage war erfolgreich, der Stream wurde sauber geschlossen, und Ihr Nutzer hat die halbe Antwort erhalten.
Am Ende können Sie einen abgeschnittenen Stream erkennen, Anfragen anhand des dokumentierten Kontextlimits dimensionieren und anhand von Belegen statt Bauchgefühl zwischen den Varianten Pro und Highspeed wählen.
Jede Chat-Completion kommt mit einem finish_reason zurück. Er lautet stop, wenn das Modell natürlich fertig geworden ist, und length, wenn es an Ihre max_tokens-Grenze gestoßen ist. Bei einem nicht-streamenden Aufruf sehen Sie ihn am Response-Objekt. Bei einem streamenden Aufruf trifft er im letzten Chunk ein, und die meisten handgeschriebenen Stream-Schleifen lesen nur delta.content.
Das ist der Fehler. Ihre Schleife verarbeitet Text, die Verbindung wird geschlossen, und finish_reason == "length" landet in einem Chunk, den Sie nie überprüft haben. Das Modell hätte noch mehr zu sagen gehabt, und Ihre Anwendung hat eine abgeschnittene Antwort gespeichert, als wäre sie vollständig.
Der Parameter max_tokens verschärft das Problem. Lassen Sie ihn ungesetzt, wenden manche Anbieter einen konservativen Standardwert an. Setzen Sie ihn zu hoch, kann ein langer Prompt das Kontextlimit überschreiten, was die gesamte Anfrage fehlschlagen lässt, statt sie abzuschneiden.
Diese Schleife liest den Text und zeichnet den Abbruchgrund aus dem letzten Chunk auf.
from openai import OpenAI client = OpenAI ( base_url = " https://api.example.com/v1 ", api_key = " sk-... ", ) stream = client. chat. completions. create ( model = " kimi-k3.1-pro ", # verwenden Sie die exakte ID, die Ihr Anbieter angibt messages = [{ " role ": " user ", " content ": user_prompt }], max_tokens = 2048, stream = True, ) finish_reason = None for chunk in stream: choice = chunk. choices [ 0 ] if choice. delta. content: yield choice. delta. content if choice. finish_reason: finish_reason = choice. finish_reason if finish_reason == " length ": raise RuntimeError ( " Generation hit the token cap mid-output " ) Enter fullscreen mode Exit fullscreen mode Die Prüfung läuft nach der Schleife, nicht innerhalb von ihr, weil der letzte Chunk oft finish_reason mit leerem Inhalt trägt. Ein Fehler an dieser Stelle verwandelt eine stille Abschneidung in einen sichtbaren Fehler, den Ihr Retry-Pfad behandeln kann.
Die Modell-IDs unterscheiden sich zwischen Anbietern, also prüfen Sie die Zeichenkette vor dem Ausliefern gegen die Modellliste Ihres Anbieters. Ein Tippfehler liefert einen 404, der wie ein Authentifizierungsproblem aussieht.
Kimi liefert K3.1 Pro in zwei Varianten aus, und es handelt sich um separate Modell-Strings rather than eine Geschwindigkeitseinstellung an einem Endpoint. Der Wechsel des Strings ändert Latenz und Ausgabe, behandeln Sie die Wahl also als echte Entscheidung.
Gehen Sie nicht davon aus, dass Highspeed denselben Text schneller liefert. Führen Sie Ihren eigenen Paritätscheck an einer Stichprobe echter Prompts durch, bevor Sie irgendetwas migrieren, das einen nachgelagerten Parser speist.
def parity_report ( prompt, model_a, model_b ): a = client. chat. completions. create ( model = model_a, messages = prompt, temperature = 0 ). choices [ 0 ]. message. content b = client. chat. completions. create ( model = model_b, messages = prompt, temperature = 0 ). choices [ 0 ]. message. content return { " identical ": a == b, " a ": a, " b ": b } Enter fullscreen mode Exit fullscreen mode Legen Sie den Modell-String in einer Konstante fest. Wenn ein Variantenwechsel stillschweigend die Ausgabe verändert, soll der Diff ein bewusster Commit sein und keine Konfigurationsänderung, die niemand bemerkt hat.
Abschneidung und Anfragefehler haben gegensätzliche Ursachen. Ein Fehler tritt auf, wenn Prompt plus max_tokens das Kontextlimit überschreiten, und eine Abschneidung, wenn max_tokens kleiner ist, als die Antwort benötigt. Budgetieren Sie explizit gegen das Limit, das Ihr Anbieter dokumentiert.
def budgeted_max_tokens ( prompt_tokens, context_limit, cap = 4096 ): remaining = context_limit - prompt_tokens if remaining <= 0: raise ValueError ( f " Prompt exceeds the context limit by { - remaining } tokens " ) return min ( cap, remaining ) Enter fullscreen mode Exit fullscreen mode Zählen Sie den Prompt mit demselben Tokenizer, den der Anbieter verwendet, nicht mit einer Zeichen-Heuristik. Eine grobe Schätzung reicht für Dashboards, aber die Anfrage, die die Grenze überschreitet, ist genau die, die Sie schlecht gemessen haben.
Eine Retry-Schleife, die eine Anfrage mit Tool-Aufrufen erneut sendet, kann einen Seiteneffekt doppelt auslösen. Rate-Limit-Fehler sind sicher wiederholbar; ein halb ausgeführter Tool-Aufruf nicht.
import time def call_with_retry ( fn, attempts = 4, base = 1.0 ): for attempt in range ( attempts ): try: return fn () except Exception as exc: if attempt == attempts - 1 or " 429 " not in str ( exc ): raise time. sleep ( base * 2 ** attempt ) Enter fullscreen mode Exit fullscreen mode Dies wiederholt nur den Rate-Limit-Fehler und reicht alles andere weiter. Geben Sie Tool-Aufrufen ihren eigenen Idempotenz-Schlüssel, damit ein Retry ein Problem der Duplikaterkennung ist und keine Doppelabbuchung.
Kimi K3.1 Pro und K3.1 Pro Highspeed kündigen die beiden Varianten an. Ich habe die Stream-Abruch-Prüfungen, das Token-Budgeting und das Retry-Handling ergänzt, die in der Ankündigung fehlen.
Diese Artikel werden ohne Paywall, Sponsoren oder Tracking recherchiert und veröffentlicht. Falls einer davon Ihnen einen Nachmittag erspart hat, hält ein kleines Trinkgeld sie am Laufen.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
You point an existing OpenAI-compatible client at Kimi K3.1 Pro and it works on the first try. Then a long generation comes back cut off mid-sentence, and nothing in your code complained. The request succeeded, the stream closed cleanly, and your user got half an answer.
By the end you can detect a truncated stream, size requests against the documented context limit, and choose between the Pro and Highspeed variants with evidence instead of vibes.
Every chat completion comes back with a finish_reason. It is stop when the model finished naturally and length when it hit your max_tokens cap. In a non-streaming call you would see it on the response object. In a streaming call it arrives on the final chunk, and most hand-written stream loops only read delta.content.
That is the failure. Your loop consumes text, the connection closes, and finish_reason == "length" lands in a chunk you never inspected. The model had more to say and your application stored a truncated answer as if it were complete.
The max_tokens parameter makes this worse. Leave it unset and some providers apply a conservative default. Set it too high and a long prompt pushes you over the context limit, which fails the whole request instead of truncating it.
This loop reads the text and records the termination reason from the last chunk.
from openai import OpenAI client = OpenAI ( base_url = " https://api.example.com/v1 ", api_key = " sk-... ", ) stream = client. chat. completions. create ( model = " kimi-k3.1-pro ", # use the exact ID your provider lists messages = [{ " role ": " user ", " content ": user_prompt }], max_tokens = 2048, stream = True, ) finish_reason = None for chunk in stream: choice = chunk. choices [ 0 ] if choice. delta. content: yield choice. delta. content if choice. finish_reason: finish_reason = choice. finish_reason if finish_reason == " length ": raise RuntimeError ( " Generation hit the token cap mid-output " ) Enter fullscreen mode Exit fullscreen mode The check runs after the loop, not inside it, because the final chunk often carries finish_reason with empty content. Raising here turns a silent truncation into a visible failure your retry path can handle.
Model IDs differ between providers, so confirm the string against your provider's model list before shipping. A typo returns a 404 that looks like an auth problem.
Kimi ships K3.1 Pro in two variants, and they are separate model strings rather than a speed setting on one endpoint. Swapping the string changes latency and output, so treat the choice as a real decision.
Do not assume Highspeed returns the same text faster. Run your own parity check on a sample of real prompts before you migrate anything that feeds a downstream parser.
def parity_report ( prompt, model_a, model_b ): a = client. chat. completions. create ( model = model_a, messages = prompt, temperature = 0 ). choices [ 0 ]. message. content b = client. chat. completions. create ( model = model_b, messages = prompt, temperature = 0 ). choices [ 0 ]. message. content return { " identical ": a == b, " a ": a, " b ": b } Enter fullscreen mode Exit fullscreen mode Pin the model string in one constant. If a variant change silently alters output, you want the diff to be a deliberate commit, not a config drift nobody noticed.
Truncation and request failure have opposite causes. You fail when the prompt plus max_tokens exceeds the context limit, and you truncate when max_tokens is smaller than the answer needs. Budget explicitly against the limit your provider documents.
def budgeted_max_tokens ( prompt_tokens, context_limit, cap = 4096 ): remaining = context_limit - prompt_tokens if remaining <= 0: raise ValueError ( f " Prompt exceeds the context limit by { - remaining } tokens " ) return min ( cap, remaining ) Enter fullscreen mode Exit fullscreen mode Count the prompt with the same tokenizer the provider uses, not a character heuristic. A rough estimate is fine for dashboards, but the request that crosses the line is the one you measured badly.
A retry loop that re-sends a request containing tool calls can fire a side effect twice. Rate-limit errors are safe to retry; a half-executed tool call is not.
import time def call_with_retry ( fn, attempts = 4, base = 1.0 ): for attempt in range ( attempts ): try: return fn () except Exception as exc: if attempt == attempts - 1 or " 429 " not in str ( exc ): raise time. sleep ( base * 2 ** attempt ) Enter fullscreen mode Exit fullscreen mode This retries only on the rate-limit error and re-raises everything else. Give tool calls their own idempotency key so a retry is a duplicate detection problem rather than a double-charge problem.
Kimi K3.1 Pro and K3.1 Pro Highspeed announces the two variants. I added the stream-termination checks, token budgeting, and retry handling that the announcement leaves out.
These write-ups are researched and published with no paywall, sponsor, or tracking. If one saved you an afternoon, a small tip keeps them coming.