This is an automated email from the ASF dual-hosted git repository. lizhimins pushed a commit to branch rocketmq-studio in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
commit 29d493aa4ae68d99d0da498b40d423b8735a2709 Author: aias00 <[email protected]> AuthorDate: Thu Jul 23 22:51:30 2026 -0700 feat: add LLM settings endpoints (#506) Add LLM settings backend endpoints: config read/write, connectivity test, and model listing. --- .../rocketmq/studio/ops/ai/LlmConfigService.java | 168 +++++++++++++++++++++ .../com/rocketmq/studio/ops/ai/LlmConfigVO.java | 40 +++++ .../com/rocketmq/studio/ops/ai/LlmController.java | 54 +++++++ .../com/rocketmq/studio/ops/ai/LlmModelItemVO.java | 30 ++++ .../rocketmq/studio/ops/ai/LlmModelsResultVO.java | 32 ++++ .../studio/ops/ai/LlmOperationResultVO.java | 39 +++++ .../studio/ops/ai/LlmConfigServiceTest.java | 129 ++++++++++++++++ .../rocketmq/studio/ops/ai/LlmControllerTest.java | 119 +++++++++++++++ 8 files changed, 611 insertions(+) diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigService.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigService.java new file mode 100644 index 00000000..2134f2b9 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigService.java @@ -0,0 +1,168 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import com.rocketmq.studio.settings.GeneralSettingsVO; +import com.rocketmq.studio.settings.SettingsService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class LlmConfigService { + + private static final String OPENAI = "openai"; + private static final Map<String, List<LlmModelItemVO>> PROVIDER_MODELS = Map.of( + OPENAI, List.of( + new LlmModelItemVO("gpt-4o", "GPT-4o"), + new LlmModelItemVO("gpt-4-turbo", "GPT-4 Turbo"), + new LlmModelItemVO("gpt-4", "GPT-4")), + "azure", List.of( + new LlmModelItemVO("gpt-4o", "GPT-4o"), + new LlmModelItemVO("gpt-4", "GPT-4")), + "deepseek", List.of( + new LlmModelItemVO("deepseek-chat", "DeepSeek Chat"), + new LlmModelItemVO("deepseek-reasoner", "DeepSeek Reasoner")), + "tongyi", List.of( + new LlmModelItemVO("qwen-max", "Qwen Max"), + new LlmModelItemVO("qwen-plus", "Qwen Plus"), + new LlmModelItemVO("qwen-turbo", "Qwen Turbo")), + "ollama", List.of( + new LlmModelItemVO("llama3", "Llama 3"), + new LlmModelItemVO("mistral", "Mistral"), + new LlmModelItemVO("qwen2.5", "Qwen 2.5")), + "bedrock", List.of( + new LlmModelItemVO("anthropic.claude-3-sonnet", "Claude 3 Sonnet"), + new LlmModelItemVO("anthropic.claude-3-haiku", "Claude 3 Haiku"), + new LlmModelItemVO("meta.llama3-70b", "Llama 3 70B"))); + + private final SettingsService settingsService; + private LlmConfigVO overrides; + + public synchronized LlmConfigVO getConfig() { + if (overrides != null) { + return copy(overrides); + } + return fromGeneralSettings(settingsService.getGeneralSettings()); + } + + public synchronized void saveConfig(LlmConfigVO config) { + LlmConfigVO normalized = normalize(config); + overrides = copy(normalized); + GeneralSettingsVO current = settingsService.getGeneralSettings(); + settingsService.saveGeneralSettings(GeneralSettingsVO.builder() + .theme(current.getTheme()) + .compact(current.isCompact()) + .desktopNotify(current.isDesktopNotify()) + .notifySound(current.isNotifySound()) + .sessionTimeout(current.getSessionTimeout()) + .requireLogin(current.isRequireLogin()) + .llmProvider(normalized.getProvider()) + .apiKey(normalized.getApiKey()) + .model(normalized.getModel()) + .baseUrl(normalized.getApiBase()) + .build()); + } + + public LlmOperationResultVO testConfig(LlmConfigVO config) { + LlmConfigVO normalized = normalize(config); + String provider = normalized.getProvider(); + boolean keyRequired = !"ollama".equals(provider); + if (keyRequired && isBlank(normalized.getApiKey())) { + return LlmOperationResultVO.failure("API Key is required"); + } + if ("azure".equals(provider) && isBlank(normalized.getDeploymentName())) { + return LlmOperationResultVO.failure("Deployment name is required"); + } + if (isBlank(normalized.getModel())) { + return LlmOperationResultVO.failure("Model is required"); + } + return LlmOperationResultVO.success("Configuration accepted"); + } + + public synchronized LlmModelsResultVO listModels() { + String provider = getConfig().getProvider(); + List<LlmModelItemVO> models = PROVIDER_MODELS.getOrDefault(provider, PROVIDER_MODELS.get(OPENAI)); + return new LlmModelsResultVO(0, models); + } + + private LlmConfigVO fromGeneralSettings(GeneralSettingsVO settings) { + String provider = defaultString(settings.getLlmProvider(), OPENAI); + return LlmConfigVO.builder() + .provider(provider) + .apiKey(defaultString(settings.getApiKey(), "")) + .apiBase(defaultString(settings.getBaseUrl(), defaultApiBase(provider))) + .model(defaultString(settings.getModel(), defaultModel(provider))) + .maxTokens(4096) + .temperature(0.7) + .enabled(!isBlank(settings.getApiKey())) + .apiVersion("2024-02-15-preview") + .awsRegion("us-east-1") + .build(); + } + + private LlmConfigVO normalize(LlmConfigVO config) { + String provider = normalizeProvider(config == null ? null : config.getProvider()); + return LlmConfigVO.builder() + .provider(provider) + .apiKey(defaultString(config == null ? null : config.getApiKey(), "")) + .apiBase(defaultString(config == null ? null : config.getApiBase(), defaultApiBase(provider))) + .model(defaultString(config == null ? null : config.getModel(), defaultModel(provider))) + .maxTokens(config == null || config.getMaxTokens() <= 0 ? 4096 : config.getMaxTokens()) + .temperature(config == null ? 0.7 : config.getTemperature()) + .enabled(config != null && config.isEnabled()) + .deploymentName(defaultString(config == null ? null : config.getDeploymentName(), "")) + .apiVersion(defaultString(config == null ? null : config.getApiVersion(), "2024-02-15-preview")) + .awsRegion(defaultString(config == null ? null : config.getAwsRegion(), "us-east-1")) + .build(); + } + + private LlmConfigVO copy(LlmConfigVO config) { + return normalize(config); + } + + private String normalizeProvider(String provider) { + String normalized = defaultString(provider, OPENAI).toLowerCase(Locale.ROOT); + return PROVIDER_MODELS.containsKey(normalized) ? normalized : OPENAI; + } + + private String defaultModel(String provider) { + return PROVIDER_MODELS.getOrDefault(provider, PROVIDER_MODELS.get(OPENAI)).get(0).getId(); + } + + private String defaultApiBase(String provider) { + return switch (provider) { + case "deepseek" -> "https://api.deepseek.com/v1"; + case "tongyi" -> "https://dashscope.aliyuncs.com/compatible-mode/v1"; + case "ollama" -> "http://localhost:11434/v1"; + default -> "https://api.openai.com/v1"; + }; + } + + private String defaultString(String value, String fallback) { + return isBlank(value) ? fallback : value.trim(); + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigVO.java new file mode 100644 index 00000000..85d6460c --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmConfigVO.java @@ -0,0 +1,40 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LlmConfigVO { + private String provider; + private String apiKey; + private String apiBase; + private String model; + private int maxTokens; + private double temperature; + private boolean enabled; + private String deploymentName; + private String apiVersion; + private String awsRegion; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmController.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmController.java new file mode 100644 index 00000000..8f7e08bc --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmController.java @@ -0,0 +1,54 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +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.RestController; + +@RestController +@RequestMapping("/api/llm") +@RequiredArgsConstructor +public class LlmController { + + private final LlmConfigService llmConfigService; + + @GetMapping("/config") + public LlmConfigVO getConfig() { + return llmConfigService.getConfig(); + } + + @PostMapping("/config") + public LlmOperationResultVO saveConfig(@RequestBody LlmConfigVO config) { + llmConfigService.saveConfig(config); + return LlmOperationResultVO.success("saved"); + } + + @PostMapping("/config/test") + public LlmOperationResultVO testConfig(@RequestBody LlmConfigVO config) { + return llmConfigService.testConfig(config); + } + + @GetMapping("/models") + public LlmModelsResultVO listModels() { + return llmConfigService.listModels(); + } +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelItemVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelItemVO.java new file mode 100644 index 00000000..0a49758f --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelItemVO.java @@ -0,0 +1,30 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LlmModelItemVO { + private String id; + private String name; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelsResultVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelsResultVO.java new file mode 100644 index 00000000..0e95658f --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmModelsResultVO.java @@ -0,0 +1,32 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LlmModelsResultVO { + private int status; + private List<LlmModelItemVO> data; +} diff --git a/server/src/main/java/com/rocketmq/studio/ops/ai/LlmOperationResultVO.java b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmOperationResultVO.java new file mode 100644 index 00000000..2fcfebf6 --- /dev/null +++ b/server/src/main/java/com/rocketmq/studio/ops/ai/LlmOperationResultVO.java @@ -0,0 +1,39 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LlmOperationResultVO { + private int status; + private String msg; + private String errMsg; + + public static LlmOperationResultVO success(String message) { + return new LlmOperationResultVO(0, message, null); + } + + public static LlmOperationResultVO failure(String message) { + return new LlmOperationResultVO(1, null, message); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/ai/LlmConfigServiceTest.java b/server/src/test/java/com/rocketmq/studio/ops/ai/LlmConfigServiceTest.java new file mode 100644 index 00000000..a916c5dd --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/ai/LlmConfigServiceTest.java @@ -0,0 +1,129 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import com.rocketmq.studio.settings.GeneralSettingsVO; +import com.rocketmq.studio.settings.SettingsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LlmConfigServiceTest { + + private SettingsService settingsService; + private LlmConfigService llmConfigService; + + @BeforeEach + void setUp() { + settingsService = mock(SettingsService.class); + when(settingsService.getGeneralSettings()).thenReturn(GeneralSettingsVO.builder() + .theme("dark") + .compact(true) + .desktopNotify(true) + .notifySound(false) + .sessionTimeout(45) + .requireLogin(true) + .llmProvider("openai") + .apiKey("sk-test") + .model("gpt-4o") + .baseUrl("https://api.openai.com/v1") + .build()); + llmConfigService = new LlmConfigService(settingsService); + } + + @Test + void getConfigShouldMapGeneralSettingsToLlmConfig() { + LlmConfigVO config = llmConfigService.getConfig(); + + assertThat(config.getProvider()).isEqualTo("openai"); + assertThat(config.getApiKey()).isEqualTo("sk-test"); + assertThat(config.getApiBase()).isEqualTo("https://api.openai.com/v1"); + assertThat(config.getModel()).isEqualTo("gpt-4o"); + assertThat(config.isEnabled()).isTrue(); + } + + @Test + void saveConfigShouldPreserveGeneralSettingsAndStoreLlmFields() { + LlmConfigVO config = LlmConfigVO.builder() + .provider("deepseek") + .apiKey("sk-deepseek") + .apiBase("https://api.deepseek.com/v1") + .model("deepseek-chat") + .maxTokens(8192) + .temperature(0.2) + .enabled(true) + .build(); + + llmConfigService.saveConfig(config); + + ArgumentCaptor<GeneralSettingsVO> captor = ArgumentCaptor.forClass(GeneralSettingsVO.class); + verify(settingsService).saveGeneralSettings(captor.capture()); + GeneralSettingsVO saved = captor.getValue(); + assertThat(saved.getTheme()).isEqualTo("dark"); + assertThat(saved.isCompact()).isTrue(); + assertThat(saved.getLlmProvider()).isEqualTo("deepseek"); + assertThat(saved.getApiKey()).isEqualTo("sk-deepseek"); + assertThat(saved.getModel()).isEqualTo("deepseek-chat"); + assertThat(saved.getBaseUrl()).isEqualTo("https://api.deepseek.com/v1"); + assertThat(llmConfigService.getConfig().getProvider()).isEqualTo("deepseek"); + } + + @Test + void testConfigShouldRejectMissingRequiredApiKey() { + LlmOperationResultVO result = llmConfigService.testConfig(LlmConfigVO.builder() + .provider("openai") + .apiKey("") + .model("gpt-4o") + .build()); + + assertThat(result.getStatus()).isEqualTo(1); + assertThat(result.getErrMsg()).isEqualTo("API Key is required"); + } + + @Test + void testConfigShouldAllowOllamaWithoutApiKey() { + LlmOperationResultVO result = llmConfigService.testConfig(LlmConfigVO.builder() + .provider("ollama") + .apiBase("http://localhost:11434/v1") + .model("llama3") + .build()); + + assertThat(result.getStatus()).isZero(); + assertThat(result.getMsg()).isEqualTo("Configuration accepted"); + } + + @Test + void listModelsShouldUseSavedProvider() { + llmConfigService.saveConfig(LlmConfigVO.builder() + .provider("tongyi") + .apiKey("dashscope-key") + .model("qwen-plus") + .enabled(true) + .build()); + + LlmModelsResultVO result = llmConfigService.listModels(); + + assertThat(result.getStatus()).isZero(); + assertThat(result.getData()).extracting("id").contains("qwen-max", "qwen-plus"); + } +} diff --git a/server/src/test/java/com/rocketmq/studio/ops/ai/LlmControllerTest.java b/server/src/test/java/com/rocketmq/studio/ops/ai/LlmControllerTest.java new file mode 100644 index 00000000..e5b2d8fc --- /dev/null +++ b/server/src/test/java/com/rocketmq/studio/ops/ai/LlmControllerTest.java @@ -0,0 +1,119 @@ +/* + * 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 com.rocketmq.studio.ops.ai; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(LlmController.class) +@AutoConfigureMockMvc(addFilters = false) +class LlmControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private LlmConfigService llmConfigService; + + @Test + void getConfigShouldReturnFrontendContractShape() throws Exception { + when(llmConfigService.getConfig()).thenReturn(LlmConfigVO.builder() + .provider("openai") + .apiKey("sk-test") + .apiBase("https://api.openai.com/v1") + .model("gpt-4o") + .maxTokens(4096) + .temperature(0.7) + .enabled(true) + .build()); + + mockMvc.perform(get("/api/llm/config")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.provider").value("openai")) + .andExpect(jsonPath("$.apiBase").value("https://api.openai.com/v1")) + .andExpect(jsonPath("$.model").value("gpt-4o")) + .andExpect(jsonPath("$.enabled").value(true)); + } + + @Test + void saveConfigShouldReturnStatusZero() throws Exception { + LlmConfigVO config = LlmConfigVO.builder() + .provider("openai") + .apiKey("sk-test") + .model("gpt-4o") + .enabled(true) + .build(); + + mockMvc.perform(post("/api/llm/config") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(config))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(0)) + .andExpect(jsonPath("$.msg").value("saved")); + + verify(llmConfigService).saveConfig(any(LlmConfigVO.class)); + } + + @Test + void testConfigShouldReturnOperationResult() throws Exception { + when(llmConfigService.testConfig(any(LlmConfigVO.class))) + .thenReturn(LlmOperationResultVO.success("Configuration accepted")); + + mockMvc.perform(post("/api/llm/config/test") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(LlmConfigVO.builder() + .provider("ollama") + .model("llama3") + .build()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(0)) + .andExpect(jsonPath("$.msg").value("Configuration accepted")); + } + + @Test + void listModelsShouldReturnStatusAndData() throws Exception { + when(llmConfigService.listModels()).thenReturn(new LlmModelsResultVO(0, List.of( + new LlmModelItemVO("gpt-4o", "GPT-4o"), + new LlmModelItemVO("gpt-4", "GPT-4")))); + + mockMvc.perform(get("/api/llm/models")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(0)) + .andExpect(jsonPath("$.data[0].id").value("gpt-4o")) + .andExpect(jsonPath("$.data[1].name").value("GPT-4")); + } +}
