> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://humanloop.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://humanloop.com/docs/_mcp/server.

> Add logging to your Vercel AI SDK application with Humanloop.

Add Humanloop observability to a chat agent by calling the tool built with Vercel AI SDK. It builds on the AI SDK's [Next.js example](https://sdk.vercel.ai/docs/getting-started/nextjs-app-router).

> **Tip**
>
> Looking for Node.js? See the [guide here](/docs/v5/integrations/vercel-ai-sdk/agent-nodejs-observability).

## Prerequisites

#### Account setup

Create a Humanloop Account

1. [Create an account](https://app.humanloop.com/signup) or [log in](https://app.humanloop.com/login) to Humanloop

2. Get a Humanloop API key from [Organization Settings](https://app.humanloop.com/account/api-keys).

Add an OpenAI API Key

If you're the first person in your organization, you'll need to add an API key to a model provider.

1. Go to OpenAI and [grab an API key](https://platform.openai.com/api-keys).
2. In Humanloop [Organization Settings](https://app.humanloop.com/account/api-keys) set up OpenAI as a model provider.

> **Info**
>
> Using the Prompt Editor will use your OpenAI credits in the same way that the
> OpenAI playground does. Keep your API keys for Humanloop and the model
> providers private.

#### Create project

Create a new Next.js project. This command will create a new directory named `my-ai-app`
and set up a basic Next.js application inside of it.

**`npm`**

```bash title="npm"
npx create-next-app@latest my-ai-app
```

**`pnpm`**

```bash title="pnpm"
pnpm create next-app@latest my-ai-app
```

**`yarn`**

```bash title="yarn"
yarn create next-app@latest my-ai-app
```

Navigate to the newly created directory:

```bash
cd my-ai-app
```

#### Install dependencies

To follow this guide, you'll also need Node.js 18+ installed on your machine.

Install `ai`, '@ai-sdk/react', and `@ai-sdk/openai`, along with other necessary dependencies.

**`npm`**

```bash title="npm"
npm install ai @ai-sdk/react @ai-sdk/openai zod
npm install -D @types/node tsx typescript
```

**`pnpm`**

```bash title="pnpm"
pnpm add ai @ai-sdk/react @ai-sdk/openai zod
pnpm add -D @types/node tsx typescript
```

**`yarn`**

```bash title="yarn"
yarn add ai @ai-sdk/react @ai-sdk/openai zod
yarn add -D @types/node tsx typescript
```

#### Configure API keys

Add a `.env.local` file to your project with your Humanloop and OpenAI API keys.

```bash
touch .env.local
```

#### .env.local

```
HUMANLOOP_API_KEY=<YOUR_HUMANLOOP_KEY>
OPENAI_API_KEY=<YOUR_OPENAI_KEY>
```

## Full code

If you'd like to immediately try out the full example, you can copy and paste the code below and run the app.

#### Setup

**`npm`**

```bash title="npm" wordWrap
npm install ai @ai-sdk/react @ai-sdk/openai zod
npm install -D @types/node tsx typescript
npm install @vercel/otel @opentelemetry/api @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

**`pnpm`**

```bash title="pnpm" wordWrap
pnpm add ai @ai-sdk/react @ai-sdk/openai zod
pnpm add -D @types/node tsx typescript
pnpm add @vercel/otel @opentelemetry/api @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

**`yarn`**

```bash title="yarn" wordWrap
yarn add ai @ai-sdk/react @ai-sdk/openai zod
yarn add -D @types/node tsx typescript
yarn add @vercel/otel @opentelemetry/api @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

**`.env.local`**

```plaintext title=".env.local" wordWrap highlight={4-6}
HUMANLOOP_API_KEY=<YOUR_HUMANLOOP_KEY>
OPENAI_API_KEY=<YOUR_OPENAI_KEY>

OTEL_EXPORTER_OTLP_ENDPOINT=https://api.humanloop.com/v5/import/otel
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS="X-API-KEY=<YOUR_HUMANLOOP_KEY>" # Humanloop API key
```

**`instrumentation.ts`**

```typescript title="instrumentation.ts"
import { registerOTel } from "@vercel/otel";

export function register() {
  registerOTel({
    serviceName: "humanloop-ai-sdk-agent",
  });
}
```

#### Backend Route Handler

**`app/api/chat/route.ts`**

```typescript title="app/api/chat/route.ts" maxLines=20
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
    experimental_telemetry: {
      isEnabled: true,
      metadata: {
        "humanloop.directoryPath": "path/to/directory",
      },
    },
    tools: {
      weather: tool({
        description: "Get the weather in a location (fahrenheit)",
        parameters: 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,
          };
        },
      }),
    },
  });

  return result.toDataStreamResponse();
}
```

#### UI

**`app/page.tsx`**

```typescript title="app/page.tsx" maxLines=50
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    maxSteps: 5,
  });

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(m => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.parts.map((p, idx) =>
            p.type === "tool-invocation" || p.type === "source" ? (
              <pre key={idx}>{JSON.stringify(p, null, 2)}</pre>
            ) : p.type === "text" ? (
              <p key={idx}>{p.text}</p>
            ) : (
              <p key={idx}>{p.reasoning}</p>
            )
          )}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
}
```

**`npm`**

```bash title="npm"
npm run dev
```

**`pnpm`**

```bash title="pnpm"
pnpm run dev
```

**`yarn`**

```bash title="yarn"
yarn dev
```

## Create the agent

### Create a Route Handler

We start with a backend route that handles a chat request and streams back a response from a model. This model can call a function to get the weather in a given location.

**`app/api/chat/route.ts`**

```typescript title="app/api/chat/route.ts" maxLines=50
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
    tools: {
      weather: tool({
        description: "Get the weather in a location (fahrenheit)",
        parameters: 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,
          };
        },
      }),
    },
  });

  return result.toDataStreamResponse();
}
```

### Wire up the UI

Now that you have a Route Handler that can query an LLM, it's time to setup your frontend. The AI SDK's [UI](https://sdk.vercel.ai/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one hook, `useChat`.
Update your root page to show a chat interface and provide a user message input.

> **Note**
>
> The `maxSteps` prop allows the model to take multiple "steps" for a given
> generation, using tool calls to refine its response.

**`app/page.tsx`**

```typescript title="app/page.tsx" maxLines=50
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    maxSteps: 5,
  });

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map(m => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.parts.map((p, idx) =>
            p.type === "tool-invocation" || p.type === "source" ? (
              <pre key={idx}>{JSON.stringify(p, null, 2)}</pre>
            ) : p.type === "text" ? (
              <p key={idx}>{p.text}</p>
            ) : (
              <p key={idx}>{p.reasoning}</p>
            )
          )}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
}
```

### Run the agent

Start your app and give the agent a try.

**`npm`**

```bash title="npm"
npm run dev
```

**`pnpm`**

```bash title="pnpm"
pnpm run dev
```

**`yarn`**

```bash title="yarn"
yarn dev
```

![](/docs/_fern-img/8bad44bcfad07c4da07d99c3022ed88b4171655d5438dacc79ea9d6890b6cad3.webp)

## Log to Humanloop

The agent works and is capable of function calling. However, we rely on inputs and outputs to reason about the behavior.

Humanloop logging allows you to observe the steps taken by the agent, which we will demonstrate below.

We'll use Vercel AI SDK's built-in OpenTelemetry tracing to log to Humanloop.

### Set up OpenTelemetry

Install dependencies.

**`npm`**

```bash wordWrap title="npm"
npm install @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

**`pnpm`**

```bash wordWrap title="pnpm"
pnpm add @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

**`yarn`**

```bash wordWrap title="yarn"
yarn add @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
```

Create a file called `instrumentation.ts` in your root or /src directory and add the following code:

**`instrumentation.ts`**

```typescript title="instrumentation.ts"
import { registerOTel } from "@vercel/otel";

export function register() {
  registerOTel({
    serviceName: "humanloop-ai-sdk-agent",
  });
}
```

Configure the [OpenTelemetry exporter](https://opentelemetry.io/docs/specs/otel/protocol/exporter/) to forward logs to Humanloop.

**`.env.local`**

```plaintext highlight={4-6} title=".env.local" wordWrap
HUMANLOOP_API_KEY=<YOUR_HUMANLOOP_KEY>
OPENAI_API_KEY=<YOUR_OPENAI_KEY>

OTEL_EXPORTER_OTLP_ENDPOINT=https://api.humanloop.com/v5/import/otel
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_HEADERS="X-API-KEY=<YOUR_HUMANLOOP_KEY>" # Humanloop API key
```

### Trace AI SDK calls

The telemetry metadata associates Logs with your Files on Humanloop.

We will use a Humanloop [Prompt](/docs/explanation/prompts) to log LLM calls, and a Humanloop [Flow](/docs/explanation/flows) to group related generation calls into a trace.

The `humanloop.directoryPath` specifies the path to a Directory where your Files and Logs will be located.

**`app/api/chat/route.ts`**

```typescript title="app/api/chat/route.ts" highlight={13-19} maxLines=50
import { openai } from "@ai-sdk/openai";
import { streamText, tool } from "ai";
import { z } from "zod";

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
    experimental_telemetry: {
      isEnabled: true,
      metadata: {
        "humanloop.directoryPath": "path/to/directory",
      },
    },
    tools: {
      weather: tool({
        description: "Get the weather in a location (fahrenheit)",
        parameters: 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,
          };
        },
      }),
    },
  });

  return result.toDataStreamResponse();
}
```

Restart your app, and have a conversation with the agent.

### Explore logs on Humanloop

Now you can explore your logs on the [Humanloop platform](https://app.humanloop.com), and see the steps taken by the agent during your conversation.

![](/docs/_fern-img/d273503c2ee913947a83dbf84bb661674b4b8445b750e8af0777b31037638859.webp)

## Debugging

If you run into any issues, add OpenTelemetry debug logging to ensure the Exporter is working correctly.

**`npm`**

```bash title="npm"
npm install @opentelemetry/api
```

**`pnpm`**

```bash title="pnpm"
pnpm add @opentelemetry/api
```

**`yarn`**

```bash title="yarn"
yarn add @opentelemetry/api
```

**`instrumentation.ts`**

```typescript title="instrumentation.ts"
import { registerOTel } from "@vercel/otel";
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";

diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
export function register() {
  registerOTel({
    serviceName: "humanloop-ai-sdk-agent",
  });
}
```

## Next steps

Logging is the first step to observing your AI product. Read these guides to learn more about evals on Humanloop:

* Add [monitoring Evaluators](/docs/v5/guides/observability/monitoring) to evaluate Logs as they're made against a File.

* See evals in action in our [tutorial on evaluating an agent](/docs/v5/tutorials/agent-evaluation).