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]