This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new f94242bfa [integration][java] Send user images and documents through 
the OpenAI Responses API (#1275)
f94242bfa is described below

commit f94242bfa3a7518041fba9d5ed4e1a047b9d50e9
Author: Edson <[email protected]>
AuthorDate: Wed Oct 28 01:55:24 2026 -0400

    [integration][java] Send user images and documents through the OpenAI 
Responses API (#1275)
    
    Generated-by: Claude Code 2.1.259 (Claude Opus 5.5)
---
 .../calling-llms/chat-models/providers.md          |  20 ++-
 docs/content/docs/development/calling-llms/chat.md |   4 +-
 .../openai/OpenAIResponsesModelConnection.java     |   3 -
 .../chatmodels/openai/OpenAIResponsesUtils.java    | 103 ++++++++++--
 .../openai/OpenAIResponsesModelConnectionTest.java |  19 ---
 .../openai/OpenAIResponsesMultimodalTest.java      | 179 +++++++++++++++++++++
 6 files changed, 283 insertions(+), 45 deletions(-)

diff --git 
a/docs/content/docs/development/calling-llms/chat-models/providers.md 
b/docs/content/docs/development/calling-llms/chat-models/providers.md
index 805982c12..fb12cb573 100644
--- a/docs/content/docs/development/calling-llms/chat-models/providers.md
+++ b/docs/content/docs/development/calling-llms/chat-models/providers.md
@@ -51,7 +51,7 @@ model, deployment, and server must also support the requested 
input and tool cal
 |---|---|---|
 | [Alibaba Cloud Model Studio 
(DashScope)](#alibaba-cloud-model-studio-dashscope) | Python | No media input; 
media blocks are rejected. |
 | [OpenAI Chat Completions](#multimodal-input), [Azure OpenAI](#azure-openai), 
[vLLM](#vllm) | Java, Python | User messages: images by URL or Base64; WAV/MP3 
audio and documents by Base64. See the conversion and model restrictions below. 
|
-| [OpenAI Responses](#responses-api) | Java | No media input; media blocks are 
rejected. |
+| [OpenAI Responses](#responses-api) | Java | User messages: images by URL or 
Base64; documents by URL or Base64. No audio or video. |
 | [Anthropic](#anthropic) | Java, Python | User messages: JPEG/PNG/GIF/WebP 
images and PDF documents by URL or Base64; plain-text documents by Base64. |
 | [Ollama](#ollama) | Java, Python | User messages: Base64 images, sent 
separately from text; requires a vision model. |
 | [Gemini](#gemini) | Java | User messages: images, audio, video and documents 
by URL or Base64. |
@@ -961,10 +961,9 @@ See [Provider Capabilities](#provider-capabilities) for 
other integrations.
 The OpenAI Responses integration currently has a Java implementation only. To 
use OpenAI Responses API from Python agents, see [Cross-Language Resources]({{< 
ref "docs/development/create-agent/resources#cross-language-resources" >}}).
 {{< /hint >}}
 
-The Flink Agents Responses integration currently rejects media blocks before
-sending a request. Its capabilities differ from the Chat Completions 
integration
-above, even when both connect to OpenAI. This is an integration limit, not a 
claim
-that the upstream Responses API lacks multimodal support.
+The Flink Agents Responses integration sends images and documents from user
+messages, but not audio or video; see **Multimodal input** below. Its 
capabilities
+differ from the Chat Completions integration above, even when both connect to 
OpenAI.
 
 ##### OpenAIResponsesModelConnection Parameters
 
@@ -1040,6 +1039,17 @@ public class MyAgent extends Agent {
 
 {{< /tabs >}}
 
+**Multimodal input.** The media blocks of a user message are sent as Responses 
input content, in block order, next to its text blocks:
+
+| Block | Sent as |
+|---|---|
+| `TextBlock` | an `input_text` part |
+| `ImageBlock` | an `input_image` part: the URL, or a `data:` URI for Base64 
data, with `detail: auto` |
+| `DocumentBlock` | an `input_file` part: Base64 data as `file_data` (a 
`data:` URI) with the block's `name` as `filename` (`document` when unset), or 
a URL as `file_url` |
+| `AudioBlock`, `VideoBlock` | not supported |
+
+A user message with only text blocks is sent as before. Audio, video, and 
media in assistant messages or tool results raise 
`UnsupportedContentBlockException` before the request is sent. Which image and 
file types a model accepts is up to OpenAI.
+
 #### Available Models
 
 Visit the [OpenAI Models 
documentation](https://platform.openai.com/docs/models) for the complete and 
up-to-date list of available chat models.
diff --git a/docs/content/docs/development/calling-llms/chat.md 
b/docs/content/docs/development/calling-llms/chat.md
index e5659746f..19beaf489 100644
--- a/docs/content/docs/development/calling-llms/chat.md
+++ b/docs/content/docs/development/calling-llms/chat.md
@@ -294,8 +294,8 @@ for tool results.
 Representable content does not imply support by the selected provider 
integration
 or model. Check [Provider Capabilities]({{< ref 
"docs/development/calling-llms/chat-models/providers#provider-capabilities" >}})
 before using media. In particular, OpenAI Chat Completions and OpenAI Responses
-have different support in Flink Agents: the Responses integration currently
-rejects media blocks. Media that the integration does not support raises
+have different support in Flink Agents: only Chat Completions sends audio, and 
only
+Responses accepts documents by URL. Media that the integration does not 
support raises
 `UnsupportedContentBlockException` (Java) or `UnsupportedContentBlockError` 
(Python)
 before the request is sent. Even when the integration accepts the media, the
 model service may reject it if the selected model or deployment does not 
support it.
diff --git 
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java
 
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java
index 3ef2d4232..a349e0987 100644
--- 
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java
+++ 
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnection.java
@@ -28,7 +28,6 @@ import com.openai.models.responses.ResponseInputItem;
 import org.apache.flink.agents.api.chat.messages.ChatMessage;
 import org.apache.flink.agents.api.chat.messages.ChatResult;
 import org.apache.flink.agents.api.chat.messages.TokenUsage;
-import 
org.apache.flink.agents.api.chat.messages.UnsupportedContentBlockException;
 import org.apache.flink.agents.api.chat.model.BaseChatModelConnection;
 import org.apache.flink.agents.api.resource.ResourceContext;
 import org.apache.flink.agents.api.resource.ResourceDescriptor;
@@ -121,8 +120,6 @@ public class OpenAIResponsesModelConnection extends 
BaseChatModelConnection {
             List<ChatMessage> messages,
             List<org.apache.flink.agents.api.tools.Tool> tools,
             Map<String, Object> modelParams) {
-        // Media blocks are not sent yet; fail rather than drop them (#1059).
-        UnsupportedContentBlockException.rejectMedia("OpenAI Responses", 
messages);
         ResponseCreateParams params = buildRequest(messages, tools, 
modelParams);
         Response response = client.responses().create(params);
         ChatMessage result = OpenAIResponsesUtils.convertResponse(response);
diff --git 
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesUtils.java
 
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesUtils.java
index 1a800cbf2..e6c16b05b 100644
--- 
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesUtils.java
+++ 
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesUtils.java
@@ -26,18 +26,26 @@ import com.openai.models.responses.EasyInputMessage;
 import com.openai.models.responses.FunctionTool;
 import com.openai.models.responses.Response;
 import com.openai.models.responses.ResponseFunctionToolCall;
+import com.openai.models.responses.ResponseInputFile;
+import com.openai.models.responses.ResponseInputImage;
 import com.openai.models.responses.ResponseInputItem;
 import com.openai.models.responses.ResponseOutputItem;
 import com.openai.models.responses.ResponseOutputMessage;
 import com.openai.models.responses.ResponseReasoningItem;
 import com.openai.models.responses.Tool;
+import org.apache.flink.agents.api.chat.messages.Base64Source;
 import org.apache.flink.agents.api.chat.messages.ChatMessage;
 import org.apache.flink.agents.api.chat.messages.ContentBlock;
+import org.apache.flink.agents.api.chat.messages.DocumentBlock;
+import org.apache.flink.agents.api.chat.messages.ImageBlock;
+import org.apache.flink.agents.api.chat.messages.MediaBlock;
 import org.apache.flink.agents.api.chat.messages.MessageRole;
 import org.apache.flink.agents.api.chat.messages.ReasoningBlock;
 import org.apache.flink.agents.api.chat.messages.TextBlock;
 import org.apache.flink.agents.api.chat.messages.ToolCallBlock;
 import org.apache.flink.agents.api.chat.messages.ToolResultBlock;
+import 
org.apache.flink.agents.api.chat.messages.UnsupportedContentBlockException;
+import org.apache.flink.agents.api.chat.messages.UrlSource;
 import org.apache.flink.agents.api.tools.ToolMetadata;
 
 import java.util.ArrayList;
@@ -65,19 +73,22 @@ final class OpenAIResponsesUtils {
         return items;
     }
 
+    /**
+     * Converts a message to Responses input items. Only user messages can 
carry media: images
+     * become {@code input_image} and documents {@code input_file}, by URL or 
Base64 data. Audio and
+     * video, and media in an assistant message or a tool result, throw {@link
+     * UnsupportedContentBlockException}.
+     */
     static List<ResponseInputItem> convertSingleMessage(ChatMessage message) {
         List<ResponseInputItem> items = new ArrayList<>();
-        List<? extends ContentBlock> contentBlocks =
-                message.getRole() == MessageRole.TOOL
-                        ? ((ToolResultBlock) 
message.getBlocks().get(0)).getBlocks()
-                        : message.getBlocks();
-        for (ContentBlock block : contentBlocks) {
-            if (block instanceof 
org.apache.flink.agents.api.chat.messages.MediaBlock) {
-                throw new IllegalArgumentException(
-                        "OpenAI Responses does not support " + block.getType() 
+ " content yet");
-            }
-        }
         MessageRole role = message.getRole();
+        if (role != MessageRole.USER) {
+            requireTextOnly(
+                    message,
+                    role == MessageRole.TOOL
+                            ? ((ToolResultBlock) 
message.getBlocks().get(0)).getBlocks()
+                            : message.getBlocks());
+        }
         String content = message.getText();
 
         switch (role) {
@@ -91,12 +102,7 @@ final class OpenAIResponsesUtils {
                 break;
 
             case USER:
-                items.add(
-                        ResponseInputItem.ofMessage(
-                                ResponseInputItem.Message.builder()
-                                        
.role(ResponseInputItem.Message.Role.USER)
-                                        .addInputTextContent(content)
-                                        .build()));
+                
items.add(ResponseInputItem.ofMessage(convertUserMessage(message, content)));
                 break;
 
             case ASSISTANT:
@@ -125,6 +131,71 @@ final class OpenAIResponsesUtils {
         return items;
     }
 
+    private static ResponseInputItem.Message convertUserMessage(ChatMessage 
message, String text) {
+        ResponseInputItem.Message.Builder builder =
+                
ResponseInputItem.Message.builder().role(ResponseInputItem.Message.Role.USER);
+        if (message.getBlocks().stream().noneMatch(block -> block instanceof 
MediaBlock)) {
+            // Unchanged for text-only messages: one input_text with the text 
projection.
+            return builder.addInputTextContent(text).build();
+        }
+        for (ContentBlock block : message.getBlocks()) {
+            if (block instanceof TextBlock) {
+                builder.addInputTextContent(((TextBlock) block).getText());
+            } else if (block instanceof ImageBlock) {
+                builder.addContent(
+                        ResponseInputImage.builder()
+                                .imageUrl(urlOrDataUri((MediaBlock) block))
+                                .detail(ResponseInputImage.Detail.AUTO)
+                                .build());
+            } else if (block instanceof DocumentBlock) {
+                builder.addContent(toInputFile((DocumentBlock) block));
+            } else {
+                throw unsupported(block, "the Responses API takes images and 
documents only");
+            }
+        }
+        return builder.build();
+    }
+
+    private static ResponseInputFile toInputFile(DocumentBlock document) {
+        ResponseInputFile.Builder file = ResponseInputFile.builder();
+        if (document.getSource() instanceof UrlSource) {
+            file.fileUrl(((UrlSource) document.getSource()).getUrl());
+            if (document.getName() != null) {
+                file.filename(document.getName());
+            }
+        } else {
+            file.fileData(urlOrDataUri(document))
+                    .filename(document.getName() != null ? document.getName() 
: "document");
+        }
+        return file.build();
+    }
+
+    private static String urlOrDataUri(MediaBlock block) {
+        if (block.getSource() instanceof UrlSource) {
+            return ((UrlSource) block.getSource()).getUrl();
+        }
+        return "data:"
+                + block.getMediaType()
+                + ";base64,"
+                + ((Base64Source) block.getSource()).getData();
+    }
+
+    private static void requireTextOnly(ChatMessage message, List<? extends 
ContentBlock> blocks) {
+        for (ContentBlock block : blocks) {
+            if (block instanceof MediaBlock) {
+                throw unsupported(
+                        block,
+                        "only user messages can carry media, not "
+                                + message.getRole().getValue()
+                                + " messages");
+            }
+        }
+    }
+
+    private static UnsupportedContentBlockException unsupported(ContentBlock 
block, String reason) {
+        return UnsupportedContentBlockException.forBlock("OpenAI Responses", 
block, reason);
+    }
+
     private static List<ResponseInputItem> convertAssistantMessage(ChatMessage 
message) {
         List<ResponseInputItem> items = new ArrayList<>();
         for (ContentBlock block : message.getBlocks()) {
diff --git 
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java
 
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java
index abe574270..2651d5b5d 100644
--- 
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java
+++ 
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesModelConnectionTest.java
@@ -30,8 +30,6 @@ import org.apache.flink.agents.api.chat.messages.ChatMessage;
 import org.apache.flink.agents.api.chat.messages.ImageBlock;
 import org.apache.flink.agents.api.chat.messages.MessageRole;
 import org.apache.flink.agents.api.chat.messages.ReasoningBlock;
-import org.apache.flink.agents.api.chat.messages.TextBlock;
-import 
org.apache.flink.agents.api.chat.messages.UnsupportedContentBlockException;
 import org.apache.flink.agents.api.chat.model.BaseChatModelConnection;
 import org.apache.flink.agents.api.resource.ResourceContext;
 import org.apache.flink.agents.api.resource.ResourceDescriptor;
@@ -282,23 +280,6 @@ class OpenAIResponsesModelConnectionTest {
         }
     }
 
-    @Test
-    @DisplayName("Media blocks fail explicitly instead of being dropped")
-    void testMediaBlocksFailExplicitly() {
-        List<ChatMessage> messages =
-                List.of(
-                        ChatMessage.user(
-                                List.of(
-                                        TextBlock.of("Describe this"),
-                                        ImageBlock.fromBase64("image/png", 
"aGVsbG8="))));
-
-        assertThatThrownBy(() -> connection().chat(messages, List.of(), new 
HashMap<>()))
-                .isInstanceOf(UnsupportedContentBlockException.class)
-                .hasMessage(
-                        "OpenAI Responses cannot send an image block 
(image/png, base64 source): this"
-                                + " integration sends text only.");
-    }
-
     @Test
     void testReasoningAndToolCallRoundTripPreservesOrder() throws Exception {
         ObjectMapper mapper = ObjectMappers.jsonMapper();
diff --git 
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesMultimodalTest.java
 
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesMultimodalTest.java
new file mode 100644
index 000000000..cf8469b1a
--- /dev/null
+++ 
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAIResponsesMultimodalTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.agents.integrations.chatmodels.openai;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.openai.core.ObjectMappers;
+import org.apache.flink.agents.api.chat.messages.AudioBlock;
+import org.apache.flink.agents.api.chat.messages.Base64Source;
+import org.apache.flink.agents.api.chat.messages.ChatMessage;
+import org.apache.flink.agents.api.chat.messages.ContentBlock;
+import org.apache.flink.agents.api.chat.messages.DocumentBlock;
+import org.apache.flink.agents.api.chat.messages.ImageBlock;
+import org.apache.flink.agents.api.chat.messages.TextBlock;
+import org.apache.flink.agents.api.chat.messages.ToolResultBlock;
+import 
org.apache.flink.agents.api.chat.messages.UnsupportedContentBlockException;
+import org.apache.flink.agents.api.chat.messages.UrlSource;
+import org.apache.flink.agents.api.chat.messages.VideoBlock;
+import org.apache.flink.agents.api.resource.ResourceContext;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests how content blocks become OpenAI Responses API input content. */
+class OpenAIResponsesMultimodalTest {
+
+    private static final String URL = "https://example.com/cat.png?sig=secret";;
+    private static final String BASE64 = "aGVsbG8="; // "hello"
+
+    /** The message's input item, as the SDK serializes it. */
+    private static JsonNode wire(ChatMessage message) {
+        List<com.openai.models.responses.ResponseInputItem> items =
+                OpenAIResponsesUtils.convertSingleMessage(message);
+        assertThat(items).hasSize(1);
+        return ObjectMappers.jsonMapper().valueToTree(items.get(0));
+    }
+
+    @Test
+    @DisplayName("A text-only user message is sent as before, as one 
input_text")
+    void testTextOnlyUserMessageIsUnchanged() {
+        JsonNode content = 
wire(ChatMessage.user(List.of(TextBlock.of("hi")))).get("content");
+
+        assertThat(content).hasSize(1);
+        assertThat(content.at("/0/type").asText()).isEqualTo("input_text");
+        assertThat(content.at("/0/text").asText()).isEqualTo("hi");
+    }
+
+    @Test
+    @DisplayName("Images become input_image in block order, by URL or as a 
data URI")
+    void testImagesBecomeInputImages() {
+        JsonNode content =
+                wire(ChatMessage.user(
+                                List.of(
+                                        TextBlock.of("Compare"),
+                                        ImageBlock.fromUrl("image/png", URL),
+                                        ImageBlock.fromBase64("image/png", 
BASE64))))
+                        .get("content");
+
+        assertThat(content).hasSize(3);
+        assertThat(content.at("/0/type").asText()).isEqualTo("input_text");
+        assertThat(content.at("/0/text").asText()).isEqualTo("Compare");
+        assertThat(content.at("/1/type").asText()).isEqualTo("input_image");
+        assertThat(content.at("/1/image_url").asText()).isEqualTo(URL);
+        assertThat(content.at("/1/detail").asText()).isEqualTo("auto");
+        assertThat(content.at("/2/image_url").asText())
+                .isEqualTo("data:image/png;base64," + BASE64);
+    }
+
+    @Test
+    @DisplayName(
+            "Documents become input_file: Base64 as file_data with a filename, 
URLs as file_url")
+    void testDocumentsBecomeInputFiles() {
+        JsonNode content =
+                wire(ChatMessage.user(
+                                List.of(
+                                        new DocumentBlock(
+                                                "application/pdf",
+                                                new Base64Source(BASE64),
+                                                "report.pdf",
+                                                null,
+                                                null),
+                                        
DocumentBlock.fromBase64("application/pdf", BASE64),
+                                        DocumentBlock.fromUrl(
+                                                "application/pdf", 
"https://example.com/a.pdf";),
+                                        new DocumentBlock(
+                                                "application/pdf",
+                                                new 
UrlSource("https://example.com/b.pdf";),
+                                                "b.pdf",
+                                                null,
+                                                null))))
+                        .get("content");
+
+        assertThat(content.at("/0/type").asText()).isEqualTo("input_file");
+        assertThat(content.at("/0/file_data").asText())
+                .isEqualTo("data:application/pdf;base64," + BASE64);
+        assertThat(content.at("/0/filename").asText()).isEqualTo("report.pdf");
+        assertThat(content.at("/1/filename").asText()).isEqualTo("document");
+        
assertThat(content.at("/2/file_url").asText()).isEqualTo("https://example.com/a.pdf";);
+        assertThat(content.get(2).has("file_data")).isFalse();
+        assertThat(content.get(2).has("filename")).isFalse();
+        
assertThat(content.at("/3/file_url").asText()).isEqualTo("https://example.com/b.pdf";);
+        assertThat(content.at("/3/filename").asText()).isEqualTo("b.pdf");
+    }
+
+    @Test
+    @DisplayName("Audio and video fail without leaking the source")
+    void testUnsupportedBlocksFailExplicitly() {
+        for (ContentBlock block :
+                List.<ContentBlock>of(
+                        AudioBlock.fromBase64("audio/wav", BASE64),
+                        VideoBlock.fromUrl("video/mp4", URL))) {
+            assertThatThrownBy(() -> 
wire(ChatMessage.user(List.of(TextBlock.of("hi"), block))))
+                    .isInstanceOf(UnsupportedContentBlockException.class)
+                    .hasMessageStartingWith("OpenAI Responses cannot send a")
+                    .hasMessageContaining(" " + block.getType() + " block")
+                    .hasMessageNotContaining("secret")
+                    .hasMessageNotContaining(BASE64);
+        }
+    }
+
+    @Test
+    @DisplayName("Media in assistant messages and tool results fails 
explicitly")
+    void testMediaOutsideUserMessagesFails() {
+        List<ContentBlock> blocks =
+                List.of(TextBlock.of("see"), ImageBlock.fromUrl("image/png", 
URL));
+        ChatMessage assistant = ChatMessage.assistant(blocks);
+        ChatMessage tool =
+                ChatMessage.tool(
+                        new ToolResultBlock(
+                                "call-1",
+                                List.of(TextBlock.of("see"), 
ImageBlock.fromUrl("image/png", URL)),
+                                false));
+
+        for (ChatMessage message : List.of(assistant, tool)) {
+            assertThatThrownBy(() -> 
OpenAIResponsesUtils.convertSingleMessage(message))
+                    .isInstanceOf(UnsupportedContentBlockException.class)
+                    .hasMessageContaining("only user messages can carry 
media");
+        }
+    }
+
+    @Test
+    @DisplayName("chat() surfaces media errors with their own types, before 
any request")
+    void testChatKeepsMediaErrorTypes() {
+        ResourceDescriptor descriptor =
+                ResourceDescriptor.Builder.newBuilder(
+                                OpenAIResponsesModelConnection.class.getName())
+                        .addInitialArgument("api_key", "test-key")
+                        .addInitialArgument("model", "gpt-4o")
+                        .build();
+        OpenAIResponsesModelConnection connection =
+                new OpenAIResponsesModelConnection(
+                        descriptor, ResourceContext.fromGetResource((a, b) -> 
null));
+        ChatMessage message = 
ChatMessage.user(List.of(AudioBlock.fromBase64("audio/wav", BASE64)));
+
+        assertThatThrownBy(() -> connection.chat(List.of(message), List.of(), 
Map.of()))
+                .isExactlyInstanceOf(UnsupportedContentBlockException.class);
+    }
+}

Reply via email to