> ## Documentation index
> Fetch the complete documentation index at: https://arize.com/llms.txt
> Ask it with GET https://arize.com/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Vercel AI SDK Tracing (JS)

> Trace Vercel AI SDK calls with @arizeai/phoenix-otel and send spans to Phoenix for LLM and agent observability.

Phoenix traces [Vercel AI SDK](https://github.com/vercel/ai) (>= 7) applications through the AI SDK's OpenTelemetry integration (`@ai-sdk/otel`). The simplest setup is [`@arizeai/phoenix-otel`](https://www.npmjs.com/package/@arizeai/phoenix-otel), which handles the OpenTelemetry provider, OpenInference span processing, and export to Phoenix in a single `register()` call.

## Version compatibility

| Vercel AI SDK | @arizeai/phoenix-otel | @arizeai/openinference-vercel |
| ------------- | --------------------- | ----------------------------- |
| v7+           | 2.x                   | 3.x                           |
| v6 and older  | 1.x                   | 2.x                           |

AI SDK v7 telemetry requires Node.js 22 or newer.

## Installation

```bash
npm i --save ai @ai-sdk/otel @arizeai/phoenix-otel
```

<Warning>
Ensure your installed version of `@opentelemetry/api` matches the version installed by `@ai-sdk/otel` otherwise the AI SDK will not emit traces to the TracerProvider that you configure. If you install `ai` before the other packages, then dependency resolution in your package manager should install the correct version.
</Warning>

## Setup

Since AI SDK v7, telemetry is emitted once you register a telemetry integration with `registerTelemetry(new OpenTelemetry(...))`. Pair that with `register()` from `@arizeai/phoenix-otel`, which processes the resulting spans and exports them to Phoenix:

```typescript
// instrumentation.ts
import { OpenTelemetry } from "@ai-sdk/otel";
import { register } from "@arizeai/phoenix-otel";
import { registerTelemetry } from "ai";

// Handles all the OpenTelemetry setup and exports spans to Phoenix.
// Reads PHOENIX_COLLECTOR_ENDPOINT and PHOENIX_API_KEY from the environment.
export const provider = register({
  projectName: "my-ai-sdk-app",
});

// Point the AI SDK's telemetry at OpenTelemetry. headers: false keeps
// outgoing LLM request headers (which can contain credentials) off of spans.
registerTelemetry(new OpenTelemetry({ headers: false }));
```

Import this file before the rest of your program executes, e.g. `node --import ./instrumentation.ts index.ts`, or `import "./instrumentation.js"` at the top of your application's entrypoint.

Once the telemetry integration is registered, AI SDK calls are traced by default; no per-call configuration is required. Use the `telemetry` option for per-call metadata such as `functionId`, or to opt out with `isEnabled: false`.

```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const result = await generateText({
  model: openai("gpt-4o"),
  prompt: "Write a short story about a cat.",
  // Optional per-call metadata:
  telemetry: { functionId: "story-agent" },
});
```

This applies to agents as well: a `ToolLoopAgent` run is traced end to end, with the agent, each LLM step, and every tool call captured as spans in a single trace. See the [AI SDK agent example](https://github.com/Arize-ai/phoenix/tree/main/js/examples/apps/ai-sdk-agent) for a complete runnable project.

<Note>
`register()` uses a batch span processor by default, so spans still queued when a short-lived script exits may not be exported. Call `await provider.shutdown()` before exit to flush them, or pass `batch: false` to `register()` for immediate export.
</Note>

## Decision models

AI SDK decision models, such as OpenAI's `gpt-6-luna` through `openai.decisionModel()` or TypeSafe's Jev through `@ai-sdk/typesafe-ai`, answer typed questions about a shared state instead of generating text. Calls made through the AI SDK's `experimental_decide` are traced as **[DECISION spans](/docs/phoenix/tracing/concepts-tracing/otel-openinference/span-kinds#decision)** rather than LLM spans, with no extra instrumentor. This requires `@arizeai/openinference-vercel` 3.3.0 or later, plus `ai` 7.0.128 and `@ai-sdk/otel` 1.0.128 or later. `@arizeai/phoenix-otel` 2.x depends on `@arizeai/openinference-vercel` with a caret range, so a fresh install resolves to 3.3.0 or later. In an existing project, check the version in your lockfile and run `npm update @arizeai/openinference-vercel` if it is older.

Two options on the `OpenTelemetry` integration shape what Phoenix receives:

- `experimental_decision: true` records the decision state, questions, and answers. Without it the spans carry the model and token counts but no input or output.
- `enrichSpan: enrichSpanWithOpenInference` tells the processor which of the two spans the AI SDK emits per call is the model call. The SDK emits an outer operation span and a child model-call span with identical `gen_ai.*` attributes, so without this hint both are exported as DECISION spans. `enrichSpanWithOpenInference` is re-exported from the ESM-only `@arizeai/phoenix-otel/vercel` subpath.

```typescript
// instrumentation.ts
import { OpenTelemetry } from "@ai-sdk/otel";
import { register } from "@arizeai/phoenix-otel";
import { enrichSpanWithOpenInference } from "@arizeai/phoenix-otel/vercel";
import { registerTelemetry } from "ai";

export const provider = register({
  projectName: "my-ai-sdk-app",
});

registerTelemetry(
  new OpenTelemetry({
    headers: false,
    experimental_decision: true,
    enrichSpan: enrichSpanWithOpenInference,
  }),
);
```

With that in place, a decision call needs no per-call configuration:

```typescript
import { experimental_decide } from "ai";
import { openai } from "@ai-sdk/openai";

const { answers } = await experimental_decide({
  model: openai.decisionModel("gpt-6-luna"),
  state: "Customer message: My invoice shows a charge I don't recognize.",
  questions: {
    department: {
      type: "choice",
      instructions: "Which team should handle this message?",
      criteria: {
        billing: "Charges, refunds, and invoices",
        technical: "Bugs, outages, and setup problems",
        sales: "Pricing, plans, and upgrades",
      },
    },
  },
});

console.log(answers.department.choice, answers.department.probabilities);
```

Each call appears in Phoenix as two spans named `decide <model>`, for example `decide gpt-6-luna`: a `CHAIN` span for the decide operation, carrying the state and questions as input and the answers as output, with a child **DECISION** span for the model call carrying:

- `decision.system` and `decision.provider`, for example `openai` or `typesafe`
- `decision.request.model_name`, `decision.response.model_name`, and `decision.model_name`. The response model needs `@ai-sdk/otel` 1.0.138 or later; earlier versions do not record it on decide spans.
- `decision.token_count.input` and `decision.token_count.output`. The output count is often `0`, since the answers are typed values rather than generated tokens.

Phoenix names the decision model in the DECISION span's input card. Because the model and its usage are recorded under `decision.*` rather than `llm.*`, decision calls stay out of Phoenix's LLM token and cost reporting.

<Note>
TypeScript [classification evaluators](/docs/phoenix/sdk-api-reference/typescript/packages/phoenix-evals/classification#decision-models) from `@arizeai/phoenix-evals` take the same path when given a decision model. They enable `experimental_decision` on their own telemetry integration, so the state, questions, and answers are recorded with no further setup. They do not set `enrichSpan`, so the outer operation span is also exported as a DECISION span rather than a CHAIN span.
</Note>

## Next.js

In a Next.js project, register the telemetry integration and `@vercel/otel` from the `register()` function in `instrumentation.ts`, using the OpenInference span processor from [`@arizeai/openinference-vercel`](https://github.com/Arize-ai/openinference/tree/main/js/packages/openinference-vercel), the same processor `@arizeai/phoenix-otel` uses under the hood:

```bash
npm i --save @arizeai/openinference-vercel @arizeai/openinference-semantic-conventions @vercel/otel @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto
```

```javascript expandable
// instrumentation.ts
// Vercel / Next.js environment instrumentation
import { registerOTel, OTLPHttpProtoTraceExporter } from "@vercel/otel";
import { registerTelemetry } from "ai";
import { OpenTelemetry } from "@ai-sdk/otel";
import {
  isOpenInferenceSpan,
  OpenInferenceSimpleSpanProcessor,
} from "@arizeai/openinference-vercel";
import { SEMRESATTRS_PROJECT_NAME } from "@arizeai/openinference-semantic-conventions";

// e.g. http://localhost:6006
// e.g. https://your-phoenix.example.com
const COLLECTOR_ENDPOINT = process.env.PHOENIX_COLLECTOR_ENDPOINT;
// The project name that may appear in your collector's interface
const SERVICE_NAME = "phoenix-vercel-ai-sdk-app";

/**
 * Register function used by Next.js to instantiate instrumentation
 * correctly in all environments that Next.js can be deployed to
 */
export function register() {
  // Register the AI SDK telemetry integration. Header capture is disabled
  // because request headers can contain credentials.
  registerTelemetry(new OpenTelemetry({ headers: false }));

  registerOTel({
    serviceName: SERVICE_NAME,
    attributes: {
      [SEMRESATTRS_PROJECT_NAME]: SERVICE_NAME,
    },
    spanProcessors: [
      new OpenInferenceSimpleSpanProcessor({
        exporter: new OTLPHttpProtoTraceExporter({
          url: `${COLLECTOR_ENDPOINT}/v1/traces`,
          // (optional) if connecting to a collector with Authentication enabled
          headers: { Authorization: `Bearer ${process.env.PHOENIX_API_KEY}` },
        }),
        spanFilter: isOpenInferenceSpan,
      }),
    ],
  });
}
```

See Vercel's [instrumentation guide](https://nextjs.org/docs/app/guides/open-telemetry#using-vercelotel) for more details on configuring your instrumentation file and `@vercel/otel` within a Next.js project.

<Info>
When instrumenting a Next.js application, traced spans will not be "root spans" when the OpenInference span filter is configured. This is because Next.js parents spans underneath http requests, which do not meet the requirements to be an OpenInference span.
</Info>

<Frame>
  <iframe src="https://cdn.iframe.ly/ry2U623" width={1000} height={400} allowFullScreen scrolling="no" allow="accelerometer *; clipboard-write *; encrypted-media *; gyroscope *; picture-in-picture *; web-share *;"></iframe>
</Frame>

## Examples

- [AI SDK Agent](https://github.com/Arize-ai/phoenix/tree/main/js/examples/apps/ai-sdk-agent): an AI SDK v7 `ToolLoopAgent` traced with `@arizeai/phoenix-otel`
- [Next.js OpenAI Telemetry Example](https://github.com/Arize-ai/openinference/tree/main/js/examples/next-openai-telemetry-app) in the [OpenInference repo](https://github.com/Arize-ai/openinference/tree/main/js)

For details on Vercel AI SDK telemetry see the [Vercel AI SDK Telemetry documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry).
