writing/tutorial/2026/07
TutorialJul 17, 2026·30 min read

Kimi K3 TypeScript Integration: Build with the World's Largest Open Model

Learn to integrate Kimi K3 — Moonshot AI's 2.8T-parameter open-source flagship with a 1M-token context window — into TypeScript and Next.js apps. Covers streaming reasoning, tool calling, structured output, and vision.

Open-weight models have reached a tipping point. When Moonshot AI shipped Kimi K3 in July 2026 — 2.8 trillion total parameters, roughly 60-80 billion active per token via Mixture-of-Experts routing, a one-million-token context window — they did not just break a size record. They demonstrated that fully open models can match closed frontier systems on coding, mathematics, and long-context reasoning simultaneously.

For developers in the MENA region and anywhere outside the US, that matters. Kimi K3 is available today through Moonshot's OpenAI-compatible API, self-hostable on multi-GPU clusters, and covered by a permissive open-weights license. This tutorial walks you through integrating K3 into TypeScript step by step: basic completions, streaming reasoning tokens, tool calling, structured JSON output, and multimodal vision — finishing with a production-ready Next.js API route.

If you already completed the Kimi K2 tutorial, migration is a single model-name swap plus one new reasoning_effort parameter. We cover the migration checklist at the end.

Prerequisites

Before starting, ensure you have:

  • Node.js 20+ (node --version)
  • TypeScript 5.4+
  • A Moonshot AI account and API key from the platform console
  • Basic familiarity with async/await and OpenAI-compatible SDKs

No GPU required — all examples use Moonshot's hosted API.

What You'll Build

By the end of this tutorial you will have:

  1. A typed K3 client using the standard OpenAI SDK
  2. A streaming function that separates reasoning tokens from final answers
  3. A tool-calling agent loop with K3 calling your TypeScript functions
  4. A structured-output extractor that returns validated, typed JSON
  5. A vision helper for image analysis
  6. A production Next.js 15 API route wrapping K3 as a Server-Sent Events stream

Understanding Kimi K3

Before writing code, here is the mental model that shapes every integration decision.

MoE architecture. Kimi K3 has 2.8 trillion total parameters but activates only around 60-80 billion per token through Mixture-of-Experts routing. Hosting the model requires many A100/H100 GPUs, but per-token inference cost stays competitive because only a small fraction of parameters runs per forward pass.

Reasoning always on. Unlike K2, K3 does not let you disable the thinking phase. Every request goes through internal chain-of-thought. You control depth with reasoning_effort — currently only "max" is accepted; Moonshot will add "medium" and "low" in future releases.

Fixed sampling. Parameters like temperature, top_p, n, and penalties are locked by the model. Do not send them — they are silently ignored.

One-million-token context. The context window is 1 million tokens with automatic prompt caching. Feeding a 300,000-token codebase to a single request is practical at K3's pricing tier.

OpenAI-compatible API. Base URL https://api.moonshot.ai/v1 — identical to K2. You can also use https://api.moonshot.ai/anthropic for the Anthropic dialect (useful for Claude Code and Cline configs).

Step 1: Project Setup

mkdir kimi-k3-demo && cd kimi-k3-demo
npm init -y
npm install openai zod dotenv
npm install -D typescript @types/node tsx
npx tsc --init

Edit tsconfig.json to target modern ESM output:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "outDir": "dist",
    "esModuleInterop": true
  }
}

Create .env:

MOONSHOT_API_KEY=sk-your-key-here

And a typed client in src/client.ts:

import OpenAI from "openai";
import "dotenv/config";
 
if (!process.env.MOONSHOT_API_KEY) {
  throw new Error("MOONSHOT_API_KEY is not set in the environment.");
}
 
export const kimi = new OpenAI({
  apiKey: process.env.MOONSHOT_API_KEY,
  baseURL: "https://api.moonshot.ai/v1",
});
 
export const K3 = "kimi-k3" as const;

This is the only file you need to change when switching providers — your business logic stays untouched.

Step 2: Basic Chat Completion

K3 responses include a reasoning_content field alongside the standard content. Create src/basic.ts to retrieve both:

import { kimi, K3 } from "./client.js";
 
async function main() {
  const response = await kimi.chat.completions.create({
    model: K3,
    reasoning_effort: "max",   // K3-specific; currently the only accepted value
    messages: [
      {
        role: "user",
        content: "Explain Mixture-of-Experts routing in under 100 words.",
      },
    ],
    max_completion_tokens: 4096,
  } as any); // OpenAI SDK types do not yet include reasoning_effort
 
  const choice = response.choices[0];
 
  // reasoning_content lives on the message but is not in SDK types yet
  const reasoning = (choice.message as any).reasoning_content as string | null;
  const answer = choice.message.content;
 
  if (reasoning) {
    console.log("=== Reasoning ===");
    console.log(reasoning);
    console.log();
  }
 
  console.log("=== Answer ===");
  console.log(answer);
}
 
main().catch(console.error);

Run it:

npx tsx src/basic.ts

You will see K3's internal chain-of-thought printed before the final answer — a visible trace of how it reached the conclusion.

Step 3: Streaming with Reasoning Tokens

Streaming exposes reasoning and content as separate delta channels. This lets you show a "thinking…" indicator while the model reasons, then reveal the answer once it finalises.

Create src/stream.ts:

import { kimi, K3 } from "./client.js";
 
async function streamK3(prompt: string): Promise<void> {
  const stream = await kimi.chat.completions.create({
    model: K3,
    reasoning_effort: "max",
    messages: [{ role: "user", content: prompt }],
    stream: true,
    max_completion_tokens: 8192,
  } as any);
 
  let inReasoning = false;
 
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta as any;
 
    if (delta?.reasoning_content) {
      if (!inReasoning) {
        process.stdout.write("\n[thinking] ");
        inReasoning = true;
      }
      process.stdout.write(delta.reasoning_content);
    } else if (delta?.content) {
      if (inReasoning) {
        process.stdout.write("\n\n[answer] ");
        inReasoning = false;
      }
      process.stdout.write(delta.content);
    }
  }
 
  process.stdout.write("\n");
}
 
streamK3(
  "What are the trade-offs between dense and sparse transformer architectures?"
).catch(console.error);

The key insight: K3 emits reasoning_content deltas during its thinking phase, then switches to content deltas for the final response. The two streams never overlap.

Step 4: Tool Calling

K3 supports standard OpenAI-style function calling. The tool schema and response parsing are identical to what you would write for GPT-4o, making K3 a drop-in replacement in tool-calling pipelines.

Create src/tools.ts:

import OpenAI from "openai";
import { kimi, K3 } from "./client.js";
 
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "get_npm_downloads",
      description: "Fetch weekly download count for an npm package",
      parameters: {
        type: "object",
        properties: {
          package_name: {
            type: "string",
            description: "The npm package name, e.g. 'react'",
          },
        },
        required: ["package_name"],
      },
    },
  },
];
 
// Simulated tool — replace with a real fetch in production
async function get_npm_downloads(packageName: string): Promise<string> {
  const mockData: Record<string, number> = {
    react: 28_000_000,
    next: 8_500_000,
    openai: 4_200_000,
  };
  const count = mockData[packageName] ?? 500_000;
  return `${packageName} had ${count.toLocaleString()} downloads last week.`;
}
 
async function runAgent(question: string): Promise<void> {
  const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
    { role: "user", content: question },
  ];
 
  for (let i = 0; i < 5; i++) {
    const response = await kimi.chat.completions.create({
      model: K3,
      reasoning_effort: "max",
      messages,
      tools,
      tool_choice: "auto",
      max_completion_tokens: 4096,
    } as any);
 
    const choice = response.choices[0];
    messages.push(choice.message as any);
 
    if (choice.finish_reason === "stop") {
      console.log("Final answer:", choice.message.content);
      return;
    }
 
    if (choice.finish_reason === "tool_calls") {
      const toolCalls = choice.message.tool_calls ?? [];
      for (const call of toolCalls) {
        const args = JSON.parse(call.function.arguments) as { package_name: string };
        const result = await get_npm_downloads(args.package_name);
        console.log(
          `Tool called: ${call.function.name}(${args.package_name}) → ${result}`
        );
        messages.push({
          role: "tool",
          tool_call_id: call.id,
          content: result,
        });
      }
    }
  }
 
  console.log("Agent loop ended without a stop signal.");
}
 
runAgent(
  "Compare weekly npm download counts for react, next, and openai."
).catch(console.error);

Step 5: Structured Output

K3 supports JSON Schema structured output with strict: true. Combined with Zod for TypeScript type safety, this produces fully validated, typed responses with zero manual parsing risk.

Create src/structured.ts:

import { kimi, K3 } from "./client.js";
import { z } from "zod";
 
const codeReviewSchema = z.object({
  overall_score: z.number().min(1).max(10),
  issues: z.array(
    z.object({
      severity: z.enum(["critical", "warning", "suggestion"]),
      line: z.number().optional(),
      description: z.string(),
      fix: z.string(),
    })
  ),
  summary: z.string(),
});
 
type CodeReview = z.infer<typeof codeReviewSchema>;
 
async function reviewCode(code: string): Promise<CodeReview> {
  const response = await kimi.chat.completions.create({
    model: K3,
    reasoning_effort: "max",
    messages: [
      {
        role: "system",
        content: "You are a senior TypeScript code reviewer. Return structured JSON only.",
      },
      {
        role: "user",
        content: `Review this code:\n\n\`\`\`typescript\n${code}\n\`\`\``,
      },
    ],
    response_format: {
      type: "json_schema",
      json_schema: {
        name: "code_review",
        strict: true,
        schema: {
          type: "object",
          properties: {
            overall_score: { type: "number" },
            issues: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  severity: {
                    type: "string",
                    enum: ["critical", "warning", "suggestion"],
                  },
                  line: { type: "number" },
                  description: { type: "string" },
                  fix: { type: "string" },
                },
                required: ["severity", "description", "fix"],
              },
            },
            summary: { type: "string" },
          },
          required: ["overall_score", "issues", "summary"],
        },
      },
    },
    max_completion_tokens: 4096,
  } as any);
 
  const raw = response.choices[0].message.content ?? "{}";
  return codeReviewSchema.parse(JSON.parse(raw));
}
 
const sampleCode = `
async function fetchUser(id: string) {
  const res = await fetch('/api/users/' + id)
  const data = await res.json()
  return data.user
}
`;
 
reviewCode(sampleCode)
  .then((review) => {
    console.log(`Score: ${review.overall_score}/10`);
    review.issues.forEach((issue) => {
      console.log(`[${issue.severity}] ${issue.description}`);
      console.log(`  Fix: ${issue.fix}`);
    });
    console.log(`Summary: ${review.summary}`);
  })
  .catch(console.error);

The strict: true flag makes K3 guarantee the schema is followed — no additional properties, no missing required fields.

Step 6: Vision — Analysing Images

K3 accepts images as base64-encoded data URIs or via Moonshot's ms:// file reference system. For one-off analysis the base64 path is simplest.

Create src/vision.ts:

import { kimi, K3 } from "./client.js";
import { readFileSync } from "fs";
import path from "path";
 
async function analyseImage(imagePath: string, question: string): Promise<string> {
  const buffer = readFileSync(imagePath);
  const base64 = buffer.toString("base64");
  const ext = path.extname(imagePath).slice(1);
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
 
  const response = await kimi.chat.completions.create({
    model: K3,
    reasoning_effort: "max",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "image_url",
            image_url: { url: `data:${mimeType};base64,${base64}` },
          },
          { type: "text", text: question },
        ],
      },
    ],
    max_completion_tokens: 2048,
  } as any);
 
  return response.choices[0].message.content ?? "";
}
 
export { analyseImage };

Usage:

import { analyseImage } from "./vision.js";
 
const result = await analyseImage(
  "./screenshot.png",
  "Describe the UI and list any accessibility issues you notice."
);
console.log(result);

K3 supports JPEG, PNG, WebP, and GIF formats via base64. For large images or repeated uploads, use the Moonshot Files API to get an ms:// reference instead of re-uploading the bytes on every request.

Step 7: Next.js 15 API Route

Wrap K3 streaming in a Next.js App Router route for production use. The route forwards the dual-channel stream (reasoning + content) as Server-Sent Events.

app/api/kimi-k3/route.ts:

import { NextRequest } from "next/server";
import OpenAI from "openai";
 
const kimi = new OpenAI({
  apiKey: process.env.MOONSHOT_API_KEY!,
  baseURL: "https://api.moonshot.ai/v1",
});
 
export async function POST(request: NextRequest) {
  const { messages } = (await request.json()) as {
    messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
  };
 
  const stream = await kimi.chat.completions.create({
    model: "kimi-k3",
    reasoning_effort: "max",
    messages,
    stream: true,
    max_completion_tokens: 16384,
  } as any);
 
  const encoder = new TextEncoder();
 
  return new Response(
    new ReadableStream({
      async start(controller) {
        try {
          for await (const chunk of stream) {
            const delta = chunk.choices[0]?.delta as any;
            const data = {
              reasoning: delta?.reasoning_content ?? null,
              content: delta?.content ?? null,
            };
            controller.enqueue(
              encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
            );
          }
          controller.enqueue(encoder.encode("data: [DONE]\n\n"));
          controller.close();
        } catch (err) {
          controller.error(err);
        }
      },
    }),
    {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
      },
    }
  );
}

Client-side consumer:

async function streamK3Chat(
  userMessage: string,
  onChunk: (text: string, isReasoning: boolean) => void
) {
  const res = await fetch("/api/kimi-k3", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      messages: [{ role: "user", content: userMessage }],
    }),
  });
 
  const reader = res.body?.getReader();
  const decoder = new TextDecoder();
  if (!reader) return;
 
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
 
    const lines = decoder.decode(value).split("\n").filter((l) => l.startsWith("data: "));
    for (const line of lines) {
      const payload = line.slice(6);
      if (payload === "[DONE]") return;
      const parsed = JSON.parse(payload) as {
        reasoning: string | null;
        content: string | null;
      };
      if (parsed.reasoning) onChunk(parsed.reasoning, true);
      if (parsed.content) onChunk(parsed.content, false);
    }
  }
}

In your React component, pass separate state setters for reasoning and content to onChunk to render them in separate UI panels — a thinking collapsible and a final answer box, for example.

Migrating from Kimi K2

If you have existing code from the K2 tutorial, migration is minimal:

ChangeK2K3
Model IDkimi-k2kimi-k3
Thinking parameterthinking: { type: "enabled", budget_tokens: N }reasoning_effort: "max"
Context window128K tokens1M tokens
TemperatureConfigurableFixed — do not send
VisionNot supportedSupported via base64 / ms://
Reasoning stream fieldNot presentreasoning_content delta

The base URL (https://api.moonshot.ai/v1) and authentication header are unchanged.

Troubleshooting

"Invalid parameter: temperature" — K3 does not accept sampling parameters. Remove temperature, top_p, n, and penalty fields from your requests.

reasoning_content is null — Ensure you pass reasoning_effort: "max" and cast the request with as any until the OpenAI SDK types catch up to K3.

Context exceeds limit — K3 accepts up to 1 million tokens input, but max_completion_tokens defaults to 131,072. For very long output, increase it explicitly up to 1,048,576.

Streaming cuts off early — Complex reasoning tasks generate long chain-of-thought traces before the final answer. Raise max_completion_tokens if output appears truncated.

Structured output validation fails — Ensure your JSON Schema uses only types supported by K3's strict mode: object, array, string, number, boolean, and null. Avoid anyOf / oneOf at the top level.

Next Steps

  • Feed an entire repository to K3 using the 1M-token context window and ask for a holistic architecture review — something impossible with 128K models
  • Combine vision with PDF-to-image conversion to build document review pipelines
  • Try the Anthropic-compatible endpoint (https://api.moonshot.ai/anthropic) to use K3 inside Claude Code by setting ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic
  • Compare K3's reasoning depth with Claude Fable 5 on long-horizon coding tasks

Conclusion

Kimi K3 is the largest open-source model available as of July 2026, and its OpenAI-compatible API means you can integrate it into any TypeScript or Next.js project with minimal friction. The key additions over K2 are the reasoning_effort parameter that exposes built-in chain-of-thought, the separate reasoning_content streaming channel, native vision support, and a 1M-token context window that genuinely accommodates entire codebases in a single prompt. Start with the typed client above, add streaming for user-facing interfaces, and reach for structured output whenever you need machine-readable responses from an AI reasoning system.