From 6129ca90025fbde88bd90cd9e59bd2e5559c1e35 Mon Sep 17 00:00:00 2001 From: weiqingy Date: Sat, 8 Aug 2026 21:12:21 -0700 Subject: [PATCH 1/2] [integrations][openai] Let provider exceptions reach the caller unwrapped The three OpenAI-family connections caught every exception from a chat call and rethrew it as a generic RuntimeException. The SDK exceptions already carry the HTTP status and the provider's error payload, so wrapping them forced callers to unwrap a cause to see what went wrong, and it diverged from the Python connections, which propagate the SDK exception as-is. The catch also covered request building, so a local validation failure such as a tool message missing its externalId surfaced as "Failed to call OpenAI chat completions API" rather than as the IllegalArgumentException it is. Generated-by: Claude Code 2.1.226 --- .../AzureOpenAIChatModelConnection.java | 12 ++--- .../openai/OpenAICompletionsConnection.java | 46 +++++++++---------- .../OpenAIResponsesModelConnection.java | 36 +++++++-------- .../OpenAICompletionsConnectionTest.java | 18 ++++++++ .../OpenAIResponsesModelConnectionTest.java | 41 ++++++++++++++++- 5 files changed, 97 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 9f9267f11..db3270db0 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 @@ private ChatMessage doChat( List tools, Map 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 f39c6ed3e..3af78b72c 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 @@ private ChatMessage doChat( List tools, Map 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 85d260032..5dca0b6a4 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 ChatMessage chat( List messages, List tools, Map 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/OpenAICompletionsConnectionTest.java b/integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/OpenAICompletionsConnectionTest.java index 1c5a4d15b..78bd007e3 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 @@ -99,6 +99,24 @@ void testConnectionArgumentValidation() { OpenAIClientTestUtils.assertNoTimeoutConfigured(connection); } + @Test + @DisplayName("A request-building failure reaches the caller as its own type, not a wrapper") + void testRequestBuildingFailurePropagatesUnwrapped() { + List 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("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 d5f0641c8..acfaf4675 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,6 +18,8 @@ package org.apache.flink.agents.integrations.chatmodels.openai; +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; @@ -25,13 +27,16 @@ import org.junit.jupiter.api.Test; 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 +47,21 @@ private static ResourceDescriptor.Builder connectionDescriptor() { 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 Map params(String model) { + Map params = new HashMap<>(); + params.put("model", model); + return params; + } + @Test @DisplayName("Constructor throws when api_key is missing") void testConstructorMissingApiKey() { @@ -201,4 +221,21 @@ void testZeroMaxRetriesAccepted() { 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 toolMessageWithoutExternalId = + List.of(new ChatMessage(MessageRole.TOOL, "result", Map.of())); + + assertThatThrownBy( + () -> + connection() + .chat( + toolMessageWithoutExternalId, + List.of(), + params("gpt-4o"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("externalId"); + } } From 0a00294b806c6d2d4b72c963c7c2ba2a09a44054 Mon Sep 17 00:00:00 2001 From: weiqingy Date: Mon, 10 Aug 2026 20:28:52 -0700 Subject: [PATCH 2/2] [integrations][openai] Cover provider error propagation with a local endpoint The existing tests only exercised a request-building IllegalArgumentException, so they would still pass if a future change caught and wrapped only OpenAI SDK exceptions, and the Azure path had no coverage of the change at all. FakeOpenAIErrorEndpoint serves an OpenAI error envelope over loopback, letting each connection's error path run without a live API call. One test per connection asserts the concrete BadRequestException and the code it carries from the provider payload. A 400 is used because the SDK does not retry it, which keeps the exchange to a single request. Generated-by: Claude Code 2.1.226 --- .../AzureOpenAIChatModelConnectionTest.java | 34 +++++++++ .../openai/FakeOpenAIErrorEndpoint.java | 76 +++++++++++++++++++ .../OpenAICompletionsConnectionTest.java | 30 ++++++++ .../OpenAIResponsesModelConnectionTest.java | 34 +++++++++ 4 files changed, 174 insertions(+) create mode 100644 integrations/chat-models/openai/src/test/java/org/apache/flink/agents/integrations/chatmodels/openai/FakeOpenAIErrorEndpoint.java 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 2bda0db2b..6b7b2d3b1 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.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 @@ private static AzureOpenAIChatModelConnection connection() { 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 @@ void testChatRejectsReservedKeyInAdditionalKwargs() { .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 000000000..78608f499 --- /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 78bd007e3..269dee296 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.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 @@ private static OpenAICompletionsConnection connection() { 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 params(String model) { Map params = new HashMap<>(); params.put("model", model); @@ -117,6 +129,24 @@ void testRequestBuildingFailurePropagatesUnwrapped() { .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 acfaf4675..dfb8d6255 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,6 +18,7 @@ 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; @@ -26,6 +27,7 @@ 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; @@ -56,12 +58,27 @@ private static OpenAIResponsesModelConnection connection() { 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 params(String model) { Map params = new HashMap<>(); params.put("model", model); return params; } + private static List userMessage() { + return List.of(new ChatMessage(MessageRole.USER, "hi")); + } + @Test @DisplayName("Constructor throws when api_key is missing") void testConstructorMissingApiKey() { @@ -238,4 +255,21 @@ void testRequestBuildingFailurePropagatesUnwrapped() { .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); + } + } }