Code Mode lets the model write one program that calls several tools. One sandbox run replaces one round trip per tool.
The model gets one tool, execute_typescript, and you get one result back.
You need a working chat first. If you do not have one, start with the Quick Start. This setup takes about 5 minutes.
npm i @tanstack/ai @tanstack/ai-code-mode @tanstack/ai-isolate-node @tanstack/ai-openai zodpnpm add @tanstack/ai @tanstack/ai-code-mode @tanstack/ai-isolate-node @tanstack/ai-openai zodyarn add @tanstack/ai @tanstack/ai-code-mode @tanstack/ai-isolate-node @tanstack/ai-openai zodbun add @tanstack/ai @tanstack/ai-code-mode @tanstack/ai-isolate-node @tanstack/ai-openai zodThe Node driver needs Node.js 22 or later. On Bun, the edge, or Cloudflare, pick a different driver from Isolate Drivers.
// app/api/chat/route.ts (Next.js). Any route that takes a Request works.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
toolDefinition,
} from "@tanstack/ai";
import { createCodeMode } from "@tanstack/ai-code-mode";
import { createNodeIsolateDriver } from "@tanstack/ai-isolate-node";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const temperatures: Record<string, number> = {
Tokyo: 18,
Paris: 12,
"New York City": 9,
};
const fetchWeather = toolDefinition({
name: "fetchWeather",
description: "Get current weather for a city",
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({
temperature: z.number(),
condition: z.string(),
}),
}).server(async ({ location }) => ({
temperature: temperatures[location] ?? 20,
condition: "sunny",
}));
const { tools, systemPrompt } = createCodeMode({
driver: createNodeIsolateDriver(),
tools: [fetchWeather],
});
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
systemPrompts: ["You are a helpful weather assistant.", systemPrompt],
tools,
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}The model calls execute_typescript one time, with a program like this:
const cities = ["Tokyo", "Paris", "New York City"];
const results = await Promise.all(
cities.map((city) => external_fetchWeather({ location: city }))
);
const warmest = results.reduce((prev, curr) =>
curr.temperature > prev.temperature ? curr : prev
);
return {
comparison: results.map((r, i) => ({ city: cities[i], ...r })),
warmest: cities[results.indexOf(warmest)],
};The three weather calls run in parallel inside the sandbox. The model gets one result and writes its answer from it.
The Node, QuickJS, QuickJS Bun, and Cloudflare drivers have no access to the host file system, network, or process. The Daytona and E2B drivers run the code in a full remote Linux sandbox, with its own file system, network, and processes.
When a user stops a chat run, a tool call that the sandbox started keeps going, unless the tool listens for the abort. Each external_* call gets the abortSignal of the chat run in its second argument. Pass it to the slow work:
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const searchDocs = toolDefinition({
name: "searchDocs",
description: "Search the docs",
inputSchema: z.object({ query: z.string() }),
}).server(async ({ query }, ctx) => {
const res = await fetch(
`https://search.example/v1?q=${encodeURIComponent(query)}`,
{ signal: ctx?.abortSignal },
);
return res.json();
});MCP tools from @tanstack/ai-mcp already stop their request when the run aborts.
A tool that runs inside Code Mode gets these fields:
The tool does not get toolCallId or inputResponse. These fields belong to the execute_typescript call.
| Option | Type | Description |
|---|---|---|
| driver | IsolateDriver | Required. The sandbox that runs the code. See Isolate Drivers. |
| tools | Array<ServerTool> | Required, at least one. Each tool needs a .server() implementation. Each one becomes an external_* function. |
| timeout | number | Time limit for one execution, in ms. Default: 30000. |
| memoryLimit | number | Memory limit in MB. Default: 128. The Node, QuickJS, QuickJS Bun, and E2B drivers use it. |
| getSnippetBindings | () => Promise<Record<string, ToolBinding>> | Extra bindings that load at each execution. Snippets use it. |
| lazyToolsConfig | LazyToolsConfig | How much of each lazy tool the catalog shows. See Lazy Tools. |
| onSecretParameter | 'warn' | 'throw' | 'ignore' | (info) => void | What to do when a tool parameter name looks like a secret. Default: 'warn'. |
| transpile | (code: string) => string | Promise<string> | Replaces the step that strips TypeScript. The default uses sucrase and is safe for browsers and the edge. |
| debug | DebugOption | Debug logging, same shape as chat({ debug }). Failed executions log under errors (on by default). Successful ones log under tools. Pass false to silence it. See Debug Logging. |
createCodeMode returns:
A custom transpile function must accept top-level return and await. The snippet helpers ignore transpile and always use the default.
interface CodeModeToolResult {
success: boolean;
result?: unknown; // Return value of the code
logs?: Array<string>; // Captured console output
error?: {
message: string;
name?: string;
line?: number;
stack?: string;
};
}createCodeMode calls two functions. When you need only the tool or only the prompt, use them:
import {
createCodeModeTool,
createCodeModeSystemPrompt,
} from "@tanstack/ai-code-mode";
const tool = createCodeModeTool(config);
const prompt = createCodeModeSystemPrompt(config);@tanstack/ai-code-mode also exports these helpers for custom pipelines:
Code Mode sends these custom events while it runs. Each event carries the toolCallId of its execute_typescript call. Showing Code Mode in the UI renders them.
| Event | When | Payload |
|---|---|---|
| code_mode:execution_started | The execution starts | { timestamp, codeLength } |
| code_mode:console | For each console.log, error, warn, or info call | { level, message, timestamp } |
| code_mode:external_call | Before an external_* function runs | { function, args, timestamp } |
| code_mode:external_result | After an external_* call succeeds | { function, result, duration } |
| code_mode:external_error | When an external_* call fails | { function, error, duration } |
| code_mode:execution_finished | The execution ends, with success or failure | { timestamp, durationMs, phase, success, logCount, error } |
Code Mode needs a model that writes valid TypeScript and calls the external_* functions correctly. Many small or older models get the calling convention wrong, even with a clear system prompt.
We run one multi-step benchmark against a gold reference. The task joins three tables, finds customers who bought from every product category, and sums the spend per category. The harness is in packages/ai-code-mode/models-eval/.
| Rank | Model | Stars | Acc | Comp | TS | CME | Latency | Tokens |
|---|---|---|---|---|---|---|---|---|
| 1 | grok:grok-4-1-fast-non-reasoning | ★★★ | 10 | 9 | 6 | 10 | 7.0s | n/a |
| 2 | ollama:gpt-oss:20b | ★★★ | 10 | 8 | 6 | 5 | 45.1s | 23.6k |
| 3 | anthropic:claude-haiku-4-5 | ★★★ | 10 | 10 | 7 | 10 | 9.4s | 8.5k |
| 4 | gemini:gemini-2.5-flash | ★★★ | 10 | 7 | 5 | 9 | 7.3s | 6.9k |
| 5 | ollama:nemotron-cascade-2 | ★★★ | 10 | 9 | 5 | 5 | 60.4s | 11.7k |
| 6 | openai:gpt-4o-mini | ★★☆ | 10 | 8 | 8 | 10 | 19.2s | 8.7k |
| 7 | ollama:gemma4:31b | ★★☆ | 10 | 8 | 4 | 5 | 264.2s | 6.4k |
The columns:
What the results show:
To run the benchmark yourself:
cd packages/ai-code-mode/models-eval
pnpm install
pnpm eval # full suite (needs cloud API keys + Anthropic for judging)
pnpm eval -- --ollama-only # local models only
pnpm eval -- --no-judge # skip Anthropic-based judging