> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# AI SDK

> What the quickstart left out for a Vercel AI SDK loop: per-request pack builds, dynamic-tool message parts, and the one Next.js config line.

The [quickstart](/existing-agent/quickstart) wires an AI SDK loop end to end. This page is the rest.

## Build the pack per request

`vendoTools` takes the caller and returns a Promise, so it belongs inside the handler rather than at module scope.

```ts app/api/chat/route.ts focus={8,13-14} theme={null}
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { vendoTools } from "@vendoai/vendo/ai-sdk";
import { vendoModel } from "@vendoai/vendo/server";
import { resolvePrincipal, vendo } from "@/lib/vendo";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const caller = await resolvePrincipal(req);
  if (!caller) return new Response("Unauthorized", { status: 401 });
  return streamText({
    model: vendoModel(),
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools: { ...(await vendoTools(vendo, { principal: caller })) },
  }).toUIMessageStreamResponse();
}
```

`stopWhen` is not optional in practice. The AI SDK stops after one step by
default, so without it the turn calls a Vendo tool and ends there — the person
sees the tool fire and never gets an answer.

`principal` is not nullable, and an auth preset's resolver returns `Principal | null`. Drop the 401 and the call stops typechecking. Both imports come from the `lib/vendo.ts` in the [quickstart](/existing-agent/quickstart).

Pass `sessionId` alongside `principal` to carry your own session id into Vendo's audit trail. Leave it out and the shim mints one per pack build.

<Warning>
  The principal your loop passes has to be the one your wire route resolves. If they disagree, the agent reports success while the embed polls forever, because the person looking at the chat owns none of what the agent made.

  Resolve both from the same session, and never take a principal from the client. See [Auth](/production/auth).
</Warning>

## Render the parts

The shim builds every tool with the AI SDK's `dynamicTool`, so `useChat` streams them as `dynamic-tool` parts rather than `tool-<name>`. Match on that.

Where that match lives is yours. The path below is a name, not a contract — only your own chat imports it, so a `components/` directory you do not have is a directory you do not need. [`examples/ai-sdk-agent`](https://github.com/runvendo/vendo/tree/main/examples/ai-sdk-agent) inlines the same branch in `app/page.tsx` instead.

```tsx components/vendo-part.tsx focus={5-8} theme={null}
import type { UIMessage } from "ai";
import { VendoToolResult } from "@vendoai/vendo/react";

export function VendoPart({ part }: { part: UIMessage["parts"][number] }) {
  if (part.type !== "dynamic-tool") return null;
  return part.state === "output-available"
    ? <VendoToolResult output={part.output} />
    : <div>Running {part.toolName}…</div>;
}
```

Render that part outside any `<p>`. The embeds root in a `<div>`, so a chat that wraps every message part in a paragraph puts block markup inside one — the browser reparses it, and React throws a hydration error on the message that held the screen. Use a `<div>`, or no wrapper at all.

`<VendoToolResult>` handles plain data, app refs, and approval refs, so you never branch on the envelope yourself. Full contract: [Embeds in your chat](/existing-agent/embeds).

<Frame>
  <img src="https://mintcdn.com/vendo/Bl9khJxYuQX2mLio/images/existing-agents/ai-sdk-dashboard.png?fit=max&auto=format&n=Bl9khJxYuQX2mLio&q=85&s=eebf4ad975d2029c32bcff63069c2c9f" alt="The AI SDK example chat answering a dashboard request with a generated weather comparison screen rendered inline in the assistant message" width="448" height="800" data-path="images/existing-agents/ai-sdk-dashboard.png" />
</Frame>

## One Next.js line

`serverExternalPackages` keeps Vendo's app checker and its native and wasm dependencies out of the bundler.

```ts next.config.ts focus={2} theme={null}
const nextConfig = {
  serverExternalPackages: ["@vendoai/apps", "esbuild", "@electric-sql/pglite", "@vendoai/store"],
};

export default nextConfig;
```

On Next 14 the key is `experimental.serverComponentsExternalPackages` — same list, old name and location (renamed in Next 15).

`@vendoai/apps` syntax-checks generated apps with `esbuild`, and PGlite — loaded by `@vendoai/store` — backs persistence. `@vendoai/apps` is the entry that matters: it reaches esbuild through a variable specifier the bundler cannot see, so an `"esbuild"` entry on its own is inert. Paste all four — `vendo doctor` fails [`E-CFG-004`](/production/troubleshooting/e-cfg-004) on any one that is missing. The wire route needs no `runtime` or `dynamic` export, since Node is the default runtime and route handlers are already dynamic.

## Two models, one key

Your loop and Vendo's own turns each call a model. `vendoModel()` resolves through the Cloud gateway on your `VENDO_API_KEY`, and the builds behind `vendo_make` resolve the same way while `createVendo`'s `models` block is unset.

A name you pass reaches the gateway verbatim, since there is no client-side translation of model ids. Which seat takes which name is on [Model credentials](/production/model-credentials).

`vendo doctor` cannot see your loop's model, so an install is not done until a chat turn renders a Vendo tool output.

## The full example

[`examples/ai-sdk-agent`](https://github.com/runvendo/vendo/tree/main/examples/ai-sdk-agent) is the stock [AI SDK Next.js chatbot](https://ai-sdk.dev/docs/getting-started/nextjs-app-router) with this diff applied. Every added line sits between `--- vendo` and `--- /vendo` markers, so `grep` shows you the whole integration.
