Text generation · Capture thinking content from a completion1 / 8
  1. 02
  2. 03
  3. 04
  4. 05
  5. 06
  6. 07
  7. 08

Capture thinking content from a completion

Example on GitHub(packages/sdk/examples/completion-events.ts)

Chapter 2 starts with one specific feature of the completion() surface: thinking content.

Some models emit a <think>...</think> reasoning block before the final answer. The reasoning arrives in the same token stream that produces the response. Without capture, that reasoning either ends up inside the response text, mixed into the user-visible answer, or the chat-template layer strips it and you can't recover it from the SDK surface.

captureThinking: true tells the SDK to parse <think> blocks out of the raw model output and surface them as their own event type, separate from the final answer. The call would look like:

const result = completion({
  modelId,
  history: [{ role: "user", content: "Why is the sky blue?" }],
  stream: true,
  captureThinking: true,
});

That single boolean changes the shape of the events stream. A model that emits <think>The user is asking about Rayleigh scattering.</think>The sky appears blue because... fires four event types: thinkingDelta for each reasoning token (before the answer), then contentDelta for each answer token (after the reasoning completes), then completionStats with throughput numbers, then completionDone with stopReason: "eos".

contentDelta goes to stdout, thinkingDelta routes to stderr with a [think] prefix so it doesn't mix with the response. Both event types carry the same text field. The event loop for this kind of call would look like:

for await (const event of result.events) {
  switch (event.type) {
    case "contentDelta":
      process.stdout.write(event.text);
      break;
    case "thinkingDelta":
      process.stderr.write(`[think] ${event.text}`);
      break;
  }
}

After the loop, the joined reasoning sits in final.thinkingText. Reading the full reasoning block at once is one await away:

const final = await result.final;
if (final.thinkingText) {
  console.log(`\n▸ Thinking: ${final.thinkingText}`);
}

For convenience, the aggregated final.thinkingText joins every reasoning token into one string. We read it after the events loop exits.

Note: process.stdout is reserved for the user-visible answer across this curriculum. Reasoning is part of the model's behaviour but not the answer, so it goes to process.stderr. On small models the SDK surfaces thinking as a stream of short tokens (single words and punctuation), so a per-token process.stderr.write in the event loop duplicates the label on every fragment. Read final.thinkingText after the events loop and emit one labeled block instead. If you'd rather show both interleaved to the user, write both to stdout with different prefixes.

Put it to the test

  1. Set captureThinking: true in the completion() options.
  2. Handle thinkingDelta by writing event.text to process.stderr with a [think] prefix.
  3. After the events loop, await result.final and log final.thinkingText with a ▸ Thinking: prefix.
index.ts

$ Run your code to see results

$