Skip to content

openai: chat message content recorded as empty parts when it is a list of content parts #357

Description

@HQidea

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    New issues

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions