@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).
Span-shape attributes
Set on the span you want to control. All keys are namespaced underlmnr.span.*.
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.
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:
LLM attributes
Set these on a span wherelmnr.span.type = "LLM".
Provider, model, tokens, cost
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.
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
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
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.
- TypeScript
- Python
gen_ai.*) are part of the OpenTelemetry GenAI semantic conventions; everything under lmnr.* is Laminar-specific.
Anti-patterns
- Setting
lmnr.association.properties.session_idon every span. Set it once on the root; Laminar lifts it onto the trace at ingest. Repeating it everywhere wastes attribute bytes. - Putting
service.nameor app version on each span. These are Resource attributes. Set them on the tracer provider’sResourceso they’re emitted once per batch. - Setting
lmnr.span.type = "EXECUTOR"on application code.EXECUTORis reserved for auto-instrumentation and the evaluations framework. UseDEFAULTorTOOL. - 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. Emitgen_ai.input.messagesandgen_ai.output.messages. - Passing non-primitive attribute values directly. OTel attribute values are limited to
string | number | boolean | string[] | number[] | boolean[]. Forlmnr.span.input,lmnr.span.output, per-message content, or non-primitive metadata values,JSON.stringifyfirst.
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-stringifiedfixed64, enum-name strings) are accepted.
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.