Package: opentelemetry-instrumentation-genai-openai (current main; the same code ships in opentelemetry-python-contrib as opentelemetry-instrumentation-openai-v2 ≤ 2.4b0, which per that repo's guidelines now only takes security patches — hence filing here).
Bug
When a Chat Completions message's content is the list-of-content-parts form (the standard OpenAI shape for multi-part text and for multimodal requests), the instrumentation records the message with empty parts in gen_ai.input.messages:
{"role": "user", "parts": []}
The message content is silently dropped from telemetry, even though content capture is enabled and the actual request carried the content.
Reproduction
from opentelemetry.instrumentation.genai.openai.utils import _prepare_input_messages
messages = [
{"role": "system", "content": "You are a fact extractor."},
{
"role": "user",
"content": [
{"type": "text", "text": "user: remember, my name is Tom"},
{"type": "text", "text": "assistant: got it, Tom!"},
],
},
]
for m in _prepare_input_messages(messages):
print(m.role, len(m.parts))
# system 1
# user 0 <-- content dropped
End-to-end this shows up as GENERATION observations with an empty user message in any OTLP backend that renders gen_ai.input.messages (we hit it in production tracing multi-part-text extraction calls into Langfuse and spent a while chasing a phantom "empty prompt" bug in our own pipeline).
Root cause
_prepare_input_messages gates content on _is_text_part, which only accepts str (or an iterable of str):
def _is_text_part(content: Any) -> bool:
return isinstance(content, str) or (
isinstance(content, Iterable)
and all(isinstance(part, str) for part in content)
)
A content-parts array is a list of dicts (or typed part objects), so it fails the check and nothing is appended. _prepare_output_messages shares the same helper.
Note the semconv message models already have the right part types for these shapes (Text, Uri, Blob, File in opentelemetry-util-genai), and the sibling opentelemetry-instrumentation-genai-anthropic package already converts content blocks per-part (convert_content_to_parts) — the OpenAI package just predates that treatment.
Expected
{"type": "text", ...} parts → one Text part each
{"type": "image_url", ...} → Uri (modality image)
{"type": "input_audio", ...} → Blob (modality audio)
{"type": "file", ...} → File
- unrecognized part types skipped rather than nuking the whole message
I have a fix with unit tests ready and will open a PR referencing this issue.
Package:
opentelemetry-instrumentation-genai-openai(currentmain; the same code ships in opentelemetry-python-contrib asopentelemetry-instrumentation-openai-v2≤ 2.4b0, which per that repo's guidelines now only takes security patches — hence filing here).Bug
When a Chat Completions message's
contentis the list-of-content-parts form (the standard OpenAI shape for multi-part text and for multimodal requests), the instrumentation records the message with empty parts ingen_ai.input.messages:{"role": "user", "parts": []}The message content is silently dropped from telemetry, even though content capture is enabled and the actual request carried the content.
Reproduction
End-to-end this shows up as GENERATION observations with an empty user message in any OTLP backend that renders
gen_ai.input.messages(we hit it in production tracing multi-part-text extraction calls into Langfuse and spent a while chasing a phantom "empty prompt" bug in our own pipeline).Root cause
_prepare_input_messagesgates content on_is_text_part, which only acceptsstr(or an iterable ofstr):A content-parts array is a list of dicts (or typed part objects), so it fails the check and nothing is appended.
_prepare_output_messagesshares the same helper.Note the semconv message models already have the right part types for these shapes (
Text,Uri,Blob,Fileinopentelemetry-util-genai), and the siblingopentelemetry-instrumentation-genai-anthropicpackage already converts content blocks per-part (convert_content_to_parts) — the OpenAI package just predates that treatment.Expected
{"type": "text", ...}parts → oneTextpart each{"type": "image_url", ...}→Uri(modalityimage){"type": "input_audio", ...}→Blob(modalityaudio){"type": "file", ...}→FileI have a fix with unit tests ready and will open a PR referencing this issue.