Kimi API Streaming: SSE, Usage & Tool Deltas

Kimi API streaming returns a sequence of Server-Sent Events instead of making the client wait for one complete JSON response. In our frozen August 7, 2026 run, one kimi-k3 text stream returned the exact target, complete usage and [DONE]. A second request that forced one specific function was rejected with HTTP 400 before an SSE stream began. We retained that failure once and did not change the request or retry it.

Direct result: Text streaming passed. The new literal tool-delta capture did not run because the API rejected tool_choice: specified while thinking was enabled. Older K3 evidence contains parsed tool-call deltas, but it is reported separately because it is not a literal data: transcript.

The result at a glance

Frozen caseTransport resultScored outputTimingCost record
STREAM-TEXT-01HTTP 200; text/event-stream; 27 JSON data: records plus [DONE]; zero parse errorsExact STREAM-ALPHA-731; finish_reason: stop; no tool call; PassFirst SSE 2,524.462 ms; first answer 2,954.674 ms; wall 2,962.322 ms$0.000978 usage-derived
STREAM-TOOL-01HTTP 400 JSON error before SSE; no stream or deltasNot scored; no tool executedFailure returned before an SSE timing record existedNo usage returned; $0.005547 held only as a conservative cap reserve, not a billed-cost claim

This is not a two-case reliability percentage. The cases tested different response paths, each once. The result supports “the frozen text stream completed once” and “the frozen specific-tool request failed before streaming once.”

What the official Kimi documentation says

Kimi’s streaming guide uses stream: true, sends JSON in SSE data: records and uses data: [DONE] as the transport completion marker. Usage is returned at the end of the stream when requested.

For tool calls, Kimi’s tool-calling guide shows that a client must assemble function-name and argument fragments by tool-call index. The complete assistant tool-call message must be preserved before the application returns any tool result.

Those are vendor-documented behaviors. The timings, record counts, exact text and HTTP 400 below are our independent observations from the retained run.

How the successful text stream arrived

The request used the official Global endpoint, model ID kimi-k3, reasoning_effort: low, max_completion_tokens: 96, streaming enabled and usage requested. The synthetic prompt required exactly STREAM-ALPHA-731. There was one attempt and no automatic or corrective retry.

The redacted transcript ended like this:

data: {..."content":"STREAM"...}
data: {..."content":"-"...}
data: {..."content":"ALPHA"...}
data: {..."content":"-"...}
data: {..."content":"731"...}
data: {..."finish_reason":"stop","usage":{...}}
data: {..."choices":[],"usage":{...}}
data: [DONE]

The assembled answer matched the oracle exactly. The final usage object reported 131 prompt tokens, 39 completion tokens and 170 total tokens. Nineteen completion tokens were attributed to reasoning. The retained public transcript removes the reasoning text while preserving its character count, final answer, model, usage, ordering and timing.

Usage appeared in both the terminal choice record and the following empty-choices record in this one capture. A parser should therefore keep the latest complete usage object rather than assuming that only one exact event shape can carry it.

What “redacted SSE transcript” means here

Our file preserves decoded data: record order, blank separators and [DONE], but it is not an untouched packet capture. JSON inside each line was normalized while the API key and reasoning text were removed before storage. TCP segmentation, HTTP chunk boundaries, provider-side queue time and server clock time are outside the evidence boundary.

Per-event elapsed times were recorded locally after a complete line was decoded. They are useful for reconstructing this client run; they are not provider service-level measurements.

Why the specific tool stream is a retained failure

The second frozen request supplied one synthetic, non-operative inventory_lookup function and forced that exact function through a specific tool_choice. The API returned:

{
  "error": {
    "message": "tool_choice 'specified' is incompatible with thinking enabled",
    "type": "invalid_request_error"
  }
}

Because the response was HTTP 400 rather than text/event-stream, there were no new tool deltas, no [DONE] marker and no usage object to score. The protocol prohibited changing a failed case and presenting the replacement as if it were the original result, so no retry was performed.

The spend ledger initially marked the cost unresolved because the failure returned no usage. For cap safety, it later reserved a transport-inclusive worst case of $0.005547. That number is conservative budget accounting only. It is not evidence that Kimi billed $0.005547, and the page does not convert a zero immediate balance change into a zero-cost claim.

For request failures like this, use the Kimi API Error Decoder and compare the exact current model and tool-choice rules before making a deliberate new test. Do not blindly retry a permanent 400 response.

The older 15-call K3 evidence is separate

Our earlier Kimi K3 API pilot completed 15 streamed requests. Its public response files contain 2,130 parsed and sanitized event objects with zero recorded parse-error objects, including eight synthetic tool-selection streams.

One older TC01 tool stream split the argument string across these parsed deltas:

{"sku":"
ORB
-
17
"
}

Concatenating them in order produced {"sku":"ORB-17"}. That is useful evidence for argument accumulation. It is not a replacement for the failed August 7 literal tool transcript because the older runner stored parsed event objects rather than literal data: lines, did not retain [DONE] as an event object and did not timestamp every event.

The Kimi API Tool Calling guide remains the owner of function definitions, argument validation, execution safety and the older 8/8 tool-selection result. This page owns SSE framing, stream assembly, usage placement and transport limits.

Inspect a sanitized Kimi SSE stream locally

Private browser tool

Inspect a sanitized Kimi SSE stream

Paste literal data records or a JSON array of sanitized events. The inspector reconstructs content, tool fragments, usage and completion state without sending or saving input.

Redact first: Inputs containing an API-key-shaped value or Authorization header are blocked.

Only paste a publication-safe capture. Reasoning text should already be replaced with redaction markers.

The SSE Inspector is designed for sanitized evidence, not credentials. Paste a redacted data: transcript or event JSONL to inspect:

  • JSON event count and parse failures;
  • whether [DONE] is present;
  • model and finish reason;
  • usage objects and where they appeared;
  • assembled visible content;
  • tool-call IDs, names and concatenated arguments by index; and
  • first-event, first-answer and wall timings when elapsed-time fields are present.

The tool runs entirely in the visitor’s browser, makes no Kimi request and stores no pasted transcript in cookies, local storage, session storage or WordPress. It has no API-key field. Deterministic parsing, synthetic secret-shaped input blocking, reset, block-specific assets and programmatic labels passed retained Draft and public checks. The public page and Inspector were also rechecked at 1280 and 390 CSS pixels with no page or tool overflow.

Do not paste an unredacted production stream into a website tool. Remove API keys, authorization headers, personal data, uploaded-document content and private reasoning fields first.

A safe streaming parser pattern

Authentication and the first request belong in our Kimi API setup guide. The important streaming boundary is to buffer across network chunks, process complete SSE lines, retain the final usage object and delay argument parsing until the tool call is complete.

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let doneSeen = false;
let content = "";
let usage = null;
const toolCalls = [];

function mergeToolDelta(delta) {
  const index = Number.isInteger(delta.index) ? delta.index : 0;
  toolCalls[index] ??= {
    id: "",
    type: "function",
    function: { name: "", arguments: "" },
  };

  const target = toolCalls[index];
  if (typeof delta.id === "string") target.id += delta.id;
  if (typeof delta.function?.name === "string") {
    target.function.name += delta.function.name;
  }
  if (typeof delta.function?.arguments === "string") {
    target.function.arguments += delta.function.arguments;
  }
}

while (true) {
  const { value, done } = await reader.read();
  buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });

  const lines = buffer.split(/\r?\n/);
  buffer = lines.pop() ?? "";

  for (const line of lines) {
    if (!line.startsWith("data:")) continue;
    const data = line.slice(5).trimStart();

    if (data === "[DONE]") {
      doneSeen = true;
      continue;
    }

    const event = JSON.parse(data);
    if (event.usage) usage = event.usage;

    const choice = event.choices?.[0];
    if (typeof choice?.delta?.content === "string") {
      content += choice.delta.content;
    }
    for (const delta of choice?.delta?.tool_calls ?? []) {
      mergeToolDelta(delta);
    }
  }

  if (done) break;
}

if (!doneSeen) throw new Error("Stream ended without [DONE]");

for (const call of toolCalls.filter(Boolean)) {
  call.parsedArguments = JSON.parse(call.function.arguments);
}

Production code also needs cancellation, timeouts, maximum response size, redacted logging and explicit handling for a non-200 JSON error before it touches the SSE parser.

Common SSE mistakes

SymptomLikely implementation problemSafer check
JSON fails randomlyA network chunk was treated as one complete eventBuffer decoded text and process complete SSE lines only
Tool arguments fail to parseEach delta fragment was parsed independentlyConcatenate by tool-call index, then parse once complete
A tool call overwrites anotherThe client uses one global accumulatorKeep a separate accumulator for each tool-call index
Usage is missingThe client inspects only content-bearing choicesRetain usage wherever it appears near the stream end
Partial output is treated as completeConnection close is treated as successRequire the intended finish state and [DONE]; retain an explicit incomplete status otherwise
Duplicate downstream actionA disconnected tool-enabled request is retried blindlyDetermine whether execution occurred and enforce idempotency before retrying
Secrets enter logsThe complete request or transport object is serializedLog a redacted case ID, status, model, timing and bounded usage fields only

Download the evidence bundle

Download the Kimi API Streaming Evidence Bundle. It contains the frozen protocol and fixtures, both sanitized requests, the successful response and score, the retained HTTP 400 failure, the redacted SSE transcript, timestamped event JSONL, the track-only spend ledger, parser code, offline tests and a file manifest.

Package SHA-256: 663C0BD968C5F1C8B74D06D86C2FE75DD66CE37328DE256846C50FEBA2D08FAF Package size: 28,628 bytes Files: 24, including the manifest

The manifest independently rechecked 23 covered files with zero hash or byte-count mismatch. Both dependency-free offline SSE tests passed. The ZIP contains no backslash entry, no detected API-key pattern, no private account preflight and no returned reasoning text. The older 15-call K3 pack remains a separate download on the K3 test page.

Methodology and limits

  • Protocol version 1.0 was frozen before the live requests.
  • Run ID: kimi-api-reliability-lab-20260807-v1.
  • Official Global endpoint: https://api.moonshot.ai/v1.
  • Model requested and returned for the completed case: kimi-k3.
  • Execution was sequential, with one attempt per case and zero retries.
  • Inputs and the function were synthetic; no production system or side effect was involved.
  • The text sample was deliberately short and cannot establish behavior for long outputs, reconnects, parallel choices or sustained load.
  • The current literal run did not capture a successful tool stream.
  • The local client clock, network path, account, region and service load were not controlled.
  • Redaction preserved semantic event order but changed stored JSON text and removed reasoning content.
  • Costs are usage-derived under the frozen dated rate snapshot. They are not tax invoices.

Our broader evidence labels, preregistration rules and stop conditions are in How We Test Kimi AI. Corrections can be submitted through Sources & Corrections.

Frequently asked questions

Did Kimi K3 stream the exact text successfully?

Yes, once. The one frozen text request returned HTTP 200 SSE, exact STREAM-ALPHA-731, usage, zero parse errors and [DONE]. A single pass is not a reliability rate.

Did the new tool-delta stream pass?

No. The API returned HTTP 400 before SSE because the specific tool choice was incompatible with thinking enabled. We did not alter and rerun it. Older parsed K3 events demonstrate fragmented tool arguments, but they remain historical evidence with a different capture boundary.

Is the first SSE event the first visible answer token?

No. In the text case the first SSE event arrived at 2,524.462 ms, while the first visible answer delta arrived at 2,954.674 ms. Reasoning events can precede visible answer text.

Can I parse every data: line as a complete answer?

No. Each line carries a partial event. Content and tool arguments must be assembled in order, and a tool argument string should be parsed only after all of its fragments have arrived.

Does the Inspector send my transcript to Kimi?

No. The active KI AI Tools 1.2.0 Inspector is browser-local, does not call Kimi and does not persist input. Its deterministic parse, secret-blocking and reset paths passed retained Draft and public QA. Never paste an API key or unredacted production data into it.

Official sources

For general API setup, use the Kimi API guide. For function execution and validation, use Kimi API Tool Calling. For the broader 15-call model pilot, use the Kimi K3 Test.