Skip to content

Chapter 2 · Quick Start: Environment Setup and First Generation ​

Chapter goals:

  • Set up a runnable AI SDK Node.js project from scratch
  • Configure Vercel AI Gateway credentials and understand the equivalent custom provider configuration
  • Implement a terminal streaming chat with streamText
  • Add tools to the agent and enable stopWhen multi-step tool calling

2.1 Prerequisites ​

To follow this chapter's hands-on exercise, you need:

  • Node.js 22+ and pnpm installed locally;
  • A Vercel AI Gateway API key (register on the Vercel website to obtain);
  • Or: any OpenAI-compatible server's baseURL + apiKey (custom provider approach, see Section 2.4).

2.2 Create the Application and Install Dependencies ​

Create a directory with mkdir, enter it, and run pnpm init to generate package.json:

bash
mkdir my-ai-app
cd my-ai-app
pnpm init

Install AI SDK and other necessary dependencies:

bash
pnpm add ai zod dotenv
pnpm add -D @types/node tsx typescript
  • The ai package is the AI SDK itself;
  • zod is used to define type-safe schemas and pass them to the LLM;
  • dotenv reads environment variables (AI Gateway key or custom provider credentials);
  • The three -D dev dependencies are for running TypeScript code.

2.3 Configure Credentials ​

Create a .env file in the project root:

bash
touch .env

Edit .env and fill in your credentials:

env
# Method 1: Vercel AI Gateway
AI_GATEWAY_API_KEY=xxxxxxxxx

💡 AI SDK automatically reads the AI_GATEWAY_API_KEY environment variable to complete AI Gateway authentication.

If using a custom OpenAI-compatible provider, change to:

env
OPENAI_COMPATIBLE_BASE_URL=https://api.custom.com/v1
OPENAI_COMPATIBLE_API_KEY=yyyyyyyyy

2.4 First Streaming Chat Program ​

Create index.ts:

ts
import { ModelMessage, streamText, createGateway } from 'ai';
import 'dotenv/config';
import * as readline from 'node:readline/promises';

const gateway = createGateway({
  apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: gateway('openai/gpt-5'),
      messages,
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

The custom provider version only needs to swap the model construction (everything else is identical):

ts
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';

const myProvider = createOpenAICompatible({
  name: 'my-provider',
  baseURL: process.env.OPENAI_COMPATIBLE_BASE_URL ?? '',
  apiKey: process.env.OPENAI_COMPATIBLE_API_KEY ?? '',
});
// Then change the model to:
//   model: myProvider('gpt-4o-mini'),

Code walkthrough:

  1. Set up a readline interface to read input from the terminal, supporting CLI interactive sessions;
  2. Initialize the messages array to保存 conversation history, keeping context across multi-turn conversations;
  3. Inside the main function loop:
    • Capture user input into userInput;
    • Push the input as a user message into messages;
    • Call streamText imported from the ai package, passing model and messages;
    • Iterate result.textStream to print incremental text to the terminal in real-time;
    • Append the assistant's response to messages.

Run the application:

bash
pnpm tsx index.ts

After the terminal prompt appears, type a message and you will see the AI respond in real-time!

2.5 Adding Tools to the Agent ​

LLMs have strong generation capabilities, but struggle with discrete tasks (such as math calculations) or interactions with the external world (such as checking the weather) — this is where tools come in.

Tools are actions the LLM can call; their results are fed back to the LLM to participate in the next response. For example, when a user asks about the weather, an agent without tools can only give a vague answer based on training data; with a weather tool, it can provide real-time, location-specific information.

Modify index.ts to add a simple weather tool:

ts
import { ModelMessage, streamText, tool, createGateway } from 'ai';
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const gateway = createGateway({
  apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: gateway('openai/gpt-5'),
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

Key points in the updated code:

  1. Import the tool function from the ai package;
  2. Define a tools object containing the weather tool, which:
    • Uses description to help the agent understand when to use it;
    • Defines inputSchema with a Zod schema, declaring that a location string is required. The agent will try to extract this parameter from the conversation context; if unavailable, it asks the user;
    • Defines an execute async function that simulates fetching weather data (returning a random temperature here) — in production this would be replaced with a real third-party API call.

Try asking "What's the weather in New York?" and observe how the agent uses the new tool. Notice the assistant's text response is empty? This is because the agent generated a tool call rather than text this round. You can access them via the toolCalls and toolResults keys on the result:

ts
console.log(await result.toolCalls);
console.log(await result.toolResults);

2.6 Enabling Multi-Step Tool Calling ​

You may have noticed: although the tool result is visible, the agent did not use it to answer the original question — because once a tool call is generated, this round of generation is considered complete.

The solution is to enable multi-step tool calling with stopWhen: it automatically feeds the tool result back to the agent to trigger a new round of generation, until your defined stopping condition is met. In this example, we want the agent to use the weather tool's result to answer.

Continue modifying index.ts:

ts
import { ModelMessage, streamText, tool, isStepCount, createGateway } from 'ai';
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const gateway = createGateway({
  apiKey: process.env.AI_GATEWAY_API_KEY ?? '',
});

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: gateway('openai/gpt-5'),
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
      },
      stopWhen: isStepCount(5),
      onStepEnd: async ({ toolResults }) => {
        if (toolResults.length) {
          console.log(JSON.stringify(toolResults, null, 2));
        }
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

Two additions:

  1. stopWhen: isStepCount(5) allows a single generation to consume up to 5 "steps";
  2. The onStepEnd callback prints each step's toolResults, helping observe the agent's tool usage (so you can delete the two console.log calls from the previous example).

2.7 Adding a Second Tool ​

Add a Fahrenheit-to-Celsius conversion tool to experience multi-step collaboration:

ts
convertFahrenheitToCelsius: tool({
  description: 'Convert a temperature in fahrenheit to celsius',
  inputSchema: z.object({
    temperature: z
      .number()
      .describe('The temperature in fahrenheit to convert'),
  }),
  execute: async ({ temperature }) => {
    const celsius = Math.round((temperature - 32) * (5 / 9));
    return {
      celsius,
    };
  },
}),

After adding this to the tools object in the code above, ask "What's the weather in New York in celsius?" and you will see the full interaction chain:

  1. The agent calls the weather tool to query New York;
  2. The terminal prints the tool result;
  3. Then it calls the temperature conversion tool to convert Fahrenheit to Celsius;
  4. The agent synthesizes the information and answers in natural language with New York's temperature.

This multi-step approach lets the agent collect information and give more accurate, context-aware answers. You can create more complex tools to connect to real APIs, databases, or any external system, bridging the gap between the model's knowledge cutoff and the real-time world.

2.8 Chapter Summary ​

  • Project four-piece set: pnpm init → install ai/zod/dotenv → write .env → run with pnpm tsx index.ts;
  • streamText + iterating textStream is the standard pattern for streaming output; the messages array maintains multi-turn context;
  • Tool three elements: description, Zod inputSchema, execute async function;
  • By default generation ends after one tool call; stopWhen: isStepCount(n) lets tool results auto-feed back to trigger subsequent steps;
  • All examples' models can be constructed with either createGateway or createOpenAICompatible.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Which property of a tool helps the agent understand when to use it?

2. Why was the assistant's text response empty after the first addition of the weather tool?

3. What does stopWhen: isStepCount(5) do?

4. To switch to a custom OpenAI-compatible server, what content needs to change?

🛠️ Hands-On Exercises ​

  1. Complete the full setup from sections 2.2–2.4; run the terminal chat once with Gateway and once with the custom provider.
  2. Add a getCurrentTime tool (returns the current time string) to the agent; test the behavior difference when asking "What time is it?"
  3. Change isStepCount(5) to isStepCount(1) and ask about the weather again; observe and explain the phenomenon; then change back to 5 to verify recovery.

Next: Chapter 3 · Provider Management: AI Gateway and Custom Providers