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"));
+    }
+}

Reply via email to