[opentelemetry-instrumentation-genai-anthropic] Capture Anthropic image and document inputs as GenAI BlobPart, UriPart, FilePart, and GenericPart message parts - #589
Conversation
Pull request dashboard statusWaiting on the author · refreshed 2026-09-11 22:33 UTC Respond to 5 review items (e.g. link a commit, explain why not, ask a follow-up): Status above doesn't look right?
|
1d081e3 to
2346929
Compare
BlobPart, UriPart, FilePart, and GenericPart message partsBlobPart, UriPart, FilePart, and GenericPart message parts
BlobPart, UriPart, FilePart, and GenericPart message partsBlobPart, UriPart, FilePart, and GenericPart message parts
2346929 to
ad6177c
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Adds multimodal (image + document) input extraction for the Anthropic instrumentation so spans represent inputs as GenAI BlobPart, UriPart, FilePart, and GenericPart.
Changes:
- Extend Anthropic content conversion to support image/document sources (base64, url, file, nested content).
- Add unit tests verifying extractor behavior and span-captured multimodal parts (sync/async/streaming) plus a new conformance scenario.
- Add changelog entry and a new VCR cassette for the conformance scenario.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py | Implements multimodal source parsing into GenAI message parts. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_messages_extractors.py | Adds unit coverage for image/document source extraction and ordering. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_sync_messages.py | Adds sync span-capture tests for multimodal inputs and generator-preservation. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_async_messages.py | Adds async span-capture tests for multimodal inputs (create + streaming). |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_conformance.py | Registers the new multimodal conformance scenario. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/conformance/multimodal.py | New conformance scenario asserting multimodal parts are emitted correctly. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/cassettes/test_chat_anthropic_multimodal_image_llm_call.yaml | Adds VCR cassette for conformance replay. |
| instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/589.added | Documents the new multimodal capture capability. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
b0e67ad to
5ba834a
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The multimodal streaming tests reference non-matching existing cassettes and the new document URL/text MIME type handling currently hard-codes values that can produce incorrect telemetry.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py:160
- For document URL sources, the code hard-codes mime_type="application/pdf" even when the request doesn't provide a media type. This can record an incorrect MIME type for non-PDF documents; prefer using a provided "media_type" when present, otherwise leave mime_type as None.
return [
UriPart(
mime_type="application/pdf",
modality="document",
uri=url,
instrumentation/opentelemetry-instrumentation-genai-anthropic/src/opentelemetry/instrumentation/genai/anthropic/utils.py:173
- For document text sources, the code hard-codes mime_type="text/plain" instead of using the request's "media_type" field. This can produce incorrect telemetry when a different media type is supplied.
BlobPart(
mime_type="text/plain",
modality="document",
content=data.encode(),
)
]
- Files reviewed: 10/10 changed files
- Comments generated: 5
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The new document-content conversion can drop generator-backed nested document inputs entirely (losing evidence that a document was provided), and the PR description’s listed test commands don’t match the Anthropic changes.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
instrumentation/opentelemetry-instrumentation-genai-anthropic/.changelog/589.added:1
- The PR description's "How has this been tested?" section lists only LangChain tox envs, but this change is in the Anthropic instrumentation package (new Anthropic unit + conformance coverage and cassettes). Please update the PR description to reflect the actual Anthropic test commands that were run so reviewers can reproduce accurately.
Capture Anthropic image and document inputs as GenAI ``BlobPart``, ``UriPart``, ``FilePart``, and ``GenericPart`` message parts
- Files reviewed: 12/12 changed files
- Comments generated: 1
- Review effort level: Lite
d3f947b to
777373f
Compare
…Part`, `FilePart`, and `GenericPart` message parts
08a265f to
d156507
Compare
| if isinstance(data, PathLike) or callable(getattr(data, "read", None)): | ||
| return GenericPart(type=modality) |
There was a problem hiding this comment.
| if isinstance(data, PathLike) or callable(getattr(data, "read", None)): | |
| return GenericPart(type=modality) | |
| if isinstance(data, PathLike) or callable(getattr(data, "read", None)): | |
| return GenericPart(type="blob") |
In GenAI semconv message parts, type is "blob" (with modality indicating "image" or "document"). "image" and "document" are not valid part types.
| return [ | ||
| UriPart( | ||
| mime_type="application/pdf", | ||
| modality="document", | ||
| uri=url, | ||
| ) | ||
| ] |
There was a problem hiding this comment.
| return [ | |
| UriPart( | |
| mime_type="application/pdf", | |
| modality="document", | |
| uri=url, | |
| ) | |
| ] | |
| media_type = source_dict.get("media_type") | |
| return [ | |
| UriPart( | |
| mime_type=media_type if isinstance(media_type, str) else None, | |
| modality="document", | |
| uri=url, | |
| ) | |
| ] |
Anthropic document URL sources can specify a media_type. Hardcoding "application/pdf" overrides the actual media type when non-PDF documents (or unknown types) are supplied.
| def _convert_document_block(block: Mapping[str, Any]) -> MessagePart | None: | ||
| parts = _extract_document_source(block.get("source")) | ||
| metadata = { | ||
| key: block[key] | ||
| for key in ("title", "context", "citations") | ||
| if block.get(key) is not None | ||
| } | ||
| source = block.get("source") | ||
| source_mapping = ( | ||
| cast(Mapping[str, object], source) | ||
| if isinstance(source, Mapping) | ||
| else None | ||
| ) | ||
| is_nested = ( | ||
| source_mapping is not None and source_mapping.get("type") == "content" | ||
| ) | ||
| if metadata or (is_nested and parts): | ||
| return GenericPart(type="document") | ||
| return parts[0] if parts else None |
There was a problem hiding this comment.
When document metadata like title, context, or citations is present, returning GenericPart(type="document") discards the extracted BlobPart or UriPart (and "document" is not a valid semconv part type). We should preserve the extracted content part regardless of metadata.
Also, if source.type == "content" has multiple nested parts (e.g. text + image), returning a single MessagePart forces dropping them. If convert_content_to_parts extends a list of parts per block, we can preserve all nested parts without collapsing them into GenericPart:
# Test showing the issue:
content = [{
"type": "document",
"title": "Report",
"source": {"type": "base64", "media_type": "application/pdf", "data": "QUJD"},
}]
# Expecting BlobPart(mime_type="application/pdf", modality="document", content=b"ABC")
# Currently returns GenericPart(type="document")| return parsed | ||
|
|
||
|
|
||
| def _multimodal_input_message(): |
There was a problem hiding this comment.
nit: _multimodal_input_message and _assert_multimodal_input are duplicated identically in tests/test_async_messages.py. Consider moving them to a shared helper or conftest to keep them in sync.
| @@ -0,0 +1 @@ | |||
| Capture Anthropic image and document inputs as GenAI ``BlobPart``, ``UriPart``, ``FilePart``, and ``GenericPart`` message parts | |||
BlobPart, UriPart, FilePart, and GenericPart message partsBlobPart, UriPart, FilePart, and GenericPart message parts
Description
Ensures multimodal feature parity with - #296
Type of change
Please delete options that are not relevant.
How has this been tested?
Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. List any relevant details for your test
configuration.
Checklist
See CONTRIBUTING.md
for the style guide, changelog guidance, and more.