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 a146caab [integrations][openai] Let provider exceptions reach the
caller unwrapped (#989)
a146caab is described below
commit a146caab4b3cff783fb6ef3c6aad255d4e12bdf6
Author: Weiqing Yang <[email protected]>
AuthorDate: Mon Aug 17 00:53:54 2026 -0700
[integrations][openai] Let provider exceptions reach the caller unwrapped
(#989)
Generated-by: Claude Code 2.1.226
---
.../openai/AzureOpenAIChatModelConnection.java | 12 +---
.../openai/OpenAICompletionsConnection.java | 46 ++++++-------
.../openai/OpenAIResponsesModelConnection.java | 36 +++++-----
.../openai/AzureOpenAIChatModelConnectionTest.java | 34 ++++++++++
.../chatmodels/openai/FakeOpenAIErrorEndpoint.java | 76 ++++++++++++++++++++++
.../openai/OpenAICompletionsConnectionTest.java | 48 ++++++++++++++
.../openai/OpenAIResponsesModelConnectionTest.java | 75 ++++++++++++++++++++-
7 files changed, 271 insertions(+), 56 deletions(-)
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java
index 9f9267f1..db3270db 100644
---
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnection.java
@@ -289,15 +289,9 @@ public class AzureOpenAIChatModelConnection extends
BaseChatModelConnection {
List<Tool> tools,
Map<String, Object> modelParams,
Object outputSchema) {
- try {
- ChatCompletionCreateParams params =
- buildRequest(messages, tools, modelParams, outputSchema);
- return toResponse(client.chat().completions().create(params),
modelParams);
- } catch (IllegalArgumentException e) {
- throw e;
- } catch (Exception e) {
- throw new RuntimeException("Failed to call Azure OpenAI chat
completions API.", e);
- }
+ ChatCompletionCreateParams params =
+ buildRequest(messages, tools, modelParams, outputSchema);
+ return toResponse(client.chat().completions().create(params),
modelParams);
}
// Package-private so response handling can be asserted against a
constructed completion without
diff --git
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnection.java
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnection.java
index f39c6ed3..3af78b72 100644
---
a/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnection.java
+++
b/integrations/chat-models/openai/src/main/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnection.java
@@ -191,33 +191,29 @@ public class OpenAICompletionsConnection extends
BaseChatModelConnection {
List<Tool> tools,
Map<String, Object> modelParams,
Object outputSchema) {
- try {
- ChatCompletionCreateParams params =
- buildRequest(messages, tools, modelParams, outputSchema);
- ChatCompletion completion =
client.chat().completions().create(params);
- ChatMessage response =
- OpenAIChatCompletionsUtils.convertFromOpenAIMessage(
- completion.choices().get(0).message());
-
- // Stash token usage
- if (completion.usage().isPresent()) {
- String modelName = modelParams != null ? (String)
modelParams.get("model") : null;
- if (modelName == null || modelName.isBlank()) {
- modelName = this.defaultModel;
- }
- if (modelName != null && !modelName.isBlank()) {
- response.getExtraArgs().put("model_name", modelName);
- response.getExtraArgs()
- .put("promptTokens",
completion.usage().get().promptTokens());
- response.getExtraArgs()
- .put("completionTokens",
completion.usage().get().completionTokens());
- }
+ ChatCompletionCreateParams params =
+ buildRequest(messages, tools, modelParams, outputSchema);
+ ChatCompletion completion = client.chat().completions().create(params);
+ ChatMessage response =
+ OpenAIChatCompletionsUtils.convertFromOpenAIMessage(
+ completion.choices().get(0).message());
+
+ // Stash token usage
+ if (completion.usage().isPresent()) {
+ String modelName = modelParams != null ? (String)
modelParams.get("model") : null;
+ if (modelName == null || modelName.isBlank()) {
+ modelName = this.defaultModel;
+ }
+ if (modelName != null && !modelName.isBlank()) {
+ response.getExtraArgs().put("model_name", modelName);
+ response.getExtraArgs()
+ .put("promptTokens",
completion.usage().get().promptTokens());
+ response.getExtraArgs()
+ .put("completionTokens",
completion.usage().get().completionTokens());
}
-
- return response;
- } catch (Exception e) {
- throw new RuntimeException("Failed to call OpenAI chat completions
API.", e);
}
+
+ return response;
}
// Package-private so the request body (including the native
response_format) can be asserted
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 85d26003..5dca0b6a 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
@@ -129,28 +129,24 @@ public class OpenAIResponsesModelConnection extends
BaseChatModelConnection {
List<ChatMessage> messages,
List<org.apache.flink.agents.api.tools.Tool> tools,
Map<String, Object> modelParams) {
- try {
- ResponseCreateParams params = buildRequest(messages, tools,
modelParams);
- Response response = client.responses().create(params);
- ChatMessage result = convertResponse(response);
-
- if (response.usage().isPresent()) {
- String modelName = modelParams != null ? (String)
modelParams.get("model") : null;
- if (modelName == null || modelName.isBlank()) {
- modelName = this.defaultModel;
- }
- if (modelName != null && !modelName.isBlank()) {
- result.getExtraArgs().put("model_name", modelName);
- result.getExtraArgs().put("promptTokens",
response.usage().get().inputTokens());
- result.getExtraArgs()
- .put("completionTokens",
response.usage().get().outputTokens());
- }
+ ResponseCreateParams params = buildRequest(messages, tools,
modelParams);
+ Response response = client.responses().create(params);
+ ChatMessage result = convertResponse(response);
+
+ if (response.usage().isPresent()) {
+ String modelName = modelParams != null ? (String)
modelParams.get("model") : null;
+ if (modelName == null || modelName.isBlank()) {
+ modelName = this.defaultModel;
+ }
+ if (modelName != null && !modelName.isBlank()) {
+ result.getExtraArgs().put("model_name", modelName);
+ result.getExtraArgs().put("promptTokens",
response.usage().get().inputTokens());
+ result.getExtraArgs()
+ .put("completionTokens",
response.usage().get().outputTokens());
}
-
- return result;
- } catch (Exception e) {
- throw new RuntimeException("Failed to call OpenAI Responses API.",
e);
}
+
+ return result;
}
private ResponseCreateParams buildRequest(
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java
index 2bda0db2..6b7b2d3b 100644
---
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/AzureOpenAIChatModelConnectionTest.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.integrations.chatmodels.openai;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.openai.errors.BadRequestException;
import com.openai.models.ChatModel;
import com.openai.models.ResponseFormatJsonSchema;
import com.openai.models.chat.completions.ChatCompletion;
@@ -43,6 +44,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
+import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
@@ -96,6 +98,18 @@ class AzureOpenAIChatModelConnectionTest {
return connection(CAPABLE_API_VERSION);
}
+ private static AzureOpenAIChatModelConnection connection(
+ String apiVersion, String azureEndpoint, String azureUrlPathMode) {
+ ResourceDescriptor desc =
+ connectionDescriptor()
+ .addInitialArgument("api_key", "test-key")
+ .addInitialArgument("api_version", apiVersion)
+ .addInitialArgument("azure_endpoint", azureEndpoint)
+ .addInitialArgument("azure_url_path_mode",
azureUrlPathMode)
+ .build();
+ return new AzureOpenAIChatModelConnection(desc, NOOP);
+ }
+
@Test
void testConnectionArgumentDefaultsAndZeroTimeout() {
AzureOpenAIChatModelConnection connection =
@@ -222,6 +236,26 @@ class AzureOpenAIChatModelConnectionTest {
.hasMessageContaining("temperature");
}
+ @Test
+ @DisplayName("A provider error reaches the caller as the SDK exception
carrying its payload")
+ void testProviderErrorPropagatesUnwrapped() throws IOException {
+ try (FakeOpenAIErrorEndpoint endpoint =
FakeOpenAIErrorEndpoint.rejectingWith400()) {
+ // A loopback endpoint is a custom gateway rather than an
*.openai.azure.com resource,
+ // so LEGACY is what builds the deployment-scoped Azure request
path against it.
+ AzureOpenAIChatModelConnection connection =
+ connection(CAPABLE_API_VERSION, endpoint.baseUrl(),
"LEGACY");
+
+ assertThatThrownBy(() -> connection.chat(userMessage(), List.of(),
params(null), null))
+ .isInstanceOfSatisfying(
+ BadRequestException.class,
+ e -> {
+ assertThat(e.statusCode()).isEqualTo(400);
+
assertThat(e.code()).contains(FakeOpenAIErrorEndpoint.ERROR_CODE);
+ })
+
.hasMessageContaining(FakeOpenAIErrorEndpoint.ERROR_MESSAGE);
+ }
+ }
+
@Test
@DisplayName("Native response_format json_schema strict applied for a POJO
on a capable model")
void testNativeAppliedForCapableDeploymentModel() {
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/FakeOpenAIErrorEndpoint.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/FakeOpenAIErrorEndpoint.java
new file mode 100644
index 00000000..78608f49
--- /dev/null
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/FakeOpenAIErrorEndpoint.java
@@ -0,0 +1,76 @@
+/*
+ * 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.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A loopback HTTP endpoint that answers every request with the provider error
envelope OpenAI
+ * returns for a rejected request, so a connection's error path can be
exercised without a live API
+ * call. A 400 is chosen because the SDK does not retry it, which keeps the
exchange to a single
+ * request.
+ */
+final class FakeOpenAIErrorEndpoint implements AutoCloseable {
+
+ static final String ERROR_MESSAGE = "The requested model does not exist.";
+ static final String ERROR_CODE = "model_not_found";
+
+ private static final String ERROR_BODY =
+ "{\"error\":{\"message\":\""
+ + ERROR_MESSAGE
+ + "\",\"type\":\"invalid_request_error\","
+ + "\"param\":\"model\",\"code\":\""
+ + ERROR_CODE
+ + "\"}}";
+
+ private final HttpServer server;
+
+ private FakeOpenAIErrorEndpoint(HttpServer server) {
+ this.server = server;
+ }
+
+ static FakeOpenAIErrorEndpoint rejectingWith400() throws IOException {
+ HttpServer server = HttpServer.create(new
InetSocketAddress("127.0.0.1", 0), 0);
+ byte[] body = ERROR_BODY.getBytes(StandardCharsets.UTF_8);
+ server.createContext(
+ "/",
+ exchange -> {
+ exchange.getResponseHeaders().add("Content-Type",
"application/json");
+ exchange.sendResponseHeaders(400, body.length);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+ server.setExecutor(null);
+ server.start();
+ return new FakeOpenAIErrorEndpoint(server);
+ }
+
+ String baseUrl() {
+ return "http://127.0.0.1:" + server.getAddress().getPort();
+ }
+
+ @Override
+ public void close() {
+ server.stop(0);
+ }
+}
diff --git
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java
index 1c5a4d15..269dee29 100644
---
a/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java
+++
b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.integrations.chatmodels.openai;
+import com.openai.errors.BadRequestException;
import com.openai.models.ResponseFormatJsonSchema;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import org.apache.flink.agents.api.chat.messages.ChatMessage;
@@ -33,6 +34,7 @@ import org.apache.flink.agents.api.tools.ToolType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
@@ -65,6 +67,16 @@ class OpenAICompletionsConnectionTest {
return new OpenAICompletionsConnection(desc, NOOP);
}
+ private static OpenAICompletionsConnection connection(String apiBaseUrl) {
+ ResourceDescriptor desc =
+
ResourceDescriptor.Builder.newBuilder(OpenAICompletionsConnection.class.getName())
+ .addInitialArgument("api_key", "test-key")
+ .addInitialArgument("api_base_url", apiBaseUrl)
+ .addInitialArgument("model", "gpt-4o")
+ .build();
+ return new OpenAICompletionsConnection(desc, NOOP);
+ }
+
private static Map<String, Object> params(String model) {
Map<String, Object> params = new HashMap<>();
params.put("model", model);
@@ -99,6 +111,42 @@ class OpenAICompletionsConnectionTest {
OpenAIClientTestUtils.assertNoTimeoutConfigured(connection);
}
+ @Test
+ @DisplayName("A request-building failure reaches the caller as its own
type, not a wrapper")
+ void testRequestBuildingFailurePropagatesUnwrapped() {
+ List<ChatMessage> toolMessageWithoutExternalId =
+ List.of(new ChatMessage(MessageRole.TOOL, "result", Map.of()));
+
+ assertThatThrownBy(
+ () ->
+ connection()
+ .chat(
+ toolMessageWithoutExternalId,
+ List.of(),
+ params("gpt-4o"),
+ null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("externalId");
+ }
+
+ @Test
+ @DisplayName("A provider error reaches the caller as the SDK exception
carrying its payload")
+ void testProviderErrorPropagatesUnwrapped() throws IOException {
+ try (FakeOpenAIErrorEndpoint endpoint =
FakeOpenAIErrorEndpoint.rejectingWith400()) {
+ OpenAICompletionsConnection connection =
connection(endpoint.baseUrl());
+
+ assertThatThrownBy(
+ () -> connection.chat(userMessage(), List.of(),
params("gpt-4o"), null))
+ .isInstanceOfSatisfying(
+ BadRequestException.class,
+ e -> {
+ assertThat(e.statusCode()).isEqualTo(400);
+
assertThat(e.code()).contains(FakeOpenAIErrorEndpoint.ERROR_CODE);
+ })
+
.hasMessageContaining(FakeOpenAIErrorEndpoint.ERROR_MESSAGE);
+ }
+ }
+
@Test
@DisplayName("Native response_format json_schema strict applied for a POJO
on a capable model")
void testNativeAppliedForPojoCapableModel() {
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 d5f0641c..dfb8d625 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
@@ -18,20 +18,27 @@
package org.apache.flink.agents.integrations.chatmodels.openai;
+import com.openai.errors.BadRequestException;
+import org.apache.flink.agents.api.chat.messages.ChatMessage;
+import org.apache.flink.agents.api.chat.messages.MessageRole;
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;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
- * Unit tests for {@link OpenAIResponsesModelConnection} — constructor
validation and default
- * resolution only, no network access.
+ * Unit tests for {@link OpenAIResponsesModelConnection} — constructor
validation, default
+ * resolution and error propagation, none of which need a live API call.
*/
class OpenAIResponsesModelConnectionTest {
@@ -42,6 +49,36 @@ class OpenAIResponsesModelConnectionTest {
OpenAIResponsesModelConnection.class.getName());
}
+ private static OpenAIResponsesModelConnection connection() {
+ ResourceDescriptor desc =
+ connectionDescriptor()
+ .addInitialArgument("api_key", "test-key")
+ .addInitialArgument("model", "gpt-4o")
+ .build();
+ return new OpenAIResponsesModelConnection(desc, NOOP);
+ }
+
+ private static OpenAIResponsesModelConnection connection(String
apiBaseUrl) {
+ ResourceDescriptor desc =
+ ResourceDescriptor.Builder.newBuilder(
+ OpenAIResponsesModelConnection.class.getName())
+ .addInitialArgument("api_key", "test-key")
+ .addInitialArgument("api_base_url", apiBaseUrl)
+ .addInitialArgument("model", "gpt-4o")
+ .build();
+ return new OpenAIResponsesModelConnection(desc, NOOP);
+ }
+
+ private static Map<String, Object> params(String model) {
+ Map<String, Object> params = new HashMap<>();
+ params.put("model", model);
+ return params;
+ }
+
+ private static List<ChatMessage> userMessage() {
+ return List.of(new ChatMessage(MessageRole.USER, "hi"));
+ }
+
@Test
@DisplayName("Constructor throws when api_key is missing")
void testConstructorMissingApiKey() {
@@ -201,4 +238,38 @@ class OpenAIResponsesModelConnectionTest {
OpenAIResponsesModelConnection conn = new
OpenAIResponsesModelConnection(desc, NOOP);
assertThat(conn.getMaxRetries()).isEqualTo(0);
}
+
+ @Test
+ @DisplayName("A request-building failure reaches the caller as its own
type, not a wrapper")
+ void testRequestBuildingFailurePropagatesUnwrapped() {
+ List<ChatMessage> toolMessageWithoutExternalId =
+ List.of(new ChatMessage(MessageRole.TOOL, "result", Map.of()));
+
+ assertThatThrownBy(
+ () ->
+ connection()
+ .chat(
+ toolMessageWithoutExternalId,
+ List.of(),
+ params("gpt-4o")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("externalId");
+ }
+
+ @Test
+ @DisplayName("A provider error reaches the caller as the SDK exception
carrying its payload")
+ void testProviderErrorPropagatesUnwrapped() throws IOException {
+ try (FakeOpenAIErrorEndpoint endpoint =
FakeOpenAIErrorEndpoint.rejectingWith400()) {
+ OpenAIResponsesModelConnection connection =
connection(endpoint.baseUrl());
+
+ assertThatThrownBy(() -> connection.chat(userMessage(), List.of(),
params("gpt-4o")))
+ .isInstanceOfSatisfying(
+ BadRequestException.class,
+ e -> {
+ assertThat(e.statusCode()).isEqualTo(400);
+
assertThat(e.code()).contains(FakeOpenAIErrorEndpoint.ERROR_CODE);
+ })
+
.hasMessageContaining(FakeOpenAIErrorEndpoint.ERROR_MESSAGE);
+ }
+ }
}