OTel GenAI Message Ingestion#
Overview#
GenAI-related OpenTelemetry attributes are parsed inside OtelIngestionProcessor.extractInputAndOutput() , which converts OTel span attributes and events into Langfuse input/output fields on observations. The method uses a priority-based fallback chain: each framework's attributes are tried in sequence, and the first match wins.
extractInputAndOutput — Priority Chain#
The chain checks these sources in order :
| Priority | Condition | Source attributes |
|---|---|---|
| 1 | Langfuse SDK | langfuse.observation.input / .output |
| 2 | Genkit (genkit-tracer scope) | genkit:input / genkit:output |
| 3 | Vercel AI SDK (ai scope) | ai.prompt.messages, ai.response.text, etc. |
| 4 | OTel GenAI span event (v1.37+) | gen_ai.client.inference.operation.details event attributes |
| 5 | OTel GenAI span events (per-message) | gen_ai.{system,user,assistant,tool}.message, gen_ai.choice |
| 6 | Legacy Semantic Kernel events | gen_ai.content.prompt / .completion |
| 7 | Google Vertex AI ADK | gcp.vertex.agent.llm_request / .llm_response |
| 8 | Logfire | prompt, all_messages_events, or events array |
| 9 | LiveKit | lk.input_text / lk.chat_ctx / lk.response.text |
| 10 | MLFlow | mlflow.spanInputs / mlflow.spanOutputs |
| 11 | TraceLoop | traceloop.entity.input / .output |
| 12 | SmolAgents | input.value / output.value |
| 13 | Pydantic / Pipecat | input / output |
| 14 | Pydantic AI agent span | pydantic_ai.all_messages / final_result |
| 15 | Pydantic AI tool spans | tool_arguments / tool_response |
| 16 | TraceLoop dotted attrs | gen_ai.prompt.* / gen_ai.completion.* |
| 17 | OpenInference | llm.input_messages.* / llm.output_messages.* |
| 18 | OTel GenAI attributes | gen_ai.input.messages / gen_ai.output.messages |
| 19 | OTel tool calls | gen_ai.tool.call.arguments / .result |
Attribute keys used by the winning path are removed from filteredAttributes before they are stored in metadata.attributes .
OTel GenAI Span Events (Standard Path)#
When a span carries standard OTel GenAI per-message span events, extractInputAndOutput builds a message array from them :
- Input events match names
gen_ai.system.message,gen_ai.user.message,gen_ai.assistant.message,gen_ai.tool.message. Each event becomes{ role: "<role>", ...eventAttributes }whereroleis derived by strippinggen_ai.and.messagefrom the event name. - Output events match
gen_ai.choice. If there is exactly one choice event, a single object is returned; otherwise an array.
Each event's attributes are deserialized from protobuf via convertValueToPlainJavascript() .
gen_ai.client.inference.operation.details (v1.37+ conventions, PR #14930): OTel GenAI semantic conventions v1.37+ moved prompts/completions to an opt-in span event named gen_ai.client.inference.operation.details, which carries gen_ai.input.messages, gen_ai.output.messages, and gen_ai.system_instructions as event attributes rather than span attributes. Emitters like Hindsight use this convention. PR #14930 added extraction from this event — the processor filters for this event name, converts its attributes to plain JS, and then delegates to the same gen_ai.input/output.messages parsing (including gen_ai.system_instructions prepending). This path is inserted before the older per-message events in the chain, so v1.37+ emitters are handled first.
gen_ai.system_instructions — Multi-Format Serialization#
The attribute gen_ai.system_instructions carries system prompts separately from the message array. Two code paths invoke prependSystemInstructions() :
- Pydantic AI agent spans — when
pydantic_ai.all_messagesis the input andgen_ai.system_instructionsis present . - Generic OTel path — when
gen_ai.input.messagesis the input andgen_ai.system_instructionsis present .
prependSystemInstructions behavior :
- Parses
inputas JSON if it is a string. - No-ops if the array already contains a
{ role: "system" }message — preventing duplication. - Converts
systemInstructionsto acontentstring:- If already an array (e.g., Google Gemini sends system instructions as
[{ content: "..." }]), joinscontentfields with"\n". - Otherwise, stringifies to a plain string.
- If already an array (e.g., Google Gemini sends system instructions as
- Prepends
{ role: "system", content }and returns in the same format (string-encoded JSON if input was a string; array otherwise).
This was introduced in PR #12442 .
Dotted-Path Message Reconstruction#
For providers using the OpenInference convention (Agno, BeeAI, etc.), messages arrive as flattened dotted keys such as llm.input_messages.0.role, llm.input_messages.0.content, etc. convertKeyPathToNestedObject() reconstructs these into a nested array or object. The same function handles gen_ai.prompt.* / gen_ai.completion.* for TraceLoop. It detects whether numeric index segments are present to decide whether to build an array or a plain object.
Key Source Files#
| File | Purpose |
|---|---|
OtelIngestionProcessor.ts | All GenAI input/output extraction logic |
attributes.ts | LangfuseOtelSpanAttributes enum — all Langfuse-specific OTel attribute keys |
Related knowledge base articles:
- OTel Attribute Serialization — protobuf-to-JS conversion details and
filteredAttributesstringify behavior - OTel Ingestion and Trace Hierarchy — overall pipeline, trace deduplication, parent-child relationships
- OTel Token Usage Processing —
extractUsageDetailsand token normalization