Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.voight.xyz/llms.txt

Use this file to discover all available pages before exploring further.

@voightxyz/vercel-ai is an OpenTelemetry SpanExporter that consumes the experimental_telemetry spans the Vercel AI SDK emits natively. Register it once next to @vercel/otel, set experimental_telemetry: { isEnabled: true } on your streamText / generateText / streamObject / generateObject calls, and every call lands in your Voight dashboard with prompts, tokens, tool calls, cache reads, latency, and errors. Same backend and dashboard as @voightxyz/openai + @voightxyz/anthropic. Events from all three packages land side-by-side under the same agent.

Quick setup with the wizard

If your app already imports ai, the lowest-friction install is the wizard from the main SDK:
cd your-app
npx -y @voightxyz/sdk init
It detects ai in your package.json, prompts for a privacy level + Voight key + agent name, validates the key, and writes a ready-to-load instrumentation.ts that registers VoightExporter with @vercel/otel. 30 seconds, zero copy-paste. Full walkthrough at docs.voight.xyz/ai-apps/wizard. Continue below if you’d rather wire it manually.

Why an OTel SpanExporter

The Vercel AI SDK emits OpenTelemetry spans natively when experimental_telemetry is enabled — the same wire format every other LLM-observability tool listed in the Vercel AI SDK docs consumes (Langfuse, Helicone, Phoenix, Braintrust, Datadog, Sentry, Weights & Biases). We follow the same contract so you can:
  • Wire Voight alongside one of those tools via MultiSpanProcessor, or
  • Drop in Voight as the sole observability provider,
with the same code path either way. No vendor lock, no custom middleware.

Install

npm install ai @ai-sdk/openai @vercel/otel @voightxyz/vercel-ai
@voightxyz/vercel-ai declares @opentelemetry/api and @opentelemetry/sdk-trace-base as peer dependencies, and ai as an optional peer. Bring your own provider package (@ai-sdk/openai, @ai-sdk/anthropic, …). Requirements:

Quick start

1. Register the exporter via instrumentation.ts

// instrumentation.ts (Next.js App Router) or any OTel bootstrap.
import { registerOTel } from '@vercel/otel'
import { VoightExporter } from '@voightxyz/vercel-ai'

export function register() {
  registerOTel({
    serviceName: 'my-app',
    traceExporter: new VoightExporter({
      agent: 'production-chat-api',
      privacy: 'standard',
    }),
  })
}

2. Enable telemetry on your LLM calls

import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'

const result = streamText({
  model: openai('gpt-4o-mini'),
  prompt: req.body.prompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: 'stream-text',
  },
})

3. Set VOIGHT_KEY

# .env.local
VOIGHT_KEY=vk_your_key_here
That’s it — every wrapped streamText / generateText / streamObject / generateObject call is captured automatically. Visit your Voight dashboard to see them in real time.

Per-user attribution

For production apps where you want to attribute cost per end-user with one line of code, pass metadata on experimental_telemetry:
const result = streamText({
  model: openai('gpt-4o-mini'),
  prompt: req.body.prompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: 'stream-text',
    metadata: {
      userId: session.user.id,
      plan: session.user.plan,
      org: session.user.org,
    },
  },
})
The exporter lifts every ai.telemetry.metadata.<key> span attribute into the Voight event’s metadata.tags.<key> — which is exactly the shape that drives the per-user spend sub-tab and the per-tag filter pills. The userId value is the only one Voight treats specially (it powers the Users sub-tab). Everything else is filter-only.

Options

OptionTypeDefaultDescription
agentstringprocess.env.VOIGHT_AGENTprocess.env.HOSTNAME'unknown-agent'Stable agent identifier surfaced in the dashboard.
voightApiKeystringprocess.env.VOIGHT_KEYAPI key for the Voight ingest endpoint.
apiBasestringhttps://api.voight.xyzOverride for self-hosted Voight deployments.
privacy'minimal' | 'standard' | 'full''standard'See Privacy.
sessionIdstringAuto UUID v4 per exporter instanceStamps metadata.sessionId on every event the exporter emits. Use to scope a session yourself (per-user, per-conversation).
fetchtypeof fetchglobalThis.fetchOverride for tests / custom transports.
onError(err: unknown) => voidno-opReceives ingest network errors. Helpful during development.

What’s captured

SignalWhere it lands
Model id (with version suffix)model
Base provider (openai, anthropic, …)metadata.provider
Raw provider surface (e.g. openai.responses)metadata.providerSurface
Prompt messagesinput.messages (post-privacy filter)
Response textmetadata.responseText
Token counts (input / output)metadata.tokens
Cache reads (gen_ai.usage.cache_read_input_tokens / ai.usage.cachedInputTokens)metadata.tokens.cache_read
Cache creation (Anthropic Path-A)metadata.tokens.cache_creation
Tool / function calls (normalised to {id, name, arguments})metadata.toolCalls + toolExecuted
Streaming flag (derived from ai.streamText.doStream vs ai.generateText.doGenerate)metadata.streaming
API markermetadata.api: 'vercel-ai'
Source markermetadata.source: 'vercel-ai-sdk'
Trace grouping (auto UUID or explicit)metadata.sessionId
User-supplied telemetry metadatametadata.tags.*
Finish reasonmetadata.finishReason
Latency (ms)durationMs
ErrorserrorMessage + outcome: 'failed'

Privacy

Three levels control how aggressively prompts, responses, and tool arguments are scrubbed before they leave the process. The PII catalogue is the same as @voightxyz/openai and @voightxyz/anthropic.
LevelPrompts & response textTool argumentsTokens / model / cost / latency
minimaldroppeddropped (names kept as tags)always captured
standard (default)PII redacted (emails, phone, credit cards, JWTs, OpenAI / Anthropic / Stripe / GitHub / AWS / Slack keys)PII redactedalways captured
fullrawrawalways captured
For most apps 'standard' is the right balance — useful debugging signal without leaking secrets.

Pairing with another OTel exporter

The exporter coexists with any other SpanExporter via OTel’s MultiSpanProcessor — common when you already ship to Datadog / Honeycomb / Grafana Tempo and want to add Voight as a second sink.
import { registerOTel } from '@vercel/otel'
import { VoightExporter } from '@voightxyz/vercel-ai'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'

registerOTel({
  serviceName: 'my-app',
  traceExporter: [
    new VoightExporter({ agent: 'production-chat-api' }),
    new OTLPTraceExporter({ url: process.env.OTLP_ENDPOINT }),
  ],
})
Each exporter sees the same span batch independently — Voight only consumes spans that carry the GenAI semconv (gen_ai.*) or Vercel (ai.*) attributes, so non-LLM spans (HTTP, DB) are silently skipped from the Voight stream while still reaching your other exporter.

Out of scope (deferred)

  • Direct middleware (voightMiddleware()) for users who prefer a zero-OTel-setup wrap — planned for 0.2.
  • withTrace / log async-context helpers — OTel context propagation already provides equivalent semantics. The helpers may return in 0.2 if real usage shows a gap.
  • Batched / buffered ingest — the per-event POST is sufficient for the workloads we expect at this scale. Batching arrives with real-world failure-mode data to design against.
  • Bedrock / Vertex provider-specific paths — the gen_ai.* normalisation is expected to cover them; will revisit if a user reports a missing attribute.

Source

github.com/Voightxyz/voight-vercel-ai — Apache 2.0.