This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new e656c11712 fix(ai): skip chat client when provider credentials are 
missing
     new 130d446664 Merge remote-tracking branch 'apache/2.0.0' into 2.0.0
e656c11712 is described below

commit e656c1171200950494eb308811e6c7206cfd4bd9
Author: Logic <[email protected]>
AuthorDate: Sat Jun 6 15:13:05 2026 +0800

    fix(ai): skip chat client when provider credentials are missing
---
 .../org/apache/hertzbeat/ai/config/LlmConfig.java  | 36 ++++++---
 .../impl/ChatClientProviderServiceImpl.java        | 14 ++--
 .../apache/hertzbeat/ai/config/LlmConfigTest.java  | 79 ++++++++++++++++++
 .../impl/ChatClientProviderServiceImplTest.java    | 94 ++++++++++++++++++++++
 4 files changed, 209 insertions(+), 14 deletions(-)

diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java
index 248c2e0828..da39573081 100644
--- a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java
+++ b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/config/LlmConfig.java
@@ -20,6 +20,8 @@ package org.apache.hertzbeat.ai.config;
 
 import com.openai.client.OpenAIClient;
 import com.openai.client.okhttp.OpenAIOkHttpClient;
+import com.openai.credential.BearerTokenCredential;
+import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
 import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
@@ -32,7 +34,6 @@ import org.springframework.ai.openai.OpenAiChatOptions;
 import org.springframework.beans.factory.support.DefaultListableBeanFactory;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.event.EventListener;
 import org.springframework.util.StringUtils;
@@ -45,6 +46,8 @@ import org.springframework.util.StringUtils;
 @Slf4j
 public class LlmConfig {
 
+    static final String OPEN_AI_CHAT_CLIENT_BEAN_NAME = "openAiChatClient";
+
     private final GeneralConfigDao generalConfigDao;
 
     private ApplicationContext applicationContext;
@@ -54,10 +57,11 @@ public class LlmConfig {
         this.applicationContext = applicationContext;
     }
 
-    /**
-     * Create ChatClient bean with all dependencies created internally
-     */
-    @Bean
+    @PostConstruct
+    public void registerInitialChatClient() {
+        registerChatClient();
+    }
+
     public ChatClient openAiChatClient() {
         return createChatClient();
     }
@@ -66,6 +70,15 @@ public class LlmConfig {
      * Create ChatClient with all necessary components
      */
     private ChatClient createChatClient() {
+        try {
+            return createConfiguredChatClient();
+        } catch (RuntimeException e) {
+            log.warn("LLM Provider configuration cannot create ChatClient, 
ChatClient bean will not be created", e);
+            return null;
+        }
+    }
+
+    private ChatClient createConfiguredChatClient() {
 
         GeneralConfig providerConfig = generalConfigDao.findByType("provider");
         if (providerConfig == null || providerConfig.getContent() == null) {
@@ -105,13 +118,14 @@ public class LlmConfig {
 
         OpenAIClient openAiClient = OpenAIOkHttpClient.builder()
                 .baseUrl(modelProviderConfig.getBaseUrl())
-                .apiKey(modelProviderConfig.getApiKey())
+                
.credential(BearerTokenCredential.create(modelProviderConfig.getApiKey()))
                 .build();
 
         // Create Chat Options
         OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
                 .model(modelProviderConfig.getModel())
                 .temperature(0.3)
+                .apiKey(modelProviderConfig.getApiKey())
                 .build();
 
         // Create Chat Model
@@ -132,13 +146,17 @@ public class LlmConfig {
     public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
         log.info("Provider configuration change event received, refreshing 
ChatClient bean");
 
+        registerChatClient();
+    }
+
+    private void registerChatClient() {
         try {
             ConfigurableApplicationContext configurableContext = 
(ConfigurableApplicationContext) applicationContext;
             DefaultListableBeanFactory beanFactory = 
(DefaultListableBeanFactory) configurableContext.getBeanFactory();
 
             // Remove the existing ChatClient bean
-            if (beanFactory.containsSingleton("openAiChatClient")) {
-                beanFactory.destroySingleton("openAiChatClient");
+            if (beanFactory.containsSingleton(OPEN_AI_CHAT_CLIENT_BEAN_NAME)) {
+                beanFactory.destroySingleton(OPEN_AI_CHAT_CLIENT_BEAN_NAME);
                 log.info("Existing ChatClient bean destroyed");
             }
 
@@ -150,7 +168,7 @@ public class LlmConfig {
             }
 
             // Register the new ChatClient bean
-            beanFactory.registerSingleton("openAiChatClient", newChatClient);
+            beanFactory.registerSingleton(OPEN_AI_CHAT_CLIENT_BEAN_NAME, 
newChatClient);
 
             log.info("ChatClient bean refreshed successfully with new AI 
provider configuration");
 
diff --git 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
index edd8bd4ea8..b831cb1905 100644
--- 
a/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
+++ 
b/hertzbeat-ai/src/main/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImpl.java
@@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.core.io.Resource;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.ai.chat.messages.AssistantMessage;
@@ -70,8 +71,6 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
     @Qualifier("hertzbeatTools")
     private ToolCallbackProvider toolCallbackProvider;
     
-    private boolean isConfigured = false;
-
     @Value("classpath:/prompt/system-message.st")
     private Resource systemResource;
 
@@ -177,11 +176,16 @@ public class ChatClientProviderServiceImpl implements 
ChatClientProviderService
 
     @Override
     public boolean isConfigured() {
-        if (!isConfigured) {
+        try {
             GeneralConfig providerConfig = 
generalConfigDao.findByType("provider");
+            if (providerConfig == null || 
!StringUtils.hasText(providerConfig.getContent())) {
+                return false;
+            }
             ModelProviderConfig modelProviderConfig = 
JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
-            isConfigured = modelProviderConfig != null && 
modelProviderConfig.getApiKey() != null;   
+            return modelProviderConfig != null && 
StringUtils.hasText(modelProviderConfig.getApiKey());
+        } catch (RuntimeException e) {
+            log.warn("LLM Provider configuration cannot be read", e);
+            return false;
         }
-        return isConfigured;
     }
 }
diff --git 
a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/config/LlmConfigTest.java 
b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/config/LlmConfigTest.java
index 5c89d7a55b..3ba5d35c25 100644
--- 
a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/config/LlmConfigTest.java
+++ 
b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/config/LlmConfigTest.java
@@ -18,7 +18,10 @@
 package org.apache.hertzbeat.ai.config;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -27,10 +30,21 @@ import 
org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
 import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
 import org.apache.hertzbeat.common.util.JsonUtil;
 import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
 import org.springframework.context.support.GenericApplicationContext;
 
 class LlmConfigTest {
 
+    @Test
+    void openAiChatClientSkipsMissingProviderConfig() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        when(generalConfigDao.findByType("provider")).thenReturn(null);
+
+        LlmConfig llmConfig = new LlmConfig(generalConfigDao, new 
GenericApplicationContext());
+
+        assertNull(assertDoesNotThrow(llmConfig::openAiChatClient));
+    }
+
     @Test
     void openAiChatClientSkipsBlankApiKey() {
         GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
@@ -41,6 +55,71 @@ class LlmConfigTest {
         assertNull(assertDoesNotThrow(llmConfig::openAiChatClient));
     }
 
+    @Test
+    void openAiChatClientSkipsInvalidProviderContent() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(GeneralConfig.builder()
+                .type("provider")
+                .content("{")
+                .build());
+
+        LlmConfig llmConfig = new LlmConfig(generalConfigDao, new 
GenericApplicationContext());
+
+        assertNull(assertDoesNotThrow(llmConfig::openAiChatClient));
+    }
+
+    @Test
+    void openAiChatClientCreatesClientWithConfiguredApiKey() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(providerConfig("sk-test"));
+
+        LlmConfig llmConfig = new LlmConfig(generalConfigDao, new 
GenericApplicationContext());
+
+        assertNotNull(assertDoesNotThrow(llmConfig::openAiChatClient));
+    }
+
+    @Test
+    void applicationContextStartsWithoutProviderConfig() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        when(generalConfigDao.findByType("provider")).thenReturn(null);
+
+        new ApplicationContextRunner()
+                .withBean(GeneralConfigDao.class, () -> generalConfigDao)
+                .withUserConfiguration(LlmConfig.class)
+                .run(context -> {
+                    
assertFalse(context.containsBean(LlmConfig.OPEN_AI_CHAT_CLIENT_BEAN_NAME));
+                    assertNull(context.getStartupFailure());
+                });
+    }
+
+    @Test
+    void applicationContextStartsWithoutBlankApiKey() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(providerConfig(" "));
+
+        new ApplicationContextRunner()
+                .withBean(GeneralConfigDao.class, () -> generalConfigDao)
+                .withUserConfiguration(LlmConfig.class)
+                .run(context -> {
+                    
assertFalse(context.containsBean(LlmConfig.OPEN_AI_CHAT_CLIENT_BEAN_NAME));
+                    assertNull(context.getStartupFailure());
+                });
+    }
+
+    @Test
+    void applicationContextRegistersClientWithConfiguredApiKey() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(providerConfig("sk-test"));
+
+        new ApplicationContextRunner()
+                .withBean(GeneralConfigDao.class, () -> generalConfigDao)
+                .withUserConfiguration(LlmConfig.class)
+                .run(context -> {
+                    
assertTrue(context.containsBean(LlmConfig.OPEN_AI_CHAT_CLIENT_BEAN_NAME));
+                    assertNull(context.getStartupFailure());
+                });
+    }
+
     private static GeneralConfig providerConfig(String apiKey) {
         ModelProviderConfig providerConfig = new ModelProviderConfig();
         providerConfig.setCode("openai");
diff --git 
a/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java
 
b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java
new file mode 100644
index 0000000000..6e2b80807b
--- /dev/null
+++ 
b/hertzbeat-ai/src/test/java/org/apache/hertzbeat/ai/service/impl/ChatClientProviderServiceImplTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.hertzbeat.ai.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
+import org.apache.hertzbeat.base.dao.GeneralConfigDao;
+import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
+import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.ApplicationContext;
+
+class ChatClientProviderServiceImplTest {
+
+    @Test
+    void isConfiguredReturnsFalseWhenProviderConfigIsMissing() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        when(generalConfigDao.findByType("provider")).thenReturn(null);
+
+        ChatClientProviderServiceImpl service = newService(generalConfigDao);
+
+        assertFalse(service.isConfigured());
+    }
+
+    @Test
+    void isConfiguredReturnsFalseWhenApiKeyIsBlank() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(providerConfig(" "));
+
+        ChatClientProviderServiceImpl service = newService(generalConfigDao);
+
+        assertFalse(service.isConfigured());
+    }
+
+    @Test
+    void isConfiguredReturnsFalseWhenProviderContentIsInvalid() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(GeneralConfig.builder()
+                .type("provider")
+                .content("{")
+                .build());
+
+        ChatClientProviderServiceImpl service = newService(generalConfigDao);
+
+        assertFalse(service.isConfigured());
+    }
+
+    @Test
+    void isConfiguredReturnsTrueWhenApiKeyIsPresent() {
+        GeneralConfigDao generalConfigDao = mock(GeneralConfigDao.class);
+        
when(generalConfigDao.findByType("provider")).thenReturn(providerConfig("sk-test"));
+
+        ChatClientProviderServiceImpl service = newService(generalConfigDao);
+
+        assertTrue(service.isConfigured());
+    }
+
+    private static ChatClientProviderServiceImpl newService(GeneralConfigDao 
generalConfigDao) {
+        return new ChatClientProviderServiceImpl(
+                mock(ApplicationContext.class),
+                generalConfigDao,
+                mock(SkillRegistry.class));
+    }
+
+    private static GeneralConfig providerConfig(String apiKey) {
+        ModelProviderConfig providerConfig = new ModelProviderConfig();
+        providerConfig.setCode("openai");
+        providerConfig.setApiKey(apiKey);
+        return GeneralConfig.builder()
+                .type("provider")
+                .content(JsonUtil.toJson(providerConfig))
+                .build();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to