Vue.js (Nuxt) Quickstart
The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications.
In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects.
If you are unfamiliar with the concepts of Prompt Engineering and HTTP Streaming, you can optionally read these documents first.
Prerequisites
To follow this quickstart, you'll need:
- Node.js 18+ and pnpm installed on your local development machine.
- An OpenAI API key.
If you haven't obtained your OpenAI API key, you can do so by signing up on the OpenAI website.
Setup Your Application
Start by creating a new Nuxt application. This command will create a new directory named my-ai-app
and set up a basic Nuxt application inside it.
pnpm create nuxt my-ai-app
Navigate to the newly created directory:
cd my-ai-app
Install dependencies
Install ai
and @ai-sdk/openai
, the AI SDK's OpenAI provider.
The AI SDK is designed to be a unified interface to interact with any large language model. This means that you can change model and providers with just one line of code! Learn more about available providers and building custom providers in the providers section.
pnpm add ai@beta @ai-sdk/openai@beta @ai-sdk/vue@beta zod
Configure OpenAI API key
Create a .env
file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service.
touch .env
Edit the .env
file:
NUXT_OPENAI_API_KEY=xxxxxxxxx
Replace xxxxxxxxx
with your actual OpenAI API key and configure the environment variable in nuxt.config.ts
:
export default defineNuxtConfig({ // rest of your nuxt config runtimeConfig: { openaiApiKey: '', },});
The AI SDK's OpenAI Provider will default to using the OPENAI_API_KEY
environment variable.
Create an API route
Create an API route, server/api/chat.ts
and add the following code:
import { streamText, UIMessage, convertToModelMessages } from 'ai';import { createOpenAI } from '@ai-sdk/openai';
export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, });
return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event);
const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), });
return result.toUIMessageStreamResponse(); });});
Let's take a look at what is happening in this code:
- Create an OpenAI provider instance with the
createOpenAI
function from the@ai-sdk/openai
package. - Define an Event Handler and extract
messages
from the body of the request. Themessages
variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. Themessages
are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps. - Call
streamText
, which is imported from theai
package. This function accepts a configuration object that contains amodel
provider (defined in step 1) andmessages
(defined in step 2). You can pass additional settings to further customise the model's behaviour. Themessages
key expects aModelMessage[]
array. This type is different fromUIMessage
in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use theconvertToModelMessages
function, which strips the UI-specific metadata and transforms theUIMessage[]
array into theModelMessage[]
format that the model expects. - The
streamText
function returns aStreamTextResult
. This result object contains thetoDataStreamResponse
function which converts the result to a streamed response object. - Return the result to the client to stream the response.
Wire up the UI
Now that you have an API route that can query an LLM, it's time to setup your frontend. The AI SDK's UI package abstract the complexity of a chat interface into one hook, useChat
.
Update your root page (pages/index.vue
) with the following code to show a list of chat messages and provide a user message input:
<script setup lang="ts">import { Chat } from "@ai-sdk/vue";import { ref } from "vue";
const input = ref("");const chat = new Chat({});
const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = "";};</script>
<template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> </div> </div>
<form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div></template>
If your project has app.vue
instead of pages/index.vue
, delete the
app.vue
file and create a new pages/index.vue
file with the code above.
This page utilizes the useChat
hook, which will, by default, use the API route you created earlier (/api/chat
). The hook provides functions and state for handling user input and form submission. The useChat
hook provides multiple utility functions and state variables:
messages
- the current chat messages (an array of objects withid
,role
, andparts
properties).sendMessage
- a function to send a message to the chat API.
The component uses local state (ref
) to manage the input field value, and handles form submission by calling sendMessage
with the input text and then clearing the input field.
The LLM's response is accessed through the message parts
array. Each message contains an ordered array of parts
that represents everything the model generated in its response. These parts can include plain text, reasoning tokens, and more that you will see later. The parts
array preserves the sequence of the model's outputs, allowing you to display or process each component in the order it was generated.
Running Your Application
With that, you have built everything you need for your chatbot! To start your application, use the command:
pnpm run dev
Head to your browser and open http://localhost:3000. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Nuxt.
Enhance Your Chatbot with Tools
While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where tools come in.
Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response.
For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information.
Let's enhance your chatbot by adding a simple weather tool.
Update Your API Route
Modify your server/api/chat.ts
file to include the new weather tool:
import { streamText, UIMessage, convertToModelMessages, tool } from 'ai';import { createOpenAI } from '@ai-sdk/openai';import { z } from 'zod';
export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, });
return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event);
const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(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, }; }, }), }, });
return result.toUIMessageStreamResponse(); });});
In this updated code:
-
You import the
tool
function from theai
package andz
fromzod
for schema validation. -
You define a
tools
object with aweather
tool. This tool:- Has a description that helps the model understand when to use it.
- Defines
inputSchema
using a Zod schema, specifying that it requires alocation
string to execute this tool. The model will attempt to extract this input from the context of the conversation. If it can't, it will ask the user for the missing information. - Defines an
execute
function that simulates getting weather data (in this case, it returns a random temperature). This is an asynchronous function running on the server so you can fetch real data from an external API.
Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary input. The execute
function will then be automatically run, and the tool output will be added to the messages
as a tool
message.
Try asking something like "What's the weather in New York?" and see how the model uses the new tool.
Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result on the client via the tool-weather
part of the message.parts
array.
Tool parts are always named tool-{toolName}
, where {toolName}
is the key
you used when defining the tool. In this case, since we defined the tool as
weather
, the part type is tool-weather
.
Update the UI
To display the tool invocation in your UI, update your pages/index.vue
file:
<script setup lang="ts">import { Chat } from "@ai-sdk/vue";import { ref } from "vue";
const input = ref("");const chat = new Chat({});
const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = "";};</script>
<template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> <pre v-if="part.type === 'tool-weather'">{{ JSON.stringify(part, null, 2) }}</pre> </div> </div>
<form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div></template>
With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather tool invocations, you display a JSON representation of the tool call and its result.
Now, when you ask about the weather, you'll see the tool call and its result displayed in your chat interface.
Enabling Multi-Step Tool Calls
You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation.
To solve this, you can enable multi-step tool calls using stopWhen
. By default, stopWhen
is set to stepCountIs(1)
, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question.
Update Your API Route
Modify your server/api/chat.ts
file to include the stopWhen
condition:
import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs,} from 'ai';import { createOpenAI } from '@ai-sdk/openai';import { z } from 'zod';
export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, });
return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event);
const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), 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, }; }, }), }, });
return result.toUIMessageStreamResponse(); });});
Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question.
By setting stopWhen: stepCountIs(5)
, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Fahrenheit to Celsius.
Add another tool
Update your server/api/chat.ts
file to add a new tool to convert the temperature from Fahrenheit to Celsius:
import { streamText, UIMessage, convertToModelMessages, tool, stepCountIs,} from 'ai';import { createOpenAI } from '@ai-sdk/openai';import { z } from 'zod';
export default defineLazyEventHandler(async () => { const apiKey = useRuntimeConfig().openaiApiKey; if (!apiKey) throw new Error('Missing OpenAI API key'); const openai = createOpenAI({ apiKey: apiKey, });
return defineEventHandler(async (event: any) => { const { messages }: { messages: UIMessage[] } = await readBody(event);
const result = streamText({ model: openai('gpt-4o'), messages: convertToModelMessages(messages), stopWhen: stepCountIs(5), 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, }; }, }), 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, }; }, }), }, });
return result.toUIMessageStreamResponse(); });});
Update Your Frontend
Update your UI to handle the new temperature conversion tool by modifying the tool part handling:
<script setup lang="ts">import { Chat } from "@ai-sdk/vue";import { ref } from "vue";
const input = ref("");const chat = new Chat({});
const handleSubmit = (e: Event) => { e.preventDefault(); chat.sendMessage({ text: input.value }); input.value = "";};</script>
<template> <div> <div v-for="(m, index) in chat.messages" :key="m.id ? m.id : index"> {{ m.role === "user" ? "User: " : "AI: " }} <div v-for="(part, index) in m.parts" :key="`${m.id}-${part.type}-${index}`" > <div v-if="part.type === 'text'">{{ part.text }}</div> <pre v-if=" part.type === 'tool-weather' || part.type === 'tool-convertFahrenheitToCelsius' " >{{ JSON.stringify(part, null, 2) }}</pre > </div> </div>
<form @submit="handleSubmit"> <input v-model="input" placeholder="Say something..." /> </form> </div></template>
This update handles the new tool-convertFahrenheitToCelsius
part type, displaying the temperature conversion tool calls and results in the UI.
Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction:
- The model will call the weather tool for New York.
- You'll see the tool output displayed.
- It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius.
- The model will then use that information to provide a natural language response about the weather in New York.
This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful.
This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information.
Where to Next?
You've built an AI chatbot using the AI SDK! From here, you have several paths to explore:
- To learn more about the AI SDK, read through the documentation.
- If you're interested in diving deeper with guides, check out the RAG (retrieval-augmented generation) and multi-modal chatbot guides.
- To jumpstart your first AI project, explore available templates.