Skip to main content

Documentation Index

Fetch the complete documentation index at: https://laminar.sh/docs/llms.txt

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

This page is the reference for setting Laminar-recognized OpenTelemetry attributes from a non-SDK exporter: foreign-language SDKs, browser code, custom instrumentations, internal trace adapters. If you are using @lmnr-ai/lmnr or lmnr (Python), the SDK already sets these attributes for you. Use this page when you ship OTLP directly to Laminar without the SDK and need the literal wire keys.

How attributes flow into Laminar

Spans arrive at Laminar as standard OTLP. Laminar reads three layers:
  • Resource attributes on the tracer provider (service.name, service.version, deployment.environment).
  • Span attributes on each span: span shape, LLM telemetry, GenAI semconv content.
  • Trace-association attributes, which Laminar lifts off any span in the trace and stores once on the trace record (session, user, metadata, tags, trace type).
Trace-association attributes use an “any-span wins” merge: the first non-empty value Laminar sees during ingest is what the trace gets. The most predictable place to set them is the root span, set them once.

Span-shape attributes

Set on the span you want to control. All keys are namespaced under lmnr.span.*.
KeyTypeWhat it does
lmnr.span.typestringSpan type: DEFAULT, LLM, or TOOL. Drives transcript rendering and cost rollups. EXECUTOR, EVALUATOR, HUMAN_EVALUATOR, EVALUATION, CACHED are reserved for the evaluations framework; do not set them on application spans.
lmnr.span.inputstring (JSON-stringified)Populates the Input panel and the transcript-view inline preview. Stringify non-primitive values before setting.
lmnr.span.outputstring (JSON-stringified)Populates the Output panel and inline preview. Stringify non-primitive values.
lmnr.span.pathstring arrayOrdered span-name path from the trace root to this span (inclusive). Drives transcript rendering: subagent grouping, parent-chain reconstruction for partial traces, and the path-based clustering that powers the conversation view. The Laminar SDKs auto-fill this. Custom exporters either leave it unset (Laminar falls back to the OTel parent_span_id chain, which gives a less rich transcript) or fill it themselves to mirror what the SDK would emit.
lmnr.span.ids_pathstring arraySpan-id path from the trace root to this span (inclusive), aligned 1:1 with lmnr.span.path. Same load-bearing role as lmnr.span.path: set both or neither.
lmnr.span.parent_pathstring arrayPath of the parent span (lmnr.span.path minus the last element). Optional; only useful if you set the path yourself.
lmnr.span.parent_ids_pathstring arraySpan-id form of lmnr.span.parent_path. Optional; pair with lmnr.span.parent_path.
lmnr.span.instrumentation_sourcestringFree-form label identifying the emitter (e.g. "javascript", "my-internal-adapter"). Surfaces in span metadata.
lmnr.span.sdk_versionstringVersion of the emitter. Optional.
lmnr.span.language_versionstringRuntime version of the emitter. Optional.
lmnr.span.human_evaluator_optionsstring (JSON)Used by HumanEvaluator(). See evaluations.
Setting lmnr.span.type = "LLM" is required for LLM-specific UI and cost rollups to render at all. A span with LLM type but no gen_ai.* content shows up in the LLM section but with an empty conversation panel. See LLM attributes below.

Trace-association attributes

These describe the trace, not the individual span. Set them once on the root span: Laminar lifts them onto the trace record at ingest time. Setting them on every span works but is wasteful.
KeyTypeWhat it does
lmnr.association.properties.session_idstringGroups traces into a session.
lmnr.association.properties.user_idstringAttaches a user id to the trace.
lmnr.association.properties.rollout_session_idstringUsed by the agent debugger for rollout grouping.
lmnr.association.properties.trace_typestringTrace classification ("DEFAULT" / "EVALUATION"). Most callers leave this alone.
lmnr.association.properties.tagsstring arraySpan-level tags. Tags are unioned across every span in the trace (not root-only) and the union becomes the trace tag set.
lmnr.association.properties.metadata.<key>primitive or JSON-stringifiedTrace metadata: one attribute per key. Primitives (string, number, boolean, arrays of those) pass through; complex values must be JSON.stringifyd before set.
The metadata key shape mirrors what the Laminar SDK emits. To attach {environment: "prod", featureFlag: "new-algo", abVariant: {bucket: 3}} to a trace, set three attributes on the root span:
lmnr.association.properties.metadata.environment   = "prod"
lmnr.association.properties.metadata.featureFlag   = "new-algo"
lmnr.association.properties.metadata.abVariant     = "{\"bucket\":3}"

LLM attributes

Set these on a span where lmnr.span.type = "LLM".

Provider, model, tokens, cost

ConceptWire keyExample
Providergen_ai.system"openai"
Request modelgen_ai.request.model"gpt-5-mini"
Response modelgen_ai.response.model"gpt-5-mini-2025-04-01"
Input tokensgen_ai.usage.input_tokens1284
Output tokensgen_ai.usage.output_tokens162
Total tokensllm.usage.total_tokens1446
Input costgen_ai.usage.input_cost0.0019
Output costgen_ai.usage.output_cost0.0024
Total costgen_ai.usage.cost0.0043
Cost is calculated from gen_ai.system + gen_ai.request.model + token counts. Without all three, cost stays at zero. Setting the explicit gen_ai.usage.*_cost keys overrides the calculated values. See LLM cost tracking for the supported provider names.

Prompts and completions

Use the OTel GenAI semconv message-array form. This is the convention Laminar recommends for new exporters.
KeyDescription
gen_ai.input.messagesJSON-stringified [{role, parts: [...]}] array.
gen_ai.output.messagesJSON-stringified [{role, parts: [...]}] array.
gen_ai.system_instructionsSystem prompt, prepended to the input messages as a synthetic system entry.
Each parts entry is one of {type: "text", content}, {type: "thinking", content}, {type: "tool_call", id, name, arguments}, {type: "tool_call_response", id, response}, {type: "uri", uri}, {type: "blob", blob, mimeType}. Laminar preserves the message shape end-to-end and the frontend renders each part inline.
The indexed gen_ai.prompt.{i}.* / gen_ai.completion.{i}.* shape (older OpenLLMetry / OpenInference convention) is still ingested for backwards compatibility but is deprecated. New exporters should emit the semconv form above.

Tool definitions

KeyDescription
gen_ai.tool.definitionsJSON-stringified array of the tools the model was given for this call. Each entry is {type, function: {name, description, parameters}} (OpenAI shape) or the provider-equivalent shape with name / description / input_schema. Renders as the tools dropdown on the LLM span.
The older llm.request.functions.{i}.{name,description,parameters} indexed form is still ingested for backwards compatibility but is deprecated. New exporters should emit gen_ai.tool.definitions.

Where to put each attribute

ScopeOTel placementUse for
Process-wideResource on the tracer providerservice.name, service.version, deployment.environment, app-wide constants
Trace-wideRoot span, via lmnr.association.properties.*session, user, tags, metadata
Per-span, known at creationtracer.startSpan(name, { attributes })lmnr.span.type, lmnr.span.input, gen_ai.system, gen_ai.request.model
Per-span, known after work runsspan.setAttributes({...})lmnr.span.output, gen_ai.response.model, token counts
Resource-level attributes are sent once per batch and apply to every span in that batch; putting service.name on each span instead is wasteful and harder to filter. For trace-association attributes, root-span placement is simpler than every-span placement and produces the same result.

Worked example: a minimal correct trace without the SDK

The example below uses only @opentelemetry/api and @opentelemetry/sdk-trace-*, no @lmnr-ai/lmnr import. The Python snippet uses the same plain OTel APIs. Both build the same trace shape: an outer agent.run span with session metadata, a child llm.chat LLM span with full GenAI attributes, and a child tool.execute TOOL span.
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
import { resourceFromAttributes } from "@opentelemetry/resources";
import {
  NodeTracerProvider,
  BatchSpanProcessor,
} from "@opentelemetry/sdk-trace-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: "my-agent",
  }),
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: "https://api.lmnr.ai:443/v1/traces",
        headers: {
          Authorization: `Bearer ${process.env.LMNR_PROJECT_API_KEY}`,
        },
      }),
    ),
  ],
});
provider.register();

const tracer = trace.getTracer("my-agent", "0.1.0");

const root = tracer.startSpan("agent.run", {
  attributes: {
    // Laminar-specific
    "lmnr.span.type": "DEFAULT",
    "lmnr.span.input": JSON.stringify({ goal: "book a flight to NYC" }),

    // Trace-association: set ONCE on the root span
    "lmnr.association.properties.session_id": "sess-9f21",
    "lmnr.association.properties.user_id": "u_42",
    "lmnr.association.properties.tags": ["beta", "internal"],
    "lmnr.association.properties.metadata.environment": "production",
    "lmnr.association.properties.metadata.region": "us-west",
  },
});

await context.with(trace.setSpan(context.active(), root), async () => {
  // Child LLM span with the full required GenAI set.
  const llm = tracer.startSpan("llm.chat", {
    attributes: {
      "lmnr.span.type": "LLM",
      // GenAI semconv (OTel-defined keys)
      "gen_ai.system": "openai",
      "gen_ai.request.model": "gpt-5-mini",
      "gen_ai.input.messages": JSON.stringify([
        {
          role: "user",
          parts: [{ type: "text", content: "Find me a flight to NYC tomorrow." }],
        },
      ]),
    },
  });
  // ... call your provider, capture the response ...
  llm.setAttributes({
    "gen_ai.response.model": "gpt-5-mini-2025-04-01",
    "gen_ai.usage.input_tokens": 18,
    "gen_ai.usage.output_tokens": 42,
    "gen_ai.output.messages": JSON.stringify([
      {
        role: "assistant",
        parts: [{ type: "text", content: "I found 3 flights..." }],
      },
    ]),
    "lmnr.span.output": JSON.stringify({
      flights: [{ id: "AA101" }, { id: "DL202" }, { id: "UA303" }],
    }),
  });
  llm.setStatus({ code: SpanStatusCode.OK });
  llm.end();

  // Child TOOL span: arguments and return value go on input/output.
  const tool = tracer.startSpan("search_flights", {
    attributes: {
      "lmnr.span.type": "TOOL",
      "lmnr.span.input": JSON.stringify({
        origin: "SFO",
        destination: "JFK",
        date: "2026-05-19",
      }),
    },
  });
  // ... call your tool ...
  tool.setAttribute(
    "lmnr.span.output",
    JSON.stringify([{ id: "AA101", price: 412.5 }]),
  );
  tool.setStatus({ code: SpanStatusCode.OK });
  tool.end();
});

root.setStatus({ code: SpanStatusCode.OK });
root.end();
await provider.forceFlush();
GenAI keys (gen_ai.*) are part of the OpenTelemetry GenAI semantic conventions; everything under lmnr.* is Laminar-specific.

Anti-patterns

  • Setting lmnr.association.properties.session_id on every span. Set it once on the root; Laminar lifts it onto the trace at ingest. Repeating it everywhere wastes attribute bytes.
  • Putting service.name or app version on each span. These are Resource attributes. Set them on the tracer provider’s Resource so they’re emitted once per batch.
  • Setting lmnr.span.type = "EXECUTOR" on application code. EXECUTOR is reserved for auto-instrumentation and the evaluations framework. Use DEFAULT or TOOL.
  • Setting LLM token counts without gen_ai.system + gen_ai.request.model. Cost stays at zero. The provider name and request model are required to look up pricing.
  • Setting lmnr.span.type = "LLM" but omitting prompt and completion attributes. The span renders as an LLM call but the conversation panel is empty. Emit gen_ai.input.messages and gen_ai.output.messages.
  • Passing non-primitive attribute values directly. OTel attribute values are limited to string | number | boolean | string[] | number[] | boolean[]. For lmnr.span.input, lmnr.span.output, per-message content, or non-primitive metadata values, JSON.stringify first.

Transports for /v1/traces

The Laminar cloud (api.lmnr.ai) and self-hosted backends accept three OTLP transports:
  • OTLP/gRPC on :8443 (cloud) / :8001 (self-hosted): the recommended path.
  • OTLP/HTTP+protobuf on :443 (cloud) / :8000 (self-hosted): Content-Type: application/x-protobuf.
  • OTLP/HTTP+JSON on :443 (cloud) / :8000 (self-hosted): Content-Type: application/json. Useful for browser SDKs and other runtimes that don’t have a protobuf encoder available; spec quirks (hex or base64 IDs, decimal-stringified fixed64, enum-name strings) are accepted.
Pick whichever your runtime supports best. Attribute semantics on the wire are identical across transports.

What’s next

OpenTelemetry transport setup

Endpoints, ports, headers, and the gRPC vs HTTP comparison.

Span types

What DEFAULT, LLM, and TOOL render to in the transcript view.

LLM cost tracking

Required gen_ai.* attributes and supported provider names.

Metadata

Trace-level metadata, including the wire key for non-SDK exporters.