From dd46fe30deb6e7388b65d3d5f51c5f810efe350a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:10:42 +0000 Subject: [PATCH 1/4] Initial plan From a4f6ebeb6868710cfe2ef885755a838ddec2ec0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:16:41 +0000 Subject: [PATCH 2/4] feat(llm): introduce LlmProvider abstraction backed by OpenAiCompatibleLlmProvider Agent-Logs-Url: https://github.com/VM8gkAs/WISE-API/sessions/0edbae90-5525-4e1b-8a67-448c4592fc5f Co-authored-by: VM8gkAs <61822684+VM8gkAs@users.noreply.github.com> --- .../web/AWSBedrockController.java | 57 +++---------- .../web/controllers/ChatGptController.java | 65 +++++---------- .../wise/portal/service/llm/LlmProvider.java | 31 +++++++ .../portal/service/llm/LlmProviderConfig.java | 62 ++++++++++++++ .../llm/impl/OpenAiCompatibleLlmProvider.java | 82 +++++++++++++++++++ .../resources/application_sample.properties | 20 ++++- .../impl/OpenAiCompatibleLlmProviderTest.java | 53 ++++++++++++ 7 files changed, 281 insertions(+), 89 deletions(-) create mode 100644 src/main/java/org/wise/portal/service/llm/LlmProvider.java create mode 100644 src/main/java/org/wise/portal/service/llm/LlmProviderConfig.java create mode 100644 src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java create mode 100644 src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java diff --git a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java index 253c64c67..016b8b46d 100644 --- a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java +++ b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java @@ -1,67 +1,36 @@ package org.wise.portal.presentation.web; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.service.llm.LlmProvider; +/** + * REST endpoint that forwards chat-completion requests to the AWS Bedrock LLM provider. + * + *
The actual HTTP call is delegated to the {@link LlmProvider} abstraction, keeping + * this controller free of provider-specific details. To switch or extend the underlying + * AI backend, register a different {@link LlmProvider} bean named {@code "bedrockLlmProvider"} + * in {@link org.wise.portal.service.llm.LlmProviderConfig}. + */ @RestController @RequestMapping("/api/aws-bedrock/chat") public class AWSBedrockController { @Autowired - Environment appProperties; + @Qualifier("bedrockLlmProvider") + private LlmProvider llmProvider; @ResponseBody @Secured("ROLE_USER") @PostMapping protected String sendChatMessage(@RequestBody String body) { - String apiKey = appProperties.getProperty("aws.bedrock.api.key"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("aws.bedrock.api.key is not set"); - } - String apiEndpoint = appProperties.getProperty("aws.bedrock.runtime.endpoint"); - if (apiEndpoint == null || apiEndpoint.isEmpty()) { - throw new RuntimeException("aws.bedrock.runtime.endpoint is not set"); - } - // assume openai-only support for now. We'll add other models later. - apiEndpoint += "/openai/v1/chat/completions"; - - try { - URL url = new URL(apiEndpoint); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Authorization", "Bearer " + apiKey); - connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - connection.setRequestProperty("Accept-Charset", "UTF-8"); - connection.setDoOutput(true); - OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); - writer.write(body); - writer.flush(); - writer.close(); - BufferedReader br = new BufferedReader( - new InputStreamReader(connection.getInputStream(), "UTF-8")); - String line; - StringBuffer response = new StringBuffer(); - while ((line = br.readLine()) != null) { - response.append(line); - } - br.close(); - return response.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } + return llmProvider.chat(body); } } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java index 995a8e51c..67b550876 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java @@ -1,59 +1,36 @@ package org.wise.portal.presentation.web.controllers; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; -import java.net.URL; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.wise.portal.service.llm.LlmProvider; +/** + * REST endpoint that forwards chat-completion requests to the OpenAI LLM provider. + * + *
The actual HTTP call is delegated to the {@link LlmProvider} abstraction. The + * {@code openai.chat.api.url} property may point to any OpenAI-compatible endpoint, + * including local gateways such as Ollama or vLLM. + * + * @see org.wise.portal.service.llm.LlmProviderConfig + */ @RestController @RequestMapping("/api/chat-gpt") public class ChatGptController { - @Value("${openai.api.key:}") - private String openAiApiKey; + @Autowired + @Qualifier("openAiLlmProvider") + private LlmProvider llmProvider; - @Value("${openai.chat.api.url:https://api.openai.com/v1/chat/completions}") - private String openAiChatApiUrl; - - @ResponseBody - @Secured("ROLE_USER") - @PostMapping - protected String sendChatMessage(@RequestBody String body) { - if (openAiApiKey == null || openAiApiKey.isEmpty()) { - throw new RuntimeException("openai.api.key is not set"); - } - try { - URL url = new URL(openAiChatApiUrl); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Authorization", "Bearer " + openAiApiKey); - connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - connection.setRequestProperty("Accept-Charset", "UTF-8"); - connection.setDoOutput(true); - OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); - writer.write(body); - writer.flush(); - writer.close(); - BufferedReader br = new BufferedReader( - new InputStreamReader(connection.getInputStream(), "UTF-8")); - String line; - StringBuffer response = new StringBuffer(); - while ((line = br.readLine()) != null) { - response.append(line); - } - br.close(); - return response.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + @ResponseBody + @Secured("ROLE_USER") + @PostMapping + protected String sendChatMessage(@RequestBody String body) { + return llmProvider.chat(body); + } } diff --git a/src/main/java/org/wise/portal/service/llm/LlmProvider.java b/src/main/java/org/wise/portal/service/llm/LlmProvider.java new file mode 100644 index 000000000..be9af43ba --- /dev/null +++ b/src/main/java/org/wise/portal/service/llm/LlmProvider.java @@ -0,0 +1,31 @@ +package org.wise.portal.service.llm; + +/** + * Abstraction for AI Language Model providers that support chat completion. + * + *
Concrete implementations wrap a specific backend (e.g. OpenAI API, AWS Bedrock, + * or a local OpenAI-compatible gateway such as Ollama/vLLM) while exposing a uniform + * interface to callers. + * + *
Future providers (Gemini, Claude, OpenAI-compatible local models) should implement + * this interface and be registered as Spring beans via {@link LlmProviderConfig}. + * + * @author WISE Contributors + */ +public interface LlmProvider { + + /** + * Send a chat-completion request and return the provider's raw JSON response. + * + * @param requestBody JSON request body in the OpenAI chat-completion format + * @return raw JSON response string from the provider + * @throws RuntimeException if the provider is not configured or the upstream call fails + */ + String chat(String requestBody); + + /** + * Short, human-readable identifier for this provider (e.g. {@code "aws-bedrock"}, + * {@code "openai"}). Used for logging and future capability-based routing decisions. + */ + String getName(); +} diff --git a/src/main/java/org/wise/portal/service/llm/LlmProviderConfig.java b/src/main/java/org/wise/portal/service/llm/LlmProviderConfig.java new file mode 100644 index 000000000..1cda4e46f --- /dev/null +++ b/src/main/java/org/wise/portal/service/llm/LlmProviderConfig.java @@ -0,0 +1,62 @@ +package org.wise.portal.service.llm; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.wise.portal.service.llm.impl.OpenAiCompatibleLlmProvider; + +/** + * Spring configuration that creates named {@link LlmProvider} beans from application properties. + * + *
Each AI endpoint used by WISE gets its own named bean so that controllers can inject the + * right provider without knowing implementation details. Adding a new provider in the future + * (e.g. Gemini, Claude, or a local Ollama gateway) requires only: + *
Relevant application properties: + *
+ * # AWS Bedrock (OpenAI-compatible runtime) + * aws.bedrock.api.key= + * aws.bedrock.runtime.endpoint= + * + * # OpenAI + * openai.api.key= + * openai.chat.api.url=https://api.openai.com/v1/chat/completions + *+ * + * @author WISE Contributors + */ +@Configuration +public class LlmProviderConfig { + + /** + * Provider backed by AWS Bedrock's OpenAI-compatible runtime endpoint. + * + *
Bedrock exposes an {@code /openai/v1/chat/completions} path on top of the configured + * runtime endpoint, making it compatible with the same HTTP adapter used for OpenAI. + */ + @Bean("bedrockLlmProvider") + public LlmProvider bedrockLlmProvider( + @Value("${aws.bedrock.api.key:}") String apiKey, + @Value("${aws.bedrock.runtime.endpoint:}") String runtimeEndpoint) { + String chatApiUrl = (runtimeEndpoint == null || runtimeEndpoint.isEmpty()) + ? "" + : runtimeEndpoint + "/openai/v1/chat/completions"; + return new OpenAiCompatibleLlmProvider("aws-bedrock", apiKey, chatApiUrl); + } + + /** + * Provider backed by the OpenAI API (or any OpenAI-compatible endpoint configured via + * {@code openai.chat.api.url}, e.g. a local Ollama/vLLM gateway). + */ + @Bean("openAiLlmProvider") + public LlmProvider openAiLlmProvider( + @Value("${openai.api.key:}") String apiKey, + @Value("${openai.chat.api.url:https://api.openai.com/v1/chat/completions}") String chatApiUrl) { + return new OpenAiCompatibleLlmProvider("openai", apiKey, chatApiUrl); + } +} diff --git a/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java b/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java new file mode 100644 index 000000000..4bbf6360b --- /dev/null +++ b/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java @@ -0,0 +1,82 @@ +package org.wise.portal.service.llm.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.wise.portal.service.llm.LlmProvider; + +/** + * {@link LlmProvider} implementation for any OpenAI-compatible chat-completion endpoint. + * + *
This single class covers: + *
Instances are created by {@link org.wise.portal.service.llm.LlmProviderConfig} + * and injected into controllers by name. + * + * @author WISE Contributors + */ +public class OpenAiCompatibleLlmProvider implements LlmProvider { + + private final String name; + private final String apiKey; + private final String chatApiUrl; + + /** + * @param name short provider identifier used in logs and routing (e.g. {@code "openai"}) + * @param apiKey bearer token / API key sent in the {@code Authorization} header + * @param chatApiUrl full URL of the chat-completion endpoint + */ + public OpenAiCompatibleLlmProvider(String name, String apiKey, String chatApiUrl) { + this.name = name; + this.apiKey = apiKey; + this.chatApiUrl = chatApiUrl; + } + + @Override + public String chat(String requestBody) { + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("API key is not configured for LLM provider: " + name); + } + if (chatApiUrl == null || chatApiUrl.isEmpty()) { + throw new RuntimeException("Chat API URL is not configured for LLM provider: " + name); + } + try { + URL url = new URL(chatApiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + apiKey); + connection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + connection.setRequestProperty("Accept-Charset", "UTF-8"); + connection.setDoOutput(true); + OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); + writer.write(requestBody); + writer.flush(); + writer.close(); + BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), "UTF-8")); + String line; + StringBuffer response = new StringBuffer(); + while ((line = br.readLine()) != null) { + response.append(line); + } + br.close(); + return response.toString(); + } catch (IOException e) { + throw new RuntimeException("Chat request failed for LLM provider: " + name, e); + } + } + + @Override + public String getName() { + return name; + } +} diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index 9c9e4666d..c9ea08cca 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -223,7 +223,25 @@ aws.accessKeyId= aws.secretAccessKey= aws.region= -# OpenAI and AWS Bedrock Chat endpoints (optional) +########## AI / LLM Provider Configuration (optional) ########## +# +# WISE supports multiple AI providers through a unified LlmProvider abstraction. +# Configure one or more of the following backends by uncommenting and filling in the values. +# +# --- AWS Bedrock (OpenAI-compatible runtime) --- +# aws.bedrock.api.key= bearer token for the Bedrock runtime endpoint +# aws.bedrock.runtime.endpoint= base URL of the Bedrock runtime +# (path /openai/v1/chat/completions is appended automatically) +# +# --- OpenAI (or any OpenAI-compatible endpoint) --- +# openai.api.key= API key issued by OpenAI (or your local gateway) +# openai.chat.api.url= full URL of the chat-completions endpoint +# Default: https://api.openai.com/v1/chat/completions +# Override with a local gateway URL (e.g. Ollama, vLLM) to avoid +# sending data to a public cloud. +# +# Future providers (Gemini, Claude, etc.) will follow the same pattern: +# add the corresponding properties and a new @Bean in LlmProviderConfig. #openai.api.key= #openai.chat.api.url= #aws.bedrock.api.key= diff --git a/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java b/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java new file mode 100644 index 000000000..8c85abd01 --- /dev/null +++ b/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java @@ -0,0 +1,53 @@ +package org.wise.portal.service.llm.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.jupiter.api.Test; +import org.wise.portal.service.llm.LlmProvider; + +/** + * Unit tests for {@link OpenAiCompatibleLlmProvider}. + */ +public class OpenAiCompatibleLlmProviderTest { + + @Test + public void getName_ReturnsConfiguredName() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("openai", "test-key", + "https://api.openai.com/v1/chat/completions"); + assertEquals("openai", provider.getName()); + } + + @Test + public void getName_BedrockProviderName_ReturnsCorrectName() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("aws-bedrock", "test-key", + "https://bedrock.example.com/openai/v1/chat/completions"); + assertEquals("aws-bedrock", provider.getName()); + } + + @Test + public void chat_MissingApiKey_ThrowsRuntimeException() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("openai", "", + "https://api.openai.com/v1/chat/completions"); + assertThrows(RuntimeException.class, () -> provider.chat("{}")); + } + + @Test + public void chat_NullApiKey_ThrowsRuntimeException() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("openai", null, + "https://api.openai.com/v1/chat/completions"); + assertThrows(RuntimeException.class, () -> provider.chat("{}")); + } + + @Test + public void chat_MissingChatApiUrl_ThrowsRuntimeException() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("aws-bedrock", "test-key", ""); + assertThrows(RuntimeException.class, () -> provider.chat("{}")); + } + + @Test + public void chat_NullChatApiUrl_ThrowsRuntimeException() { + LlmProvider provider = new OpenAiCompatibleLlmProvider("aws-bedrock", "test-key", null); + assertThrows(RuntimeException.class, () -> provider.chat("{}")); + } +} From 26ef1bec2e5591c7cfdc879ebb62cea204a0cbf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:19:55 +0000 Subject: [PATCH 3/4] fix(llm): use StringBuilder, add produces=JSON to prevent XSS Agent-Logs-Url: https://github.com/VM8gkAs/WISE-API/sessions/0edbae90-5525-4e1b-8a67-448c4592fc5f Co-authored-by: VM8gkAs <61822684+VM8gkAs@users.noreply.github.com> --- .../org/wise/portal/presentation/web/AWSBedrockController.java | 3 ++- .../portal/presentation/web/controllers/ChatGptController.java | 3 ++- .../portal/service/llm/impl/OpenAiCompatibleLlmProvider.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java index 016b8b46d..7a793d80f 100644 --- a/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java +++ b/src/main/java/org/wise/portal/presentation/web/AWSBedrockController.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -28,7 +29,7 @@ public class AWSBedrockController { @ResponseBody @Secured("ROLE_USER") - @PostMapping + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) protected String sendChatMessage(@RequestBody String body) { return llmProvider.chat(body); } diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java b/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java index 67b550876..b5f165b20 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/ChatGptController.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.annotation.Secured; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -29,7 +30,7 @@ public class ChatGptController { @ResponseBody @Secured("ROLE_USER") - @PostMapping + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) protected String sendChatMessage(@RequestBody String body) { return llmProvider.chat(body); } diff --git a/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java b/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java index 4bbf6360b..ce970f808 100644 --- a/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java +++ b/src/main/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProvider.java @@ -64,7 +64,7 @@ public String chat(String requestBody) { BufferedReader br = new BufferedReader( new InputStreamReader(connection.getInputStream(), "UTF-8")); String line; - StringBuffer response = new StringBuffer(); + StringBuilder response = new StringBuilder(); while ((line = br.readLine()) != null) { response.append(line); } From 88773a17a4a6e867ee92869f10e0239207a731e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:22:20 +0000 Subject: [PATCH 4/4] test(llm): use JUnit 5 assertions and validate exception messages Agent-Logs-Url: https://github.com/VM8gkAs/WISE-API/sessions/0edbae90-5525-4e1b-8a67-448c4592fc5f Co-authored-by: VM8gkAs <61822684+VM8gkAs@users.noreply.github.com> --- .../impl/OpenAiCompatibleLlmProviderTest.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java b/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java index 8c85abd01..287fd4140 100644 --- a/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java +++ b/src/test/java/org/wise/portal/service/llm/impl/OpenAiCompatibleLlmProviderTest.java @@ -1,7 +1,8 @@ package org.wise.portal.service.llm.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.wise.portal.service.llm.LlmProvider; @@ -29,25 +30,31 @@ public void getName_BedrockProviderName_ReturnsCorrectName() { public void chat_MissingApiKey_ThrowsRuntimeException() { LlmProvider provider = new OpenAiCompatibleLlmProvider("openai", "", "https://api.openai.com/v1/chat/completions"); - assertThrows(RuntimeException.class, () -> provider.chat("{}")); + RuntimeException ex = assertThrows(RuntimeException.class, () -> provider.chat("{}")); + assertTrue(ex.getMessage().contains("API key is not configured for LLM provider: openai")); } @Test public void chat_NullApiKey_ThrowsRuntimeException() { LlmProvider provider = new OpenAiCompatibleLlmProvider("openai", null, "https://api.openai.com/v1/chat/completions"); - assertThrows(RuntimeException.class, () -> provider.chat("{}")); + RuntimeException ex = assertThrows(RuntimeException.class, () -> provider.chat("{}")); + assertTrue(ex.getMessage().contains("API key is not configured for LLM provider: openai")); } @Test public void chat_MissingChatApiUrl_ThrowsRuntimeException() { LlmProvider provider = new OpenAiCompatibleLlmProvider("aws-bedrock", "test-key", ""); - assertThrows(RuntimeException.class, () -> provider.chat("{}")); + RuntimeException ex = assertThrows(RuntimeException.class, () -> provider.chat("{}")); + assertTrue( + ex.getMessage().contains("Chat API URL is not configured for LLM provider: aws-bedrock")); } @Test public void chat_NullChatApiUrl_ThrowsRuntimeException() { LlmProvider provider = new OpenAiCompatibleLlmProvider("aws-bedrock", "test-key", null); - assertThrows(RuntimeException.class, () -> provider.chat("{}")); + RuntimeException ex = assertThrows(RuntimeException.class, () -> provider.chat("{}")); + assertTrue( + ex.getMessage().contains("Chat API URL is not configured for LLM provider: aws-bedrock")); } }