Tracing Vercel AI SDK applications
The open-source Vercel AI SDK is a popular choice for building generative AI applications due to its ease of use and integrations with popular frameworks, such as Next.js. However, builders recognize that to reach production, they also need to incorporate observability into their applications. This cookbook will show you how to use Braintrust's native integration with the Vercel AI SDK for logging and tracing a generative AI application.
Getting started
To get started, make sure you have the following ready to go:
- A Braintrust account and API key
- A project in Braintrust
- An OpenAI API key
npm
installed
In this cookbook, we're going to use a simple chat application that gives you the temperature when you ask about the weather in a given city. The chatbot uses an OpenAI model, which calls one tool that gets the weather from open-meteo and another tool that converts the weather from Celsius to Fahrenheit.
Use npx
to download the application locally:
We'll only edit a few files in this example application:
For the application to run successfully, you'll need to rename the .env.local.example
file to .env.local
in the root of the project and add the following environment variables:
Run the application, and make sure you can access it at http://localhost:3000
. Feel free to test the application by asking it about the weather in Philadelphia.
It should look like this:
Tracing the application
Initializing a logger
To send logs to Braintrust, you'll need to initialize a logger by calling the initLogger
function. This function takes an apiKey
and a projectName
as arguments. The apiKey
is your Braintrust API key, and the projectName
is the name of your project in Braintrust. For lines 1-11 in the app/(preview)/api/chat/route.ts
file, uncomment the lines where instructed to load the necessary Braintrust functions and initialize the logger. Lines 1-11 should look like this:
Automatic tracing of models
The Braintrust SDK provides functions to "wrap" models, automatically logging inputs and outputs. When working with the Vercel AI SDK, you can use the wrapAISDKModel
function, which provides a common interface for models initiated by the Vercel AI SDK (for example, @ai-sdk/openai
).
The wrapAISDKModel
function only traces the inputs and outputs of the model. It does not trace intermediary steps such as tool calls that may be invoked during the model's execution. Later in the cookbook, we will explore how to use wrapTraced
to trace tool calls and nested functions.
The wrapAISDKModel
must be used with a Vercel interface (not a model
interface directly from the OpenAI, Anthropic, or other first-party libraries
from model providers). If you are not using Vercel model interfaces (such as
@ai-sdk/openai
) and using a model provider's first-party library, you can
wrap your model
clients
with wrapOpenAI
or wrapAnthropic
.
To correctly wrap the model client in our weather app example, your model client instantiation code should look like this after uncommenting the proper lines:
When we use the chatbot again, we see three logs appear in Braintrust: one log for the getWeather
tool call, one log for the getFahrenheit
tool call, and one call to form the final response. However, it'd probably be more useful to have all of these operations in the same log.
Creating spans (and sub-spans)
When tracing events, it's common practice to place child events within a single parent event. As an example, take grouping the three logs that we produced above into the same log record. You can do this using the traced
function.
To create a parent span in our weather app, uncomment the traced
function (don't forget to uncomment the final line of code that closes the function). You can also uncomment the onFinish
argument, which will log the input and output of the streamText
function to the parent span. Your POST route should look like this when finished:
After you uncomment those lines of code, you should see the following:
A couple of things happened in this step:
- We created a root span called "POST /api/chat" to group any subsequent logs into.
- We continued to create spans via the
wrapAISDKModel
function. - We used the
onFinish
argument of thestreamText
function to gather the input and output of the LLM and return it to the root span.
This looks good so far, but we also want to know about the different tool calls that the LLM is making as it works to form its response.
Tracing tool calls
The last thing that we need to adjust is adding our tool calls and functions to the trace. You can do this by encapsulating existing functions with wrapTraced
, which will automatically capture the inputs and outputs of the functions. When using wrapTraced
, the hierarchy of nested functions is preserved.
The following code in components/tools.ts
has two main components:
- A
getFahrenheit
tool, which converts a Celsius temperature into Fahrenheit. It also nests thecheckFreezing
function inside theconvertToFahrenheit
function. - A
getWeather
tool which takes a latitude and longitude as input and returns a Celsius temperature as output.
Uncomment the code where noted so that your tools.ts
file looks like this:
After we finish uncommenting the correct lines, we see how the wrapTraced
function enriches our trace with tool calls.
Take note of how the type
argument in both traced
and wrapTraced
change the icon within the trace tree. Also, since checkFreezing
was called by weatherFunction
, the trace preserves the hierarchy.
Next steps
- Customize and extend traces to better optimize for your use case
- Read more about Brainstore, the database that powers the logging backend in Braintrust