Skip to main content
npm version This package provides a set of utilities to ingest Vercel AI SDK(>= 3.3) spans into platforms like Arize and Phoenix.
Note: This package requires you to be using the Vercel AI SDK version 3.3 or higher.

Getting Started

Installation

You will need to install the @arizeai/openinference-vercel package as well as a few packages from opentelemetry.
npm i @arizeai/openinference-vercel @opentelemetry/exporter-trace-otlp-proto @opentelemetry/api
Important! If you are using the registerOtel function from @vercel/otel (see Runtimes below) you must ensure that opentelemetry packages match those used by @vercel/otel . You must use opentelemetry packages of version 1.x or 0.1.x with version 1.x of @vercel/otel . Keep this in mind during your installs
@vercel/otelopentelemetry v1.x (0.1.x)opentelemetry v2.x (0.2.x)
1.x
2.x

AI SDK Setup

In order to trace calls to the AI SDK you must set the experimental_telemetry flag on each call.
import { openai } from "@ai-sdk/openai";
import {
  streamText,
  UIMessage,
  convertToModelMessages,
  tool,
  stepCountIs,
} from "ai";

const result = streamText({
  model: openai("gpt-4o"),
  messages: convertToModelMessages(messages),
  // Set this flag on each call to the SDK
  experimental_telemetry: { isEnabled: true },
});

Runtimes

Depending on your runtime, you may need to set up instrumentation differently. If you’re using Next.js and are tracing AI SDK calls in edge runtimes you will need to use the registerOtel function from @vercel/otel . Otherwise in Node runtimes you can use the NodeTracerProvider or the NodeSDK .
Additional Dependencies
npm i @vercel/otel
Add the following instrumentation.ts at the top level of your src directory.
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
import { registerOTel } from "@vercel/otel";
import {
  isOpenInferenceSpan,
  OpenInferenceSimpleSpanProcessor,
} from "@arizeai/openinference-vercel";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";

// Optionally set logging for debugging, remove in production
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

export function register() {
  registerOTel({
    serviceName: "next-app",
    attributes: {
      model_id: "my-ai-app",
      model_version: "1.0.0",
    },
    spanProcessors: [
      new OpenInferenceSimpleSpanProcessor({
        exporter: new OTLPTraceExporter({
          url: "https://otlp.arize.com/v1/traces",
          headers: {
            space_id: process.env.ARIZE_SPACE_ID || "",
            api_key: process.env.ARIZE_API_KEY || "",
          },
        }),
        // Optionally add a span filter to only include AI related spans
        // This will cause no traces to appear in AX because AI spans 
        // are nested below http spans
        spanFilter: isOpenInferenceSpan,
      }),
    ],
  });
}

Further Documentation

Span Filter

In some environments, enabling telemetry on the AI SDK will enable tracing on more than AI related calls. Most commonly there will be http spans for POST and GET requests. The @arizeai/openinference-vercel package exports the isOpenInferenceSpan helper function to filter these non-ai spans out. In the above examples it is included. Filtering spans in this way may cause spans in AX to be orphaned (no parent trace) as the root spans are filtered out. Because of this you won’t see any spans on the traces tab, but will see AI SDK spans on the spans tab. You can also add an additional span processor to override the behavior of Vercel, adding a trace to the highest level AI SDK span, allowing you to see spans on the traces tab. To do this, create a file called root-aware-processor.ts with the following code:
import { Context } from '@opentelemetry/api';
import { Span } from '@opentelemetry/sdk-trace-base';
import { 
  OpenInferenceBatchSpanProcessor,
  isOpenInferenceSpan,
} from '@arizeai/openinference-vercel';
import { getSession } from '@arizeai/openinference-core';
import { SESSION_ID } from '@arizeai/openinference-semantic-conventions';
import { LRUCache } from 'lru-cache';
import type { SpanExporter } from '@opentelemetry/sdk-trace-base';

/**
 * Root OpenInference span prefixes from the Vercel AI SDK.
 * These are the top-level CHAIN spans that wrap LLM/Embedding operations.
 * 
 * Note: Span names may have a function ID suffix (e.g., "ai.generateText my-function-id")
 * so we need to check the prefix, not exact match.
 * 
 * These are what we want to look for to find the "root" openinference span. 
 * The first one we see will have it's context reset to have no parent
 */
const ROOT_OI_SPAN_PREFIXES = [
  'ai.generateText',
  'ai.generateObject',
  'ai.streamText',
  'ai.streamObject',
  'ai.embed',
  'ai.embedMany',
];

function isRootOISpanByName(spanName: string): boolean {
  const functionName = spanName.split(' ')[0];
  return ROOT_OI_SPAN_PREFIXES.some(prefix => 
    functionName === prefix || functionName.startsWith(prefix + ' ')
  );
}

interface RootAwareProcessorConfig {
  exporter: SpanExporter;
  /** Size of the LRU cache for tracking trace IDs. Defaults to 1000. */
  cacheSize?: number;
}

/**
 * A span processor that:
 * 1. Propagates OpenInference session ids from context to spans
 * 2. Makes the first root OpenInference span the actual root of the trace
 *    by clearing its parent span ID
 * 
 * This solves the issue where HTTP/fetch spans become root spans in Vercel/Next.js
 * environments, causing the trace hierarchy to break when using isOpenInferenceSpan filter.
 * 
 * Uses an LRU cache to track which traces have already had their root span identified.
 * This way if for some reason there are multiple ROOT_OI_SPAN_PREFIXES spans in a trace, only the "top" one will be set to root
 */
export class RootAwareOpenInferenceProcessor extends OpenInferenceBatchSpanProcessor {
  private traceIdCache: LRUCache<string, boolean>;
  
  constructor(config: RootAwareProcessorConfig) {
    super({
      exporter: config.exporter,
      spanFilter: isOpenInferenceSpan,
    });
    this.traceIdCache = new LRUCache({ max: config.cacheSize ?? 1000 });
  }

  onStart(span: Span, parentContext: Context): void {
    const sessionInfo = getSession(parentContext);
    if (sessionInfo?.sessionId) {
      span.setAttribute(SESSION_ID, sessionInfo.sessionId);
    }

    const spanName = span.name;
    const traceId = span.spanContext().traceId;
    
    if (isRootOISpanByName(spanName)) {
      if (!this.traceIdCache.has(traceId)) {
        // This is the first root OI span for this trace - make it the actual root and then add it to the lru cache
        // parentSpanId is a readonly property so we need to cast the Span to any to modify the parent context.
        (span as any).parentSpanId = undefined;
        (span as any).parentSpanContext = undefined;
        
        this.traceIdCache.set(traceId, true);
      }
    }

    super.onStart(span, parentContext);
  }
  
  shutdown(): Promise<void> {
    this.traceIdCache.clear();
    return super.shutdown();
  }
}
You can then use this processor from the register function in your instrumentation.ts file:
export function register() {
  registerOTel({
    ...
    spanProcessors: [
      new RootAwareOpenInferenceProcessor({
        exporter: new OTLPTraceExporter({
          url: "https://otlp.arize.com/v1/traces",
          headers: {
            space_id: process.env.ARIZE_SPACE_ID || "",
            api_key: process.env.ARIZE_API_KEY || "",
          },
        })
      }),
    ],
  });
}