gen-ai: add document modality to multimodal content parts#142
Conversation
Adds the 'document' value to the Modality enum in the gen-ai input, output, and system-instructions message JSON schemas. Today PDF/DOCX and similar parts have to fall through to the free-form string branch of the modality anyOf; this change gives them a first-class enum value matching the existing image/video/audio entries. Pure addition: no removals, no changes to required fields. The notebook source under docs/gen-ai/non-normative/models.ipynb has been updated to keep the regen-from-notebook flow consistent, and a worked example was added to examples-llm-calls.md. Follow-up from open-telemetry/semantic-conventions#3673 (closed and refiled here per the new repo home for gen-ai semantic conventions).
Per @trask's direction on PR open-telemetry#143, the multimodal scenario lives in open-telemetry#142 so it can carry the document modality first, then be extended in open-telemetry#143 (byte_size) and open-telemetry#144 (stripped_reason) once this lands. Adds run_chat_with_document_input_reference(client) to the openai scenario, exercising the new 'document' enum value on a UriPart in gen_ai.input.messages. The OpenAI SDK call uses the documented file content block; the canonical OTel attribute carries a TextPart plus a document-modality UriPart, mirroring how a KYC document-extraction workload surfaces in the wire shape.
@trask's reference-scenario SKILL evaluation flagged that the previous openai multimodal block fabricated a URI and mime_type that aren't on the chat.completions.create boundary -- file_id is opaque, neither yields a public URI or mime type without a separate Files-API roundtrip. Two fixes per the evaluation: 1. Rewrite the openai scenario to use the inline file_data shape ({'type': 'file', 'file': {'file_data': 'data:application/pdf;base64,...', 'filename': '...'}}) and emit a BlobPart instead of a UriPart. Every emitted BlobPart field now derives directly from the SDK arg: - mime_type from the data-URI prefix - content from the base64 portion - modality 'document' from the classification of application/pdf No fabricated URIs. 2. Add run_chat_with_document_input() to the anthropic scenario. Anthropic's Messages API has a first-class document content block ({'type': 'document', 'source': {'type': 'base64', 'media_type': '...', 'data': '...'}}), so the BlobPart emission is directly observable from the SDK call boundary with no derivation gymnastics. Both scenarios run cleanly against the local mock server; weaver live-check reports no after_resolution policy violations on either. The bedrock-converse demonstration trask also suggested is deferred to a follow-up (DocumentBlock has the same shape; happy to add if needed). Addresses open-telemetry#142 (comment)...
|
Thanks @trask for running the SKILL — the central capture-gap critique on the URI/mime fabrication was exactly right, and the inline-data path is the cleaner fix. Addressed in 5b6905c: 1. OpenAI scenario — switched to inline
|
…rt fix Completes @trask's three-scenario request for PR open-telemetry#142: - openai: inline file_data + BlobPart (previous commit, derives mime/content/modality from SDK arg) - anthropic: native document content block + BlobPart (previous commit) - aws-bedrock: native Converse DocumentBlock + BlobPart (this commit) -- the DocumentBlock 'format' / 'source.bytes' fields map 1:1 to the BlobPart mime_type/content with zero out-of-band derivation. Most direct of the three. Also applies the ruff isort fix from this run (I001 in the anthropic scenario file: import base64 should sort after import anthropic in the new function -- ruff check --fix moved it). All three scenarios validated locally against the mock LLM server; weaver live-check reports no after_resolution policy violation on any of them. ruff format + ruff check both clean.
|
Done — Bedrock scenario added in 66f53f6 so the PR now covers all three native producers you flagged:
All three pass the SKILL's "credible reference coverage" bar — every emitted field on the BlobPart traces back to a value present on the synchronous All three scenarios run cleanly locally:
Ready for re-review. |
…nce scenarios; will rebase out when open-telemetry#142 lands # Conflicts: # CHANGELOG.md
Adds byte_size to the BlobPart emitted on gen_ai.input.messages in all three multimodal reference scenarios introduced by PR open-telemetry#142 (openai, anthropic, aws-bedrock). The byte_size value is len(pdf_bytes) -- the size of the raw payload the SDK saw on the call boundary, directly derivable without any Files-API roundtrip: - openai: pdf_bytes is the source for both the data-URI on the SDK side and the BlobPart.content + byte_size on the OTel side. - anthropic: pdf_bytes is base64-encoded into the document block's source.data; byte_size is len(pdf_bytes) before encoding. - aws-bedrock: source.bytes IS the raw bytes; byte_size is len() of the same value the SDK passed. This is the byte_size companion to the document-modality demo from PR open-telemetry#142 -- exactly the pattern an instrumentation library should expose. ruff format and ruff check both clean. Currently sits on top of a merge of the open-telemetry#142 branch; once open-telemetry#142 lands this PR rebases cleanly onto main and the open-telemetry#142 commits drop out, leaving only the byte_size schema + this scenario extension.
Round-3 Copilot review on PR open-telemetry#142: the docstring claimed Anthropic's document content block exposes 'mime type, source bytes, and filename' on the SDK boundary, but the scenario does not include a filename field in its payload. Removing the filename claim keeps the docstring accurate to the scenario; if/when a filename field is added to the schema in a follow-up (per issue open-telemetry#50), the docstring can be updated alongside.
…nce scenarios; will rebase out when open-telemetry#142 lands # Conflicts: # CHANGELOG.md # docs/gen-ai/non-normative/examples-llm-calls.md
Adds a 'stripped document' multimodal block to each of the three reference scenarios introduced by PR open-telemetry#142 (openai, anthropic, aws-bedrock). The SDK call shape is identical to the captured case in each scenario -- a real document content block on the wire -- but the OTel emission demonstrates the fail-closed observability path: - type, modality, mime_type are kept (all directly observable on the SDK boundary) - content is omitted; stripped_reason = 'size_exceeded' is set This is the canonical 'instrumentation chose not to capture' shape that motivates the schema changes in this PR. The top-level anyOf constraint added to BlobPart/FilePart/UriPart ensures these parts validate (stripped_reason branch matches), while a part with neither content nor stripped_reason would still be rejected. All three scenarios run cleanly locally; ruff format + ruff check both pass. Currently sits on top of a merge of the open-telemetry#142 branch; once open-telemetry#142 lands this PR rebases cleanly onto main, the open-telemetry#142 commits drop, and only the schema changes plus this scenario extension remain.
Add an optional non-negative byte_size to BlobPart, FilePart, and UriPart in the gen_ai input/output/system-instructions message JSON schemas (and the reference models.ipynb). Lets an instrumentation record the size of a captured media payload for cost-of-capture telemetry and storage planning. Omitted when the size is not cheaply observable - e.g. a pre-existing remote URI, where the bytes would have to be downloaded to measure; set only when the bytes were observed before becoming a URI. Per-part descriptions reflect this (decoded byte length for BlobPart, file size for FilePart, payload size for UriPart). Examples updated accordingly. Rebased onto main after open-telemetry#142 merged; reference-scenario emission deferred to the follow-up that will also carry document-modality and stripped_reason.
Add 'document' modality for application/* MIME types in DeriveModalityFromMediaType(), aligning with the updated Modality enum in the upstream gen-ai input/output message schemas (open-telemetry/semantic-conventions-genai#142). Co-authored-by: Copilot <[email protected]>
Add document modality detection in OtelMessageSerializer.DeriveModalityFromMediaType to support the new 'document' modality value added to the GenAI semantic conventions (open-telemetry/semantic-conventions-genai#142). PDF, Office, and ODF file types (application/pdf, application/msword, application/vnd.openxmlformats-officedocument.*, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/vnd.oasis.opendocument.*, application/rtf, text/rtf) now emit modality: 'document' in message-part serialization. Also migrate doc-comment wording in all OpenTelemetry* instrumentation client files from 'Semantic Conventions for Generative AI systems v1.41' to 'GenAI Semantic Conventions latest' per the new standalone conventions repo. Co-authored-by: Copilot <[email protected]>
- Add 'document' modality in DeriveModalityFromMediaType for PDF and Office document MIME types (application/pdf, application/msword, application/vnd.openxmlformats-officedocument.*, application/vnd.ms-*) per open-telemetry/semantic-conventions-genai#142 - Migrate doc-comment wording from 'Semantic Conventions for Generative AI systems v1.41' to 'GenAI Semantic Conventions latest' across all six OpenTelemetry* instrumentation client files - Augment AllOfficialOtelContentPartTypes_SerializedCorrectly test to assert document modality for application/pdf content Co-authored-by: Copilot <[email protected]>
Description
Adds
documentas a value of theModalityenum onBlobPart,FilePart, andUriPartin the gen-ai input, output, and system-instructions message JSON schemas. PDFs, DOCX, and other office-document payloads currently fall through to the free-formstringbranch of themodalityanyOf; this change gives them a first-class enum value alongsideimage/video/audio.Concrete shape on the wire:
{ "type": "blob", "modality": "document", "mime_type": "application/pdf", "content": "<base64 of raw PDF bytes>" }The Pydantic source model in
docs/gen-ai/non-normative/models.ipynbis the source of truth; the three JSON schemas are regenerated from it.Follow-up from the closed
open-telemetry/semantic-conventions#3673. Closes (or significantly advances) #50. Sister PRs in this repo:byte_size#143 andstripped_reason#144. The three are independent and additive.Motivation
User journey: content auditability and cost-attribution for document-processing GenAI workloads.
A high-volume real workload that drove this: BFSI KYC (Know Your Customer) extraction — a customer uploads a passport or utility bill as a PDF, the GenAI app extracts identity fields, and the instrumentation needs to record what kind of content was sent. Today the only way to convey "this was a PDF document, not an image" is either (a) leak the mime type alone and ask consumers to classify, or (b) fall through to the free-form
stringbranch ofmodality, fragmenting how producers describe documents.Both options block:
documentis a distinct class with different per-token cost characteristics (PDFs are commonly OCR'd or embedded by the model, which has cost implications different from raw images).Prior art:
messagesAPI has a first-classdocumentcontent-block type today.filecontent block with inlinefile_data(data URI) orfile_id.DocumentBlock(format+source.bytes+name).genai-otel-instrument(v1.1.1) already emitsmodality: "document"on PDFs captured by its OpenAI / Anthropic / Bedrock instrumentors, validated end-to-end through MinIO + OTel collector + OpenSearch on KYC extraction workloads.Issue #50 enumerates three follow-ups that this PR partially addresses (the modality enum) and that the broader split (
byte_sizein #143, optionalfilenameas a future PR) will round out.Prototype
Three matching reference scenarios were added (per @trask's evaluation on the first iteration: the emitted fields must derive from the SDK call boundary, no fabricated URIs or out-of-band lookups):
reference/scenarios/openai/run_chat_with_document_input_reference{"type":"file", "file": {"file_data": "data:application/pdf;base64,...", "filename": "..."}}reference/scenarios/anthropic/run_chat_with_document_input{"type":"document", "source": {"type":"base64", "media_type":"application/pdf", "data":"..."}}source.media_type; content ←source.data; modality from mime classificationreference/scenarios/aws-bedrock/run_converse_with_document_input_reference{"document": {"format":"pdf", "name":"...", "source": {"bytes": <raw>}}}format(pdf→application/pdf); content = base64 ofsource.bytes; modality from mime classificationAll three are runnable end-to-end against the local mock LLM server (
uv run run-scenario <library>); weaver live-check reports✔ No after_resolution policy violationon each. Every emitted BlobPart field ongen_ai.input.messagesis derivable from a value present on a single synchronousclient.X.create(...)call — no Files-API roundtrip required, no application-side URI fabrication.A worked example was also added to
docs/gen-ai/non-normative/examples-llm-calls.mdunder the existing Multimodal-inputs block.make check-policiesandmake generate-allboth run clean against this branch on a local Docker host (Weaver v0.23.0);generate-allproduces zero working-tree drift.Checklist
UnreleasedinCHANGELOG.md