@runagain/ai-sdk telemetry
bridge.
Why the bridge? AI SDK v5 and earlier wrote OpenTelemetry spans
directly when
experimental_telemetry was enabled. v7 changed this: the
SDK no longer emits OTel spans itself — it publishes telemetry events to an
in-process channel and expects a registered telemetry integration to turn
them into spans. @runagain/ai-sdk is that integration; it converts each call
into a gen_ai.* span RunAgain reads. Without it (on v7) you get a trace with
only HTTP spans and no model/tokens/content.Install
@opentelemetry/api is a peer of both @vercel/otel and @runagain/ai-sdk —
install it explicitly (Vercel’s own setup does too).Configure
.env.local
Register the exporter and the bridge
instrumentation.ts
Not on Next.js / In a serverless handler, spans are batched on a ~2s timer —
@vercel/otel? In a plain Node service, skip registerOTel and
call initRunAgain() from runagain once at startup — it registers the tracer provider
and the RunAgain exporter for you (reads RUNAGAIN_API_KEY / RUNAGAIN_INGEST_URL).
Still call registerTelemetry(runAgainAiTelemetry) on AI SDK v7. runAgainAttributes /
initRunMetadata are exported from runagain as well as @runagain/ai-sdk.await ra.flush()
before you return the response (or wrap it in the platform’s waitUntil) so the
function doesn’t freeze before they’re sent. @vercel/otel handles this for you, so
it’s only the plain-initRunAgain path that needs the explicit flush.Enable telemetry on your calls
app/api/chat/route.ts
functionId is surfaced as gen_ai.agent.name on the model/embedding span, so you
can attribute individual calls to a named agent. (Session/user/tags identity is
separate — attach it with runAgainAttributes(...), below.)embed / embedMany work the same way — opt each call in and you’ll get
embeddings spans with token usage.
Attach identity & custom metadata
Rather than hand-typing OTel attribute keys,@runagain/ai-sdk ships typed helpers that emit the
canonical keys RunAgain normalizes — session,
user, agent, environment, version, release, tags, and a free-form metadata bag.
setRunMetadata({ session, user }) writes the same attributes onto the
active OpenTelemetry span, and runAgainAttributes(...) returns a plain attribute map you can pass
to span.setAttributes(...). All fields are optional — only what you set is sent.
Run
chat <model> and embeddings <model> spans (scope
@runagain/ai-sdk) with model, tokens, and — when recording is on — the
prompt and response.
Failed and aborted calls show up as errors, not gaps. When a model call throws
or a stream is aborted, the AI SDK fires no completion event — the bridge closes the
span from the SDK’s error/abort callbacks instead, marks it as an error, and records
the exception. So a rate limit or timeout appears as a failed span rather than
silently disappearing. (A single
embedMany failure closes every parallel embedding
span under that call.)