fix(model): add DataBlock handling in OpenAIMessageConverter#1744
fix(model): add DataBlock handling in OpenAIMessageConverter#1744Sparkle6979 wants to merge 5 commits into
Conversation
DataBlock is the 2.0-forward multimodal container (image/audio/video), but convertContentBlocks() never handled it — DataBlock media was silently dropped from API requests. - Add DataBlock branch in convertContentBlocks(): infer MIME type from source (Base64Source.getMediaType / URLSource.getUrl + MediaUtils), then route to image_url / input_audio / video_url content parts - Add DataBlock to hasMediaContent() so multimodal formatting path is used - Extract addTextPart/addImagePart/addAudioPart/addVideoPart to eliminate duplication between legacy blocks and the new DataBlock path - Add DataBlockConverterTest: URL and Base64 source coverage Fixes agentscope-ai#1693
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR addresses issue #1693 by adding DataBlock support to OpenAIMessageConverter, filling a gap where the new polymorphic media container was silently dropped during message formatting. The approach is sound: infer the MIME type from the source (explicit mediaType on Base64Source, extension-based detection on URLSource), then route to the appropriate image_url / input_audio / video_url content part via extracted helper methods. The refactoring of legacy ImageBlock/AudioBlock/VideoBlock handling into dedicated addImagePart / addAudioPart / addVideoPart helpers is clean and behavior-preserving. Test coverage is reasonable for the happy paths, though a few edge cases are missing.
| } | ||
| } | ||
|
|
||
| private void addImagePart(Source source, List<OpenAIContentPart> parts) { |
There was a problem hiding this comment.
[major] When MediaUtils.determineMediaType() returns "application/octet-stream" (e.g., a URLSource whose URL has no recognizable file extension like https://cdn.example.com/stream), the DataBlock is silently dropped with only a log.warn. This is inconsistent with how legacy blocks handle failures — they add a text placeholder (e.g., [Image - processing failed: ...]). Users migrating to DataBlock (as the Javadoc recommends) may not notice the warning and will see their content silently vanish from the message. Consider adding a text placeholder in the else branch:
} else {
log.warn("DataBlock has unrecognized MIME type '{}', skipping", mimeType);
addTextPart("[Data - unrecognized type: " + mimeType + "]", parts);
}There was a problem hiding this comment.
Fixed: unrecognized MIME types now produce a text placeholder like [Data - unrecognized type: ...] instead of being silently dropped, consistent with how ImageBlock/AudioBlock failures are handled.
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: addDataPart else-branch now calls addTextPart with '[Data - unrecognized type: ...]' placeholder, exactly matching the bot's suggestion.
| } else { | ||
| log.warn("DataBlock has unknown source type: {}", source.getClass().getSimpleName()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
[minor] MediaUtils.determineMediaType() relies solely on file-extension matching. A DataBlock with a URLSource pointing to a URL without a recognizable extension (e.g., https://api.example.com/media/12345) will be routed to the application/octet-stream path and dropped. For Base64Source, the explicit mediaType field handles this correctly. Consider documenting this limitation in the addDataPart Javadoc or adding a comment, so callers understand that URLSource-based DataBlocks require URLs with standard media extensions.
There was a problem hiding this comment.
Acknowledged. This is an inherent limitation of extension-based MIME detection. Added a comment in addDataPart noting that URLSource-based DataBlocks require URLs with recognizable file extensions.
| .contains("https://example.com/song.mp3")); | ||
| assertTrue(hasAudioText, "URL-based audio should produce a text placeholder"); | ||
| } | ||
| } |
There was a problem hiding this comment.
[minor] Missing test coverage for two edge cases:
-
Unrecognized MIME type: A
DataBlockwith aURLSourcewhose URL has no recognizable extension (e.g.,https://example.com/data). This exercises theapplication/octet-stream/elsebranch inaddDataPart. -
Base64Source audio via DataBlock: A
DataBlockwith aBase64Sourceof typeaudio/wav. This exercises theBase64Source→addAudioPart→input_audiopath, which is the most important happy path for audio.
Adding these two tests would significantly improve confidence in the new code paths.
There was a problem hiding this comment.
Good catch — these edge cases are already covered in the latest commit (testDataBlockUnknownMimeType and testDataBlockBase64Audio).
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: Null-source check is now at the top of addDataPart (lines 373-375) with early return and log.warn, cleanly encapsulating the guard instead of using continue in convertContentBlocks.
| parts.add(OpenAIContentPart.text(text)); | ||
| } | ||
|
|
||
| private void addDataPart(Source source, List<OpenAIContentPart> parts) { |
There was a problem hiding this comment.
[nit] addTextPart is a trivial one-liner wrapper (parts.add(OpenAIContentPart.text(text))) that adds indirection without the null-check / try-catch / error-handling value that the other addXxxPart methods provide. It's not wrong, but it's asymmetric. Consider either inlining it back (as it was before), or adding a null-check on text to make it consistent with the defensive style of the other helpers.
There was a problem hiding this comment.
Fair point. Kept as-is for now — the wrapper provides consistency with the other addXxxPart methods at the call site (every branch uses the same pattern).
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: Nit explicitly acknowledged and deferred by developer; bot itself stated the current if-chain works fine, so conscious deferral is a valid resolution.
|
|
||
| class DataBlockConverterTest { | ||
|
|
||
| @Test |
There was a problem hiding this comment.
[praise] Good test design — the tests directly exercise the OpenAIChatFormatter end-to-end rather than mocking internals. The assertions verify both the structure (List) and the content (correct type, correct URL/data) of the output. The testDataBlockAudioUrlFallback test correctly verifies the fallback-to-text-placeholder behavior for URL-based audio.
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: This is a praise comment requiring no code change; developer acknowledged the feedback.
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR addresses issue #1693 by adding DataBlock support to OpenAIMessageConverter, filling a gap where the new polymorphic media container was silently dropped during message formatting. The approach is sound: infer the MIME type from the source (explicit mediaType on Base64Source, extension-based detection on URLSource), then route to the appropriate image_url / input_audio / video_url content part via extracted helper methods. The refactoring of legacy ImageBlock/AudioBlock/VideoBlock handling into dedicated addImagePart / addAudioPart / addVideoPart helpers is clean and behavior-preserving. Test coverage is reasonable for the happy paths, though a few edge cases are missing.
| } | ||
| } | ||
|
|
||
| private void addImagePart(Source source, List<OpenAIContentPart> parts) { |
There was a problem hiding this comment.
[major] When MediaUtils.determineMediaType() returns "application/octet-stream" (e.g., a URLSource whose URL has no recognizable file extension like https://cdn.example.com/stream), the DataBlock is silently dropped with only a log.warn. This is inconsistent with how legacy blocks handle failures — they add a text placeholder (e.g., [Image - processing failed: ...]). Users migrating to DataBlock (as the Javadoc recommends) may not notice the warning and will see their content silently vanish from the message. Consider adding a text placeholder in the else branch:
} else {
log.warn("DataBlock has unrecognized MIME type '{}', skipping", mimeType);
addTextPart("[Data - unrecognized type: " + mimeType + "]", parts);
}There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: Same issue as earlier thread; addDataPart else-branch now adds '[Data - unrecognized type: ...]' text placeholder instead of silently dropping.
| } else { | ||
| log.warn("DataBlock has unknown source type: {}", source.getClass().getSimpleName()); | ||
| return; | ||
| } |
There was a problem hiding this comment.
[minor] MediaUtils.determineMediaType() relies solely on file-extension matching. A DataBlock with a URLSource pointing to a URL without a recognizable extension (e.g., https://api.example.com/media/12345) will be routed to the application/octet-stream path and dropped. For Base64Source, the explicit mediaType field handles this correctly. Consider documenting this limitation in the addDataPart Javadoc or adding a comment, so callers understand that URLSource-based DataBlocks require URLs with standard media extensions.
| .contains("https://example.com/song.mp3")); | ||
| assertTrue(hasAudioText, "URL-based audio should produce a text placeholder"); | ||
| } | ||
| } |
There was a problem hiding this comment.
[minor] Missing test coverage for two edge cases:
-
Unrecognized MIME type: A
DataBlockwith aURLSourcewhose URL has no recognizable extension (e.g.,https://example.com/data). This exercises theapplication/octet-stream/elsebranch inaddDataPart. -
Base64Source audio via DataBlock: A
DataBlockwith aBase64Sourceof typeaudio/wav. This exercises theBase64Source→addAudioPart→input_audiopath, which is the most important happy path for audio.
Adding these two tests would significantly improve confidence in the new code paths.
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: Same fix as earlier thread; addDataPart handles null source with early return and log.warn at lines 373-375.
| parts.add(OpenAIContentPart.text(text)); | ||
| } | ||
|
|
||
| private void addDataPart(Source source, List<OpenAIContentPart> parts) { |
There was a problem hiding this comment.
[nit] addTextPart is a trivial one-liner wrapper (parts.add(OpenAIContentPart.text(text))) that adds indirection without the null-check / try-catch / error-handling value that the other addXxxPart methods provide. It's not wrong, but it's asymmetric. Consider either inlining it back (as it was before), or adding a null-check on text to make it consistent with the defensive style of the other helpers.
|
|
||
| class DataBlockConverterTest { | ||
|
|
||
| @Test |
There was a problem hiding this comment.
[praise] Good test design — the tests directly exercise the OpenAIChatFormatter end-to-end rather than mocking internals. The assertions verify both the structure (List) and the content (correct type, correct URL/data) of the output. The testDataBlockAudioUrlFallback test correctly verifies the fallback-to-text-placeholder behavior for URL-based audio.
There was a problem hiding this comment.
✅ Verified as addressed in 0af6b61: Praise comment requiring no code change; DataBlock is correctly included in hasMediaContent at line 360.
Instead of silently dropping DataBlock with unknown MIME type, add a text placeholder consistent with how ImageBlock/AudioBlock handles processing failures. This matches the legacy block behavior and gives users a visible hint when their DataBlock is not formatted correctly.
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
All blocking review comments have been addressed. 3 non-blocking suggestion(s) (minor/nit) remain open and may be addressed in a follow-up.
Background
DataBlockwas introduced in 2.0 as the forward-looking multimodal container (image/audio/video), replacingImageBlock/AudioBlock/VideoBlock. Its Javadoc states:However, none of the four provider converters process
DataBlock— it is silently dropped during message formatting. It is unclear whether this is a bug (the converter side was simply forgotten) or an intentional work-in-progress (the feature was merged before it was fully wired up).Reported as #1693.
What this PR does
Adds
DataBlockhandling toOpenAIMessageConverter, which covers all providers that extendOpenAIBaseFormatter(OpenAI, DeepSeek, GLM, and any OpenAI-compatible API):hasMediaContent(): recognizeDataBlockas media contentconvertContentBlocks(): infer MIME type from source (Base64Source.getMediaType()/URLSource.getUrl()+MediaUtils.determineMediaType()), then route to the appropriateimage_url/input_audio/video_urlcontent partaddTextPart/addImagePart/addAudioPart/addVideoPart/addDataPart) to avoid duplicating logic between legacy blocks and the new DataBlock pathNot covered
Converters for Gemini, DashScope, Anthropic, and Ollama have the same gap — they also need
DataBlocksupport. This PR only addresses the OpenAI-compatible path.Note
DataBlockappears to be a half-finished feature — introduced in the message model but never wired up on the converter side. I am not sure whether the maintainers are already working on completing this internally. If so, or if a different approach is preferred, I am happy to adjust or close this PR.