fix(otel): emit gen_ai.input.messages and gen_ai.output.messages from the genai mapper - #30431
fix(otel): emit gen_ai.input.messages and gen_ai.output.messages from the genai mapper#30431mateo-berri wants to merge 3 commits into
Conversation
… the genai mapper The canonical GenAI mapper stamped request params, usage, cost, and tools but never mapped messages_in or choices_out, so gen_ai.input.messages and gen_ai.output.messages were absent even with OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT set to span_and_event. Every vendor mapper (openinference, langfuse, weave, langtrace) already serialized this content, leaving the canonical vocabulary the only one without prompt/response bodies. Serialize both into the OTel GenAI message schema (role + typed parts; finish_reason on output) as a JSON string, the convention's documented span fallback. Text, tool_call, and tool_call_response parts are covered, with tool-call arguments parsed back into structured form. Content stays gated upstream: messages_in/choices_out arrive empty unless capture is on, so the new attributes only appear when an operator opts in.
Greptile SummaryFixes
Confidence Score: 5/5Safe to merge — the change is strictly additive, gated behind the existing content-capture flag, and uses the shared json_or_none helper to prevent any serialization failure from dropping a whole span. The new code paths are purely additive: attributes are only stamped when messages_in/choices_out are non-empty (i.e., content capture is already opted in), and the serialization is fully guarded by json_or_none with a str fallback. The refactored _llm_call preserves all previously-emitted attributes via the same collect/drop_none pipeline. Ten new tests exercise the key scenarios including edge cases and the capture-off path, and no existing tests were weakened. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/integrations/otel/mappers/genai.py | Adds gen_ai.input.messages and gen_ai.output.messages attribute population via _BLOB_ATTRS and helper functions; refactors _llm_call to dict-spread; serialization uses json_or_none for safe fallback |
| tests/test_litellm/integrations/otel/test_otel_v2_components.py | Adds 10 regression tests covering parts schema, finish_reason, capture-off omission, multimodal text-only extraction, tool calls with parsed arguments, tool results, legacy text field, role default, and non-serializable content resilience |
Reviews (3): Last reviewed commit: "fix(otel): emit text-completion output u..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Route the new gen_ai.input.messages / gen_ai.output.messages serialization through the shared json_or_none helper instead of a bare json.dumps, matching the defensive pattern the other mapper utilities already use. A stray non-serializable value in a message field is now stringified rather than raising a TypeError up through collect and dropping the whole span. Add regression tests for the unparseable / non-string tool-call arguments passthrough, the choice-without-message default role, and a non-serializable tool result surviving serialization.
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Text completion output missing
- Already fixed in commit 636a3f2 on this branch: _output_message now falls back to _text_parts(choice.get("text")) when no message body is present, covering legacy /completions choices, and a regression test was added.
You can send follow-ups to the cloud agent here.
Legacy /completions choices expose the completion on a top-level text field rather than a nested message, so _output_message produced empty parts for them. Fall back to the choice text when there is no message.
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 636a3f2. Configure here.
Relevant issues
Reported by a user testing OpenTelemetry V2 on 1.88.1 with console export: with
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_and_event, the canonical genai mapper never producedgen_ai.input.messagesorgen_ai.output.messages, while other mappers such asopeninferencedidLinear ticket
Pre-Submission checklist
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewWhat was wrong
GenAIMapperis the always-active, canonical OTel GenAI vocabulary. It stamped request params, usage, cost, finish reasons, and tool definitions, but it never readmessages_inorchoices_out. So even with content capture explicitly enabled, the two semconv attributes that carry the actual prompt and completion bodies,gen_ai.input.messagesandgen_ai.output.messages, were simply absent.This was the only mapper missing message content.
openinference,langfuse,weave, andlangtraceall serializedmessages_in/choices_out, which is why the user saw the content show up in those vocabularies but not in the canonical one. The keys themselves already existed insemconv.py(GenAI.INPUT_MESSAGES/GenAI.OUTPUT_MESSAGES); nothing ever populated them.The fix
GenAIMapper._llm_callnow serializes both message lists into the OpenTelemetry GenAI message schema: each message is{"role", "parts": [...]}, where parts are typed fragments (text,tool_call,tool_call_response), and output messages carry the choice'sfinish_reason. Spans cannot hold nested structures, so the list is serialized to a JSON string, which is the convention's documented fallback for span attributes. Tool-callargumentsblobs are parsed back from their JSON-string form into structured objects, falling back to the raw value if they are not valid JSON.Serialization goes through the shared
json_or_nonehelper (the same one the other mapper utilities use), so a stray non-serializable value in a message field is stringified rather than raising aTypeErrorup throughcollectand dropping the whole span.Chat choices carry the completion under
message, but legacy/completionschoices expose it as a top-leveltextstring with nomessage; output serialization falls back to thattextso text-completion responses still produce a populatedpartslist instead of an empty one.Content stays gated exactly as before:
messages_inandchoices_outare populated only whencapture_span_contentis on (span_only/span_and_event); otherwise they arrive as empty tuples and the new attributes resolve to nothing. An operator must still opt in before any prompt or completion body leaves the process.Example
gen_ai.output.messagesvalue for a tool-calling turn:[{"role":"assistant","parts":[{"type":"tool_call","id":"call_42","name":"get_weather","arguments":{"city":"Paris"}}],"finish_reason":"tool_calls"}]Screenshots / Proof of Fix
Run a proxy with content capture on and console export, then send one chat request and read the printed span:
In the console span output, confirm the two attributes are now present (they were absent before this change):
grep -E "gen_ai.input.messages|gen_ai.output.messages" litellm.logYou should see the request and response serialized under both keys, for example
gen_ai.input.messages->[{"role":"user","parts":[{"type":"text","content":"Reply with the single word: sunny"}]}]andgen_ai.output.messages->[{"role":"assistant","parts":[{"type":"text","content":"sunny"}],"finish_reason":"stop"}]Type
🐛 Bug Fix
Changes
litellm/integrations/otel/mappers/genai.py: add a_BLOB_ATTRStable that mapsgen_ai.input.messages/gen_ai.output.messages, plus pure helper functions that convert OpenAI-format chat messages and response choices into the OTel GenAIrole/partsschema, serialized throughjson_or_none._llm_callis reshaped into a no-mutation dict merge (matching the sibling vendor mappers), with tool-definition stamping pulled into a small_toolshelper. Output serialization falls back to a choice's top-leveltextfield so legacy/completionsresponses, which have no nestedmessage, still emit a populatedpartslist.tests/test_litellm/integrations/otel/test_otel_v2_components.py: regression tests covering input/output message serialization, the parts schema,finish_reasonon output, multimodal text-only extraction, assistant tool calls with parsed arguments, tool-result responses, the capture-off path where both attributes are omitted, non-string / unparseable tool-call arguments passthrough, the choice-without-message default role, text-completion output drawn from thetextfield, and a non-serializable tool result surviving serialization.Note
Medium Risk
When message content capture is enabled, prompts and completions are written to span attributes (PII exposure to observability backends); behavior is gated upstream and serialization is hardened with json_or_none.
Overview
Fixes the canonical GenAIMapper so LLM spans include
gen_ai.input.messagesandgen_ai.output.messageswhenmessages_in/choices_outare populated (content capture on). Previously those semconv keys existed but were never filled, unlike other vendor mappers.The mapper now serializes chat input and completion choices into the OTel GenAI
role/partsshape (text, tool calls, tool results), JSON-stringifies the lists viajson_or_none, and addsfinish_reasonon outputs. It handles multimodal input (text parts only), parses toolargumentsJSON when possible, falls back to legacytextcompletions, and omits both attributes when capture leaves empty tuples._llm_callis refactored to merge_BLOB_ATTRSwith existing attrs and a new_toolshelper. Regression tests cover the main serialization paths and failure modes.Reviewed by Cursor Bugbot for commit 636a3f2. Bugbot is set up for automated code reviews on this repo. Configure here.