ModelRig Docs Leaderboard Console ↗ GitHub Sign up

Recipe: LiveKit Agents

LiveKit Agents can export OpenTelemetry traces of a voice session. Give it an OTLP exporter pointed at ModelRig and attach your conversation id as span metadata.

Mint a key in the org you want the data in. Sign in at https://app.modelrig.ai, check the organization name in the header badge, and create a key on the API keys page. The rig_sk_ secret is shown once. One key = one org: every row the key exports lands in the organization it was minted in, whichever organization you happen to be viewing in the console. Set it on every service, worker and cron that creates a rig; your provider keys stay where they are.


# Wherever your app reads configuration: .env.local, your platform's env vars, a secret manager. Never in code.
MODELRIG_API_KEY=rig_sk_…

Point your OTLP exporter at ModelRig. ModelRig accepts OpenTelemetry traces at POST /v1/otlp/v1/traces on the ingest host (https://api.modelrig.ai unless you were given another), over OTLP/HTTP as protobuf or JSON, optionally gzipped. Authenticate with a rig_sk_ key that carries the ingest scope; every run and step lands in that key's organization. Each trace becomes one run with one attested step per span: model, provider, operation, timing, status and token usage. Prompts, completions and tool arguments are not read.


# Standard OpenTelemetry exporter variables — most OTel SDKs read them as-is.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.modelrig.ai/v1/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20rig_sk_…
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf

Minimal config

Untested config — verify against your LiveKit Agents version.


from livekit.agents.telemetry import set_tracer_provider
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

def setup_tracing(conversation_id: str) -> None:
    provider = TracerProvider()
    provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))  # reads OTEL_EXPORTER_OTLP_TRACES_*
    set_tracer_provider(provider, metadata={"gen_ai.conversation.id": conversation_id})

LiveKit's spans are not invoke_agent spans, so a session is a run with its model calls as named steps — it is numbered into turns only if you open an invoke_agent span per user turn yourself (the attributes are in the helper below). Voice transport, latency and duplex timing are out of scope; this is post-call visibility.

Set gen_ai.conversation.id to your conversation id. A trace becomes a conversation when one of its spans carries gen_ai.conversation.id and it has invoke_agent spans. Each top-level invoke_agent span is a turn, in start order, and every model call under it becomes a step turn:<n>.<k>; the run is marked as a conversation and its episode key is your conversation id. Turns are numbered within a trace: when each user message arrives as its own trace, each shows as its own one-turn conversation run, and the shared conversation id groups them. If your framework does not emit an invoke_agent span itself, wrap each user message in one.


import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("support-bot");

// One span per user message. The model calls your framework makes inside it
// become this conversation's turn steps; the conversation id joins the runs.
export function turn<T>(conversationId: string, fn: () => Promise<T>): Promise<T> {
  return tracer.startActiveSpan(
    "invoke_agent support-bot",
    { attributes: { "gen_ai.operation.name": "invoke_agent", "gen_ai.conversation.id": conversationId } },
    async (span) => {
      try {
        return await fn();
      } finally {
        span.end();
      }
    },
  );
}

What shows up

What does not show up

More on the OTLP door: https://modelrig.dev/otlp-ingest.html.