Skip to main content
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.

Prerequisites

  • Node.js 24+ (Eve’s CLI requires it)
  • An Eve agent project (npx eve@latest init my-agent)
  • An Arize AX account (sign up)
  • An AI Gateway credential for Eve’s model routing — an AI_GATEWAY_API_KEY, or a VERCEL_OIDC_TOKEN pulled with vercel link

Launch Arize AX

  1. Sign in to your Arize AX account.
  2. From Space Settings, copy your Space ID and API Key. You will set them as ARIZE_SPACE_ID and ARIZE_API_KEY below.

Install

In your Eve project, add the Arize OpenInference processor and the OpenTelemetry packages it exports through:
npm install @arizeai/openinference-vercel \
  @vercel/otel \
  @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-proto

Configure credentials

export ARIZE_SPACE_ID="<your-space-id>"
export ARIZE_API_KEY="<your-api-key>"
export AI_GATEWAY_API_KEY="<your-ai-gateway-key>"

Setup tracing

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.ts
import { 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.

Run Eve

Start the Eve dev server, then open a session against the built-in HTTP channel:
npm run dev
The dev server listens on http://127.0.0.1:2000 by default (pass --port to change it). Open a session against the built-in HTTP channel:
curl -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"What is the weather in Brooklyn?"}'
The response returns a continuationToken in the body and an x-eve-session-id header. Stream the session’s lifecycle events to watch the turn complete:
curl http://127.0.0.1:2000/eve/v1/session/<sessionId>/stream

Expected output

{"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"}}

Verify in Arize AX

  1. Open your Arize AX space and select the project named after your agent (the model_id above).
  2. 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.client llm span (prompt, response, token usage), and each tool run is a gen_ai tool 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.
  3. 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.
  4. If no traces appear, see Troubleshooting.
Arize AX trace view of a Vercel Eve agent turn, showing the ai.eve.turn agent root span with nested agent, llm, and tool spans for each step

Span filter

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:
spanFilter: (span) =>
  isOpenInferenceSpan(span) || span.name === "ai.eve.turn",
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.ts
import { 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 ?? "",
      },
    }),
  ),
],

Troubleshooting

  • 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.

Resources

Cookbook: Tracing a Vercel Eve Agent

Eve Observability Docs

OpenInference Vercel Span Processor

Eve Getting Started