Vercel AI SDK integration

View as Markdown

@getdial/ai-sdk packages Dial’s operations as Vercel AI SDK tools, so a model can send messages and place calls as part of its reasoning. It’s the TypeScript analog of the LangChain integration: a thin adapter over @getdial/sdk — it adds nothing to the REST contract, it just shapes Dial’s operations into AI SDK tool() definitions.

Not Vercel-only. The “Vercel AI SDK” is the open-source ai package — it runs on any JS host (Next.js on any platform, Node, Hono, Cloudflare Workers, Deno, Bun) and with any tool-calling model (Claude, GPT, Gemini, …). These tools work wherever it does.

Install

$npm install @getdial/ai-sdk ai zod

ai and zod are peer dependencies — your app already has them. The package pulls in @getdial/sdk as its Dial client.

Give the tools to a model

Build one DialClient, pass it to dialTools, and spread the result into the tools field. Every tool shares that one client (a single connection pool). To switch models, change one line; the tools don’t change.

1import { streamText } from "ai";
2import { anthropic } from "@ai-sdk/anthropic";
3import { DialClient } from "@getdial/sdk";
4import { dialTools } from "@getdial/ai-sdk";
5
6const dial = new DialClient({ apiKey: process.env.DIAL_API_KEY! });
7
8const result = streamText({
9 model: anthropic("claude-opus-4-8"), // or openai("gpt-5"), google("gemini-2.5-pro"), …
10 tools: dialTools(dial, {
11 fromNumberId: process.env.DIAL_FROM_NUMBER_ID, // optional default "from"
12 }),
13 messages,
14});

When fromNumberId is set, it’s the default for sendMessage and makeCall — the model can omit it, mirroring the CLI’s saved default from_number_id.

Available tools

ToolAction
listNumbersList your phone numbers
purchaseNumberProvision a new number (billable)
setNumberPropertiesUpdate a number’s nickname / inbound instruction / voice / duration cap
sendMessageSend an SMS (optionally MMS)
replyToMessageReply or react to a message
startTypingShow a typing indicator (iMessage numbers; SMS numbers ignore it)
stopTypingClear a typing indicator
listMessagesList recent messages
makeCallPlace an AI voice call
listCallsList recent calls
getCallFetch one call by id
getBillingWallet balance, subscription, per-number mode
waitForMessageBlock until the next inbound SMS arrives, or time out

Each execute returns the structured Dial object (a Message, Call, etc.) — the AI SDK serializes the tool result back into the model for you.

Or pick individual tools

When you only want a subset, import the factories directly:

1import { DialClient } from "@getdial/sdk";
2import { sendMessageTool, makeCallTool } from "@getdial/ai-sdk";
3
4const dial = new DialClient({ apiKey: process.env.DIAL_API_KEY! });
5const tools = {
6 sendMessage: sendMessageTool(dial, { fromNumberId: "pn_..." }),
7 makeCall: makeCallTool(dial, { fromNumberId: "pn_..." }),
8};

Example: a Next.js chat route

A typical AI SDK app exposes a route handler that streams the model’s response. Drop dialTools(...) into it and the chat can text and call people:

app/api/chat/route.ts
1import { streamText, convertToModelMessages, type UIMessage } from "ai";
2import { anthropic } from "@ai-sdk/anthropic";
3import { DialClient } from "@getdial/sdk";
4import { dialTools } from "@getdial/ai-sdk";
5
6const dial = new DialClient({ apiKey: process.env.DIAL_API_KEY! });
7
8export async function POST(req: Request) {
9 const { messages }: { messages: UIMessage[] } = await req.json();
10
11 const result = streamText({
12 model: anthropic("claude-opus-4-8"),
13 tools: dialTools(dial, {
14 fromNumberId: process.env.DIAL_FROM_NUMBER_ID,
15 }),
16 messages: convertToModelMessages(messages),
17 });
18
19 return result.toUIMessageStreamResponse();
20}

Now an end user typing “text the plumber at +14155550123 that I’ll be 10 minutes late” gets the model to call sendMessage with the right to and body.

Send a code and await the reply

waitForMessage pairs with sendMessage so the model can verify a number inside a single turn — send a one-time code, then block on the inbound reply:

1import { generateText } from "ai";
2import { DialClient } from "@getdial/sdk";
3import { dialTools } from "@getdial/ai-sdk";
4
5const dial = new DialClient({ apiKey: process.env.DIAL_API_KEY! });
6const { text } = await generateText({
7 model,
8 tools: dialTools(dial, { fromNumberId: "pn_..." }),
9 // The model calls sendMessage to send the code, then waitForMessage to read the reply.
10 prompt: "Text +14155550123 the verification code 4821, wait up to 60s for their reply, and tell me whether it matches.",
11});

waitForMessage opens the account event stream, returns the first matching message.received, and times out after timeoutSeconds (default 30).

sendMessage is a write action and isn’t idempotent — a re-invoke after a failure can send a duplicate. makeCall accepts an idempotencyKey: a re-invoke with the same key returns the already-placed call instead of dialing again. See Retries and idempotency.

Receiving events beyond a single turn

waitForMessage is a one-shot wait, backed by a presence-based stream (not at-least-once — missed events replay only on reconnect within ~2 minutes). To react to inbound SMS or completed calls durably and continuously, don’t model it as a tool: use the Node SDK’s newEventsConnection() directly, or register a webhook (signed and retried at-least-once), and feed events into your agent however suits your app.