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 usingDocumentation Index
Fetch the complete documentation index at: https://laminar.sh/docs/llms.txt
Use this file to discover all available pages before exploring further.
@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.*.
| Key | Type | What it does |
|---|---|---|
lmnr.span.type | string | Span 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.input | string (JSON-stringified) | Populates the Input panel and the transcript-view inline preview. Stringify non-primitive values before setting. |
lmnr.span.output | string (JSON-stringified) | Populates the Output panel and inline preview. Stringify non-primitive values. |
lmnr.span.path | string array | Ordered 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_path | string array | Span-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_path | string array | Path of the parent span (lmnr.span.path minus the last element). Optional; only useful if you set the path yourself. |
lmnr.span.parent_ids_path | string array | Span-id form of lmnr.span.parent_path. Optional; pair with lmnr.span.parent_path. |
lmnr.span.instrumentation_source | string | Free-form label identifying the emitter (e.g. "javascript", "my-internal-adapter"). Surfaces in span metadata. |
lmnr.span.sdk_version | string | Version of the emitter. Optional. |
lmnr.span.language_version | string | Runtime version of the emitter. Optional. |
lmnr.span.human_evaluator_options | string (JSON) | Used by HumanEvaluator(). See evaluations. |
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.| Key | Type | What it does |
|---|---|---|
lmnr.association.properties.session_id | string | Groups traces into a session. |
lmnr.association.properties.user_id | string | Attaches a user id to the trace. |
lmnr.association.properties.rollout_session_id | string | Used by the agent debugger for rollout grouping. |
lmnr.association.properties.trace_type | string | Trace classification ("DEFAULT" / "EVALUATION"). Most callers leave this alone. |
lmnr.association.properties.tags | string array | Span-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-stringified | Trace metadata: one attribute per key. Primitives (string, number, boolean, arrays of those) pass through; complex values must be JSON.stringifyd before set. |
{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
| Concept | Wire key | Example |
|---|---|---|
| Provider | gen_ai.system | "openai" |
| Request model | gen_ai.request.model | "gpt-5-mini" |
| Response model | gen_ai.response.model | "gpt-5-mini-2025-04-01" |
| Input tokens | gen_ai.usage.input_tokens | 1284 |
| Output tokens | gen_ai.usage.output_tokens | 162 |
| Total tokens | llm.usage.total_tokens | 1446 |
| Input cost | gen_ai.usage.input_cost | 0.0019 |
| Output cost | gen_ai.usage.output_cost | 0.0024 |
| Total cost | gen_ai.usage.cost | 0.0043 |
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.| Key | Description |
|---|---|
gen_ai.input.messages | JSON-stringified [{role, parts: [...]}] array. |
gen_ai.output.messages | JSON-stringified [{role, parts: [...]}] array. |
gen_ai.system_instructions | System prompt, prepended to the input messages as a synthetic system entry. |
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
| Key | Description |
|---|---|
gen_ai.tool.definitions | JSON-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
| Scope | OTel placement | Use for |
|---|---|---|
| Process-wide | Resource on the tracer provider | service.name, service.version, deployment.environment, app-wide constants |
| Trace-wide | Root span, via lmnr.association.properties.* | session, user, tags, metadata |
| Per-span, known at creation | tracer.startSpan(name, { attributes }) | lmnr.span.type, lmnr.span.input, gen_ai.system, gen_ai.request.model |
| Per-span, known after work runs | span.setAttributes({...}) | lmnr.span.output, gen_ai.response.model, token counts |
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.
