openinference

Multimodal Attributes

This document describes how message content arrays represent multimodal content (text, images, audio, video, and documents such as PDFs or inline text) in OpenInference spans. The same message.contents structure is also used for reasoning and provider-native tool-use parts when item ordering must be preserved.

Vendor mappings live in internal_docs/specs/audio_video/. openai-agents realtime still emits instrumentor-local input.audio.* and output.audio.* strings. Those keys are not published SpanAttributes yet.

Message Content Arrays

When a message contains multiple content items (e.g., text and images), the content is represented using the message.contents array structure with flattened attributes.

Attribute Pattern

llm.input_messages.<messageIndex>.message.contents.<contentIndex>.message_content.<attribute>

Where:

Content Type Attributes

Each content item has a type attribute that identifies its kind:

Reasoning-specific fields such as message_content.id, message_content.signature, message_content.data, and message_content.encrypted_content are defined in LLM Spans.

Text Content

llm.input_messages.0.message.contents.0.message_content.type = "text"
llm.input_messages.0.message.contents.0.message_content.text = "What is in this image?"

Image Content

llm.input_messages.0.message.contents.1.message_content.type = "image"
llm.input_messages.0.message.contents.1.message_content.image.image.url = "https://example.com/image.jpg"

For base64-encoded images:

llm.input_messages.0.message.contents.1.message_content.type = "image"
llm.input_messages.0.message.contents.1.message_content.image.image.url = "data:image/png;base64,iVBORw0KGgo..."

Audio Content

llm.input_messages.0.message.contents.2.message_content.type = "audio"
llm.input_messages.0.message.contents.2.message_content.audio.audio.url = "https://example.com/audio.mp3"
llm.input_messages.0.message.contents.2.message_content.audio.audio.transcript = "Hello, how are you?"

audio.transcript is optional. Emit it when a transcription is available on the same part. Do not emit audio.mime_type. Consumers infer MIME type from the URL path extension (.wav to audio/wav, .mp3 to audio/mpeg) or from a data URI prefix.

For OpenAI Chat Completions input_audio, build a data URI from base64 data and format (wav maps to audio/wav, mp3 maps to audio/mpeg) and store that URI in audio.audio.url. Assistant message.audio on the same API is still a chat message. Put it on llm.output_messages audio content items.

Video Content

llm.input_messages.0.message.contents.3.message_content.type = "video"
llm.input_messages.0.message.contents.3.message_content.video.video.url = "gs://bucket/clip.mp4"

For base64-encoded video:

llm.input_messages.0.message.contents.3.message_content.type = "video"
llm.input_messages.0.message.contents.3.message_content.video.video.url = "data:video/mp4;base64,AAAA..."

Do not emit a MIME type attribute for video. Consumers infer MIME type from the URL path extension (.mp4 to video/mp4, .webm to video/webm, .ogv to video/ogg, .avi to video/x-msvideo) or from a data URI prefix. Copy gs://, s3://, and https:// URIs verbatim. Do not put video in image.url. Do not store provider file ids in video.url.

The doubled prefix (message_content.video.video.url) matches message_content.image.image.url. It is the concatenation of message_content.video and video.url.

Document Content

Documents passed to a model, such as PDFs, uploaded files, or inline text, use message_content.type "document":

llm.input_messages.0.message.contents.4.message_content.type = "document"
llm.input_messages.0.message.contents.4.message_content.document.document.url = "data:application/pdf;base64,JVBERi0xLjcK..."
llm.input_messages.0.message.contents.4.message_content.document.document.mime_type = "application/pdf"
llm.input_messages.0.message.contents.4.message_content.document.document.name = "report.pdf"
Leaf Holds
document.url The document bytes: a URI copied verbatim (https://, gs://, s3://), or a base64 data URI
document.content Inline text the provider receives as a string
document.id The provider file id of a file uploaded through a Files API
document.mime_type The MIME type stated in the request
document.name The file name or title supplied with the document; not a unique identifier

Provider mappings:

Provider part url content mime_type id name
OpenAI Chat file, Azure OpenAI, LiteLLM file_data   data URI prefix, LiteLLM format file_id filename
OpenAI Responses input_file, openai-agents file_data, file_url   data URI prefix file_id filename
Anthropic document: base64 / url / text / file source data URI from data, or url data (text source) media_type; application/pdf for url sources file_id title
Bedrock Converse document data URI from bytes, or s3Location.uri text from format   name
Gemini inline_data / file_data data URI from data, or file_uri   mime_type   display_name
LangChain file / text-plain data URI from base64, or url text mime_type file_id extras.filename

The doubled prefix (message_content.document.document.url) matches message_content.image.image.url.

External Storage for Large Media

Inline base64 payloads can exceed OTLP message limits and inflate backend storage. As an experimental capability, instrumentations MAY externalize oversized images and documents at capture time: upload the decoded bytes to configured blob storage and record the destination URI in the same image.image.url or document.url attribute where the data URI would have been recorded.

llm.input_messages.0.message.contents.1.message_content.type = "image"
llm.input_messages.0.message.contents.1.message_content.image.image.url = "s3://my-bucket/oi-media/3a7bd3e2....png"
llm.input_messages.0.message.contents.2.message_content.document.document.url = "s3://my-bucket/oi-media/9f86d081....pdf"

The same applies to output-message images (llm.output_messages.*.message.contents.*.message_content.image.image.url), to output-message documents, and to input.documents / output.documents.

Semantics:

This maps directly onto the OTel GenAI semantic conventions message model: an inline data URI corresponds to a blob part, while an externalized reference corresponds to a uri part. The same blob/uri split will apply to message_content.audio.audio.url and message_content.video.video.url (modality audio or video) when GenAI dual-write covers those parts. Shared TraceConfig.mask() does not yet hide or size-gate audio or video.

Span-Kind-Independent Images

llm.input_messages / llm.output_messages carry a full chat-message structure, so they only make sense on an LLM span. Other span kinds handle images too — a TOOL span running OCR, a CHAIN step holding a browser screenshot, an image-generation call with no chat messages at all. Those spans record images with the top-level input.images / output.images attributes. These work like input.value / output.value: they are valid on any span kind and carry no role or message structure.

Attribute Pattern

<input|output>.images.<imageIndex>.image.<attribute>

Where:

For example, a tool span that receives a page scan and returns an annotated version:

{
  "openinference.span.kind": "TOOL",
  "input.images.0.image.url": "data:image/png;base64,iVBORw0KGgo...",
  "output.images.0.image.url": "https://example.com/annotated.png"
}

Multiple images are indexed:

input.images.0.image.url = "https://example.com/page-1.png"
input.images.1.image.url = "https://example.com/page-2.png"

Semantics

Redaction and Size Limits

The image privacy controls apply to these attributes the same way they apply to message-content images:

Support matches what each SDK already does for message-content images: Python enforces the size limit and can externalize to a blob uploader; JavaScript enforces the size limit and redacts oversized images to "__REDACTED__"; Java and Go apply the hide flags only.

Span-Kind-Independent Documents

Spans other than LLM record documents with input.documents / output.documents, following span-kind-independent images. Each leaf means the same as under Document Content.

<input|output>.documents.<documentIndex>.document.<attribute>

{
  "openinference.span.kind": "TOOL",
  "input.documents.0.document.url": "data:application/pdf;base64,JVBERi0xLjcK...",
  "input.documents.0.document.mime_type": "application/pdf",
  "output.documents.0.document.url": "https://example.com/filled-form.pdf"
}

An LLM span records its documents in message.contents and SHOULD NOT repeat them here.

Privacy Considerations

Hiding Images

When OPENINFERENCE_HIDE_INPUT_IMAGES is set to true:

Base64 Image Truncation

When OPENINFERENCE_BASE64_IMAGE_MAX_LENGTH is set (default: 32000):

Hiding Text Content

When OPENINFERENCE_HIDE_INPUT_TEXT is set to true:

Example: Multimodal Message

A user message with both text and image content:

{
  "llm.input_messages.0.message.role": "user",
  "llm.input_messages.0.message.contents.0.message_content.type": "text",
  "llm.input_messages.0.message.contents.0.message_content.text": "What objects do you see in this image?",
  "llm.input_messages.0.message.contents.1.message_content.type": "image",
  "llm.input_messages.0.message.contents.1.message_content.image.image.url": "https://example.com/photo.jpg"
}

Fallback for Simple Messages

When a message contains only text content (no multimodal content), it can use the simpler format:

{
  "llm.input_messages.0.message.role": "user",
  "llm.input_messages.0.message.content": "Hello, how are you?"
}

The message.content attribute is used for simple text-only messages, while message.contents is used for multimodal messages.