@voightxyz/vercel-ai
v0.1.1
Published
Voight observability for the Vercel AI SDK. OpenTelemetry SpanExporter that ingests `experimental_telemetry` spans from `streamText`/`generateText`/etc. into your Voight dashboard — prompts, tokens, tool calls, cache, latency, errors, all providers normal
Maintainers
Readme
@voightxyz/vercel-ai
0.1.0. First stable release, validated against
vercel/ai-chatbot. Bug reports + feature requests welcome on the issues page.
Voight observability for the Vercel AI SDK. An OpenTelemetry SpanExporter that ingests the experimental_telemetry spans produced by streamText / generateText / streamObject / generateObject — prompts, tokens, tool calls, cache reads, latency, errors — surfaced live in the Voight dashboard.
Same backend and dashboard as @voightxyz/openai + @voightxyz/anthropic. Events from any of the three packages land side-by-side under the same agent.
Why an OTel SpanExporter (not a middleware)
The Vercel AI SDK emits OpenTelemetry spans natively when you flip experimental_telemetry: { isEnabled: true } on a call. That's the same wire format every other LLM-observability tool listed in the Vercel AI SDK docs consumes — Langfuse, Helicone, Arize Phoenix, Braintrust, Datadog, Sentry, Weights & Biases. We follow the same contract so you can:
- Wire Voight alongside one of those tools (
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 has @opentelemetry/api and @opentelemetry/sdk-trace-base as peer dependencies and ai as an optional peer (the exporter reads OTel GenAI semantic-convention attributes, which any OTel-instrumented LLM library can emit). Bring your own provider package (@ai-sdk/openai, @ai-sdk/anthropic, …).
Quick start
Register the exporter in your Next.js app's
instrumentation.ts:// instrumentation.ts import { registerOTel } from '@vercel/otel' import { VoightExporter } from '@voightxyz/vercel-ai' export function register() { registerOTel({ serviceName: 'my-app', traceExporter: new VoightExporter({ agent: 'my-app', // voightApiKey: process.env.VOIGHT_KEY ← read from env by default }), }) }Enable telemetry on each LLM call in your route handlers:
// app/api/chat/route.ts import { openai } from '@ai-sdk/openai' import { streamText } from 'ai' export async function POST(req: Request) { const { messages } = await req.json() const result = streamText({ model: openai('gpt-4o-mini'), messages, experimental_telemetry: { isEnabled: true }, }) return result.toUIMessageStreamResponse() }Set
VOIGHT_KEYin.env.local. That's it — every call is captured automatically. Visit your Voight dashboard to see them in real time.
What's captured
| Signal | Where it lands |
|---|---|
| Model id (request) | model |
| Response model (when different from request) | metadata.responseModel |
| Provider (base, e.g. 'openai') | metadata.provider |
| Provider surface (e.g. 'openai.responses') | metadata.providerSurface (debug) |
| Prompt messages | input.messages |
| Response text | metadata.responseText |
| Token counts (input / output) | metadata.tokens.input / metadata.tokens.output |
| Cache reads | metadata.tokens.cache_read |
| Cache creation (Anthropic ephemeral) | metadata.tokens.cache_creation |
| Tool / function calls | metadata.toolCalls + toolExecuted |
| Streaming flag | metadata.streaming |
| Trace grouping | metadata.sessionId |
| Finish reason | metadata.finishReason |
| Latency (ms) | durationMs |
| Errors | errorMessage + outcome: 'failed' |
Every event carries metadata.source = 'vercel-ai-sdk' so dashboard filters can isolate Vercel AI events from those emitted by the direct wrappers.
Options
| Option | Type | Default | Notes |
|---|---|---|---|
| voightApiKey | string | process.env.VOIGHT_KEY | Required to ingest. Missing key → exporter no-ops with a one-time console warning. |
| apiBase | string | 'https://api.voight.xyz' | Override for self-hosted Voight. |
| agent | string | env VOIGHT_AGENT → HOSTNAME → 'unknown-agent' | Stable label that groups events in the dashboard. |
| privacy | 'minimal' \| 'standard' \| 'full' | 'standard' | Capture aggressiveness — see below. |
| sessionId | string | auto UUID v4 (per exporter instance) | Stamped on metadata.sessionId of every event. |
| fetch | typeof fetch | globalThis.fetch | Inject a custom client (testing, proxying). |
| onError | (err: unknown) => void | () => {} | Surface ingest failures during development. |
Privacy levels
'minimal'— model, tokens, latency, errors, tool names. Zero prompt content, zero response content, zero tool arguments.'standard'(default) — adds prompts/responses/tool-arguments scrubbed of common PII (emails, phones, credit cards, API keys, JWTs). 12 patterns + Luhn-validated cards. Same catalogue as@voightxyz/openai/@voightxyz/anthropic.'full'— everything raw, no redaction. Useful in local dev or staging.
Pairing with other exporters
The Vercel AI SDK supports a single traceExporter per OTel registration. To run Voight alongside another provider, wire a MultiSpanProcessor:
import { registerOTel } from '@vercel/otel'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { VoightExporter } from '@voightxyz/vercel-ai'
import { LangfuseExporter } from 'langfuse-vercel'
registerOTel({
serviceName: 'my-app',
spanProcessors: [
new BatchSpanProcessor(new VoightExporter({ agent: 'my-app' })),
new BatchSpanProcessor(new LangfuseExporter()),
],
})Each exporter sees the same span batch independently.
Status
| Capability | Status |
|---|---|
| streamText / generateText capture | ✅ Verified (0.1.0) |
| streamObject / generateObject capture | ✅ Same code path (no extra config) |
| OpenAI provider attribution | ✅ |
| Anthropic provider attribution | ✅ |
| Tool calls (OpenAI + Anthropic) | ✅ |
| Cache tokens (OpenAI cached_input, Anthropic cache_read + cache_creation) | ✅ |
| Privacy fan-out (3 levels) | ✅ |
| Per-request withTrace / log helpers | Deferred — OTel context already provides equivalent semantics; the helpers may return in 0.2 if real usage shows a gap. |
| Direct middleware (voightMiddleware()) | Deferred — planned for 0.2 for users who want a 1-line wrap without OTel setup. |
Links
License
Apache 2.0. See LICENSE.
