Trace Vercel Eve agents with OpenInference and send AI SDK spans to Arize AX for LLM and agent observability.
Eve is Vercel’s filesystem-first TypeScript framework for durable backend AI agents — you define an agent as files under an agent/ directory and Eve compiles it into an app that runs on Vercel Functions. Eve emits Vercel AI SDK OpenTelemetry spans for every turn, model call, and tool execution. Arize AX captures them by registering the @arizeai/openinference-vercel span processor in Eve’s agent/instrumentation.ts.
Eve auto-discovers agent/instrumentation.ts and runs it once at server startup, before any agent code. Its presence enables telemetry — there is no separate toggle, and you do not set experimental_telemetry on individual calls (Eve manages that internally). Register an OpenTelemetry provider in the setup callback, which receives the resolved agent name:
// agent/instrumentation.tsimport { defineInstrumentation } from "eve/instrumentation";import { registerOTel } from "@vercel/otel";import { isOpenInferenceSpan, OpenInferenceSimpleSpanProcessor,} from "@arizeai/openinference-vercel";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";export default defineInstrumentation({ setup: ({ agentName }) => registerOTel({ serviceName: agentName, // Arize routes spans to a project by the `model_id` resource attribute — // without it the OTLP endpoint rejects spans with an InvalidArgument error. attributes: { model_id: agentName }, spanProcessors: [ new OpenInferenceSimpleSpanProcessor({ exporter: new OTLPTraceExporter({ url: "https://otlp.arize.com/v1/traces", headers: { "arize-space-id": process.env.ARIZE_SPACE_ID ?? "", "arize-api-key": process.env.ARIZE_API_KEY ?? "", }, }), // Keep the AI spans and Eve's `ai.eve.turn` workflow span (the // per-turn parent), and drop raw HTTP/fetch spans. See the Span // filter section for why `ai.eve.turn` is kept and how to promote // it to a clean trace root. spanFilter: (span) => isOpenInferenceSpan(span) || span.name === "ai.eve.turn", }), ], }),});
OpenInferenceSimpleSpanProcessor exports each span synchronously as it ends, so it is safe on the short-lived serverless functions Eve runs on (no process-exit forceFlush to call). @arizeai/openinference-vercel translates the AI SDK spans into OpenInference before export.
{"type":"session.started","data":{"runtime":{"agentName":"weather-agent"}}}{"type":"actions.requested","data":{"actions":[{"kind":"tool-call","toolName":"get_weather","input":{"city":"Brooklyn"}}]}}{"type":"message.completed","data":{"message":"The weather in Brooklyn is **sunny** and **72°F**.","finishReason":"stop"}}
Open your Arize AX space and select the project named after your agent (the model_id above).
Within ~30 seconds you should see the turn’s spans. Eve runs on the AI SDK’s GenAI-convention spans, so every span arrives named gen_ai (or gen_ai.client for the model request) rather than ai.streamText/ai.toolCall — read the OpenInference span kind to tell them apart. Each step is an agent span over a llm span; the model request is a gen_ai.clientllm span (prompt, response, token usage), and each tool run is a gen_aitool span carrying tool.name (for example get_weather). All of a turn’s steps nest under Eve’s ai.eve.turn workflow span, which the spanFilter above keeps as the per-turn parent.
Eve attaches session context to the spans under the ai.settings.context.eve.* prefix — ai.settings.context.eve.session.id, ai.settings.context.eve.turn.id, ai.settings.context.eve.step.index, and ai.settings.context.eve.channel.kind — so you can group a trace back to its session.
Eve runs each turn inside its own ai.eve.turn workflow span, and nests the per-step gen_ai spans under it. That workflow span is not an OpenInference span, so isOpenInferenceSpan on its own drops it — and dropping it orphans every gen_ai span on the Traces tab (their parent is gone). The filter shown in Setup tracing keeps ai.eve.turn so the step spans stay anchored to one per-turn parent:
The trade-off: ai.eve.turn itself hangs off Eve’s Vercel Workflow step.execute spans, which are also non-OpenInference and dropped — so ai.eve.turn survives but remains parent-less, showing as an orphan on the Traces tab. If you also need a clean trace tree, swap the filter for a span processor that keeps ai.eve.turn, promotes it to root by clearing its parent ID, and tags it as an agent span (the openinference-vercel translation leaves the workflow span kind-less otherwise, since it isn’t an AI SDK span). Eve emits one trace per turn, so the processor promotes the single ai.eve.turn in each trace:
// root-aware-processor.tsimport { Context } from "@opentelemetry/api";import { Span, SpanExporter } from "@opentelemetry/sdk-trace-base";import { OpenInferenceSimpleSpanProcessor, isOpenInferenceSpan,} from "@arizeai/openinference-vercel";// Eve wraps each turn's OpenInference spans under its own `ai.eve.turn`// workflow span, which in turn hangs off Vercel Workflow `step.execute`// spans that are filtered. Keep `ai.eve.turn` and promote it to the trace// root so the gen_ai spans have a single, un-orphaned parent.const EVE_ROOT_SPAN_NAME = "ai.eve.turn";function isEveSpan(span: Span): boolean { return isOpenInferenceSpan(span) || span.name === EVE_ROOT_SPAN_NAME;}/** * Keeps OpenInference spans plus Eve's `ai.eve.turn` workflow span, promotes * `ai.eve.turn` to root by clearing its parent IDs, and tags it as an `agent` * span — yielding one agent-kinded root per turn with no orphaned spans. */export class RootAwareOpenInferenceProcessor extends OpenInferenceSimpleSpanProcessor { constructor(exporter: SpanExporter) { super({ exporter, spanFilter: isEveSpan }); } onStart(span: Span, parentContext: Context): void { if (span.name === EVE_ROOT_SPAN_NAME) { (span as unknown as { parentSpanId?: string }).parentSpanId = undefined; (span as unknown as { parentSpanContext?: unknown }) .parentSpanContext = undefined; span.setAttribute("openinference.span.kind", "AGENT"); } super.onStart(span, parentContext); }}
Wire it in by replacing the OpenInferenceSimpleSpanProcessor in agent/instrumentation.ts — it carries its own filter, so you no longer pass spanFilter:
import { RootAwareOpenInferenceProcessor } from "./root-aware-processor";spanProcessors: [ new RootAwareOpenInferenceProcessor( new OTLPTraceExporter({ url: "https://otlp.arize.com/v1/traces", headers: { "arize-space-id": process.env.ARIZE_SPACE_ID ?? "", "arize-api-key": process.env.ARIZE_API_KEY ?? "", }, }), ),],
No traces in Arize AX. Confirm the file is exactly agent/instrumentation.ts (Eve discovers it by path), and that ARIZE_SPACE_ID and ARIZE_API_KEY are set in the shell running npm run dev. Enable OpenTelemetry debug logs with export OTEL_LOG_LEVEL=debug and re-run.
Model auth errors. Eve routes models through AI Gateway — set AI_GATEWAY_API_KEY, or run vercel link to use a VERCEL_OIDC_TOKEN. To skip the gateway, switch the agent to a direct provider model (e.g. @ai-sdk/openai with OPENAI_API_KEY). A brand-new AI Gateway key also fails until you add a payment method — the turn errors with GatewayInternalServerError: AI Gateway requires a valid credit card on file to service requests, even if you only plan to use the free credits. Add a card in your Vercel AI Gateway dashboard to unlock them.
Version mismatch. Pin the OpenTelemetry packages to Eve’s @vercel/otel major: @vercel/otel@1.x requires @opentelemetry/*1.x; @vercel/otel@2.x requires 2.x. Mismatches surface as silently missing traces.
gen_ai spans orphaned on the Traces tab. This happens when the spanFilter drops Eve’s ai.eve.turn workflow span (for example, spanFilter: isOpenInferenceSpan alone), leaving the per-step gen_ai spans with no parent. Keep ai.eve.turn in the filter as shown in Setup tracing, or use the root-promoting RootAwareOpenInferenceProcessor for a single clean root per turn.