diff --git a/application/pom.xml b/application/pom.xml
index 8e5e6b2564..c1d4e5e14d 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -390,11 +390,7 @@
org.thingsboard.langchain4j
- langchain4j-google-ai-gemini
-
-
- org.thingsboard.langchain4j
- langchain4j-vertex-ai-gemini
+ langchain4j-google-genai
org.thingsboard.langchain4j
@@ -410,13 +406,7 @@
org.thingsboard.langchain4j
- langchain4j-github-models
-
-
- com.azure
- azure-core-test
-
-
+ langchain4j-open-ai-official
org.thingsboard.langchain4j
diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
index 15be6f3734..a2bc29104b 100644
--- a/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
+++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
@@ -15,14 +15,8 @@
*/
package org.thingsboard.server.service.ai;
-import com.fasterxml.jackson.core.io.JsonStringEncoder;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
-import dev.langchain4j.data.message.ChatMessage;
-import dev.langchain4j.data.message.Content;
-import dev.langchain4j.data.message.TextContent;
-import dev.langchain4j.data.message.UserMessage;
-import dev.langchain4j.model.ModelProvider;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
@@ -31,9 +25,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
-import java.util.List;
-import java.util.stream.Collectors;
-
@Service
@RequiredArgsConstructor
class AiChatModelServiceImpl implements AiChatModelService {
@@ -49,39 +40,7 @@ class AiChatModelServiceImpl implements AiChatModelService {
} catch (Throwable t) {
return FluentFuture.from(Futures.immediateFailedFuture(t));
}
- if (langChainChatModel.provider() == ModelProvider.GITHUB_MODELS) {
- chatRequest = prepareGithubChatRequest(chatRequest);
- }
return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
}
- private ChatRequest prepareGithubChatRequest(ChatRequest chatRequest) {
- List messages = chatRequest.messages().stream()
- .map(this::prepareUserMessage)
- .collect(Collectors.toList());
-
- return ChatRequest.builder()
- .messages(messages)
- .responseFormat(chatRequest.responseFormat())
- .build();
- }
-
- private ChatMessage prepareUserMessage(ChatMessage message) {
- if (message instanceof UserMessage userMessage) {
- List newContents = userMessage.contents().stream()
- .map(this::prepareContent)
- .collect(Collectors.toList());
-
- return UserMessage.from(newContents);
- }
- return message;
- }
-
- private Content prepareContent(Content content) {
- if (content instanceof TextContent txt) {
- return new TextContent(new String(JsonStringEncoder.getInstance().quoteAsString(txt.text())));
- }
- return content;
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
index 8b569f052c..8d0e54dc74 100644
--- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
+++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
@@ -15,26 +15,18 @@
*/
package org.thingsboard.server.service.ai;
-import com.google.api.gax.core.FixedCredentialsProvider;
-import com.google.api.gax.retrying.RetrySettings;
+import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
-import com.google.cloud.vertexai.Transport;
-import com.google.cloud.vertexai.VertexAI;
-import com.google.cloud.vertexai.api.GenerationConfig;
-import com.google.cloud.vertexai.api.PredictionServiceClient;
-import com.google.cloud.vertexai.api.PredictionServiceSettings;
-import com.google.cloud.vertexai.generativeai.GenerativeModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequestParameters;
-import dev.langchain4j.model.github.GitHubModelsChatModel;
-import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
+import dev.langchain4j.model.google.genai.GoogleGenAiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
-import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
+import dev.langchain4j.model.openaiofficial.OpenAiOfficialChatModel;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.SsrfProtectionValidator;
@@ -50,7 +42,6 @@ import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
-import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
@@ -107,14 +98,12 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
- return GoogleAiGeminiChatModel.builder()
+ return GoogleGenAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
- .frequencyPenalty(chatModelConfig.frequencyPenalty())
- .presencePenalty(chatModelConfig.presencePenalty())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
@@ -123,84 +112,26 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
- GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
-
- // construct service account credentials using service account key JSON
- ServiceAccountCredentials serviceAccountCredentials;
+ GoogleCredentials credentials;
try {
- serviceAccountCredentials = ServiceAccountCredentials.fromStream(new ByteArrayInputStream(providerConfig.serviceAccountKey().getBytes()));
+ credentials = ServiceAccountCredentials
+ .fromStream(new ByteArrayInputStream(chatModelConfig.providerConfig().serviceAccountKey().getBytes(StandardCharsets.UTF_8)))
+ .createScoped("https://www.googleapis.com/auth/cloud-platform");
} catch (IOException e) {
throw new RuntimeException("Failed to parse service account key JSON", e);
}
-
- PredictionServiceSettings predictionServiceClientSettings;
- try {
- // create prediction service settings for REST transport with service account key credentials
- PredictionServiceSettings.Builder settingsBuilder = PredictionServiceSettings.newHttpJsonBuilder()
- .setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials));
-
- // get the retry settings that control request timeout for generateContent RPC
- RetrySettings.Builder retrySettings = settingsBuilder
- .generateContentSettings()
- .getRetrySettings()
- .toBuilder();
-
- // set request timeout from model config
- if (chatModelConfig.timeoutSeconds() != null) {
- retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
- }
-
- // set updated retry settings
- settingsBuilder.generateContentSettings().setRetrySettings(retrySettings.build());
-
- // build the client settings
- predictionServiceClientSettings = settingsBuilder.build();
- } catch (IOException e) {
- throw new RuntimeException("Failed to create prediction service client settings", e);
- }
-
- // construct Vertex AI instance
- var vertexAI = new VertexAI.Builder()
- .setProjectId(providerConfig.projectId())
- .setLocation(providerConfig.location())
- .setPredictionClientSupplier(() -> createPredictionServiceClient(predictionServiceClientSettings))
- .setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys
+ return GoogleGenAiChatModel.builder()
+ .projectId(chatModelConfig.providerConfig().projectId())
+ .location(chatModelConfig.providerConfig().location())
+ .googleCredentials(credentials)
+ .modelName(chatModelConfig.modelId())
+ .temperature(chatModelConfig.temperature())
+ .topP(chatModelConfig.topP())
+ .topK(chatModelConfig.topK())
+ .maxOutputTokens(chatModelConfig.maxOutputTokens())
+ .timeout(toDuration(chatModelConfig.timeoutSeconds()))
+ .maxRetries(chatModelConfig.maxRetries())
.build();
-
- // map model config to generation config
- var generationConfigBuilder = GenerationConfig.newBuilder();
- if (chatModelConfig.temperature() != null) {
- generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
- }
- if (chatModelConfig.topP() != null) {
- generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
- }
- if (chatModelConfig.topK() != null) {
- generationConfigBuilder.setTopK(chatModelConfig.topK());
- }
- if (chatModelConfig.frequencyPenalty() != null) {
- generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
- }
- if (chatModelConfig.presencePenalty() != null) {
- generationConfigBuilder.setPresencePenalty(chatModelConfig.presencePenalty().floatValue());
- }
- if (chatModelConfig.maxOutputTokens() != null) {
- generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
- }
- var generationConfig = generationConfigBuilder.build();
-
- // construct generative model instance
- var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig);
-
- return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries());
- }
-
- private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) {
- try {
- return PredictionServiceClient.create(settings);
- } catch (IOException e) {
- throw new RuntimeException("Failed to create prediction service client", e);
- }
}
@Override
@@ -262,14 +193,16 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
- return GitHubModelsChatModel.builder()
- .gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
+ return OpenAiOfficialChatModel.builder()
+ .isGitHubModels(true)
+ .strictJsonSchema(true)
+ .apiKey(chatModelConfig.providerConfig().personalAccessToken())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
- .maxTokens(chatModelConfig.maxOutputTokens())
+ .maxCompletionTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
diff --git a/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java b/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
index fb9807a2a8..c2f7c39ce5 100644
--- a/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImplTest.java
@@ -15,20 +15,29 @@
*/
package org.thingsboard.server.service.ai;
-import com.google.cloud.vertexai.api.GenerationConfig;
+import dev.langchain4j.model.ModelProvider;
import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequestParameters;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
-import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.SsrfProtectionValidator;
+import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
+import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
+import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig;
+import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
+import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@@ -53,18 +62,280 @@ class Langchain4jChatModelConfigurerImplTest {
private final Langchain4jChatModelConfigurerImpl configurer = new Langchain4jChatModelConfigurerImpl();
- @BeforeEach
- void enableSsrfProtection() {
- SsrfProtectionValidator.setEnabled(true);
- }
-
@AfterEach
- void disableSsrfProtection() {
+ void resetSsrfProtection() {
SsrfProtectionValidator.setEnabled(false);
}
+ // ============================== Configuration correctness (one per provider) ==============================
+ // For each provider we feed a fully populated config and assert that the returned ChatModel carries the same
+ // values, using only the public ChatModel surface (provider() and defaultRequestParameters()) — no reflection.
+
+ @Test
+ void shouldConfigureOpenAiModel_whenGivenOpenAiConfig() {
+ // GIVEN
+ var config = OpenAiChatModelConfig.builder()
+ .providerConfig(OpenAiProviderConfig.builder()
+ .baseUrl("https://api.openai.com/v1")
+ .apiKey("test-key")
+ .build())
+ .modelId("gpt-4o")
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.25)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.OPEN_AI);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("gpt-4o");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.frequencyPenalty()).isEqualTo(0.5);
+ assertThat(params.presencePenalty()).isEqualTo(0.25);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureAzureOpenAiModel_whenGivenAzureOpenAiConfig() {
+ // GIVEN
+ var config = AzureOpenAiChatModelConfig.builder()
+ .providerConfig(new AzureOpenAiProviderConfig(
+ "https://my-resource.openai.azure.com/", "2024-05-01-preview", "test-key"))
+ .modelId("gpt-4o")
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.25)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.AZURE_OPEN_AI);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("gpt-4o"); // deployment name maps to modelName
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.frequencyPenalty()).isEqualTo(0.5);
+ assertThat(params.presencePenalty()).isEqualTo(0.25);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureGoogleAiGeminiModel_whenGivenGoogleAiGeminiConfig() {
+ // GIVEN
+ var config = GoogleAiGeminiChatModelConfig.builder()
+ .providerConfig(new GoogleAiGeminiProviderConfig("test-key"))
+ .modelId("gemini-2.5-flash")
+ .temperature(0.7)
+ .topP(0.9)
+ .topK(40)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.GOOGLE_GENAI);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("gemini-2.5-flash");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.topK()).isEqualTo(40);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureGoogleVertexAiGeminiModel_whenGivenGoogleVertexAiGeminiConfig() {
+ // GIVEN
+ var config = GoogleVertexAiGeminiChatModelConfig.builder()
+ .providerConfig(new GoogleVertexAiGeminiProviderConfig(
+ "key.json", "test-project", "us-central1", TEST_SERVICE_ACCOUNT_KEY))
+ .modelId("gemini-2.5-flash")
+ .temperature(0.7)
+ .topP(0.9)
+ .topK(40)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.GOOGLE_GENAI);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("gemini-2.5-flash");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.topK()).isEqualTo(40);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureMistralAiModel_whenGivenMistralAiConfig() {
+ // GIVEN
+ var config = MistralAiChatModelConfig.builder()
+ .providerConfig(new MistralAiProviderConfig("test-key"))
+ .modelId("mistral-large-latest")
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.25)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.MISTRAL_AI);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("mistral-large-latest");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.frequencyPenalty()).isEqualTo(0.5);
+ assertThat(params.presencePenalty()).isEqualTo(0.25);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureAnthropicModel_whenGivenAnthropicConfig() {
+ // GIVEN
+ var config = AnthropicChatModelConfig.builder()
+ .providerConfig(new AnthropicProviderConfig("test-key"))
+ .modelId("claude-opus-4-8")
+ .temperature(0.7)
+ .topP(0.9)
+ .topK(40)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.ANTHROPIC);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("claude-opus-4-8");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.topK()).isEqualTo(40);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
@Test
- void configureChatModel_openAi_withPrivateIp_shouldThrow() {
+ void shouldConfigureAmazonBedrockModel_whenGivenAmazonBedrockConfig() {
+ // GIVEN
+ var config = AmazonBedrockChatModelConfig.builder()
+ .providerConfig(new AmazonBedrockProviderConfig(
+ "us-east-1", "test-access-key-id", "test-secret-access-key"))
+ .modelId("anthropic.claude-3-5-sonnet-20240620-v1:0")
+ .temperature(0.7)
+ .topP(0.9)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.AMAZON_BEDROCK);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("anthropic.claude-3-5-sonnet-20240620-v1:0");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.maxOutputTokens()).isEqualTo(500);
+ }
+
+ @Test
+ void shouldConfigureGitHubModelsModel_whenGivenGitHubModelsConfig() {
+ // GIVEN
+ var config = GitHubModelsChatModelConfig.builder()
+ .providerConfig(new GitHubModelsProviderConfig("ghp-test-token"))
+ .modelId("gpt-4o")
+ .temperature(0.7)
+ .topP(0.9)
+ .frequencyPenalty(0.5)
+ .presencePenalty(0.25)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.GITHUB_MODELS);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("gpt-4o");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.frequencyPenalty()).isEqualTo(0.5);
+ assertThat(params.presencePenalty()).isEqualTo(0.25);
+ assertThat(params.maxOutputTokens()).isEqualTo(500); // maxCompletionTokens maps to maxOutputTokens
+ }
+
+ @Test
+ void shouldConfigureOllamaModel_whenGivenOllamaConfig() {
+ // GIVEN
+ var config = OllamaChatModelConfig.builder()
+ .providerConfig(new OllamaProviderConfig(
+ "http://localhost:11434", new OllamaProviderConfig.OllamaAuth.None()))
+ .modelId("llama3")
+ .temperature(0.7)
+ .topP(0.9)
+ .topK(40)
+ .contextLength(4096)
+ .maxOutputTokens(500)
+ .timeoutSeconds(60)
+ .maxRetries(3)
+ .build();
+
+ // WHEN
+ ChatModel chatModel = configurer.configureChatModel(config);
+
+ // THEN
+ assertThat(chatModel.provider()).isEqualTo(ModelProvider.OLLAMA);
+ ChatRequestParameters params = chatModel.defaultRequestParameters();
+ assertThat(params.modelName()).isEqualTo("llama3");
+ assertThat(params.temperature()).isEqualTo(0.7);
+ assertThat(params.topP()).isEqualTo(0.9);
+ assertThat(params.topK()).isEqualTo(40);
+ assertThat(params.maxOutputTokens()).isEqualTo(500); // numPredict maps to maxOutputTokens
+ }
+
+ // ============================== Base URL SSRF validation ==============================
+ // Providers that accept a user-supplied base URL must reject hosts that resolve to private/loopback addresses
+ // when SSRF protection is enabled.
+
+ @Test
+ void shouldThrow_whenOpenAiBaseUrlIsPrivateIp() {
+ // GIVEN
+ SsrfProtectionValidator.setEnabled(true);
var config = OpenAiChatModelConfig.builder()
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl("http://172.17.0.1:8080/")
@@ -73,13 +344,16 @@ class Langchain4jChatModelConfigurerImplTest {
.modelId("gpt-4o")
.build();
+ // WHEN / THEN
assertThatThrownBy(() -> configurer.configureChatModel(config))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
- void configureChatModel_openAi_withLocalhostUrl_shouldThrow() {
+ void shouldThrow_whenOpenAiBaseUrlIsLocalhost() {
+ // GIVEN
+ SsrfProtectionValidator.setEnabled(true);
var config = OpenAiChatModelConfig.builder()
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl("http://localhost:22/")
@@ -88,57 +362,42 @@ class Langchain4jChatModelConfigurerImplTest {
.modelId("gpt-4o")
.build();
+ // WHEN / THEN
assertThatThrownBy(() -> configurer.configureChatModel(config))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
- void configureChatModel_azureOpenAi_withPrivateIp_shouldThrow() {
+ void shouldThrow_whenAzureOpenAiEndpointIsPrivateIp() {
+ // GIVEN
+ SsrfProtectionValidator.setEnabled(true);
var config = AzureOpenAiChatModelConfig.builder()
.providerConfig(new AzureOpenAiProviderConfig(
"http://10.0.0.1:8080/", null, "test-key"))
.modelId("gpt-4o")
.build();
+ // WHEN / THEN
assertThatThrownBy(() -> configurer.configureChatModel(config))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
@Test
- void configureChatModel_ollama_withPrivateIp_shouldThrow() {
+ void shouldThrow_whenOllamaBaseUrlIsPrivateIp() {
+ // GIVEN
+ SsrfProtectionValidator.setEnabled(true);
var config = OllamaChatModelConfig.builder()
.providerConfig(new OllamaProviderConfig(
"http://192.168.1.100:11434/", new OllamaProviderConfig.OllamaAuth.None()))
.modelId("llama3")
.build();
+ // WHEN / THEN
assertThatThrownBy(() -> configurer.configureChatModel(config))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("URI is invalid");
}
- @Test
- void configureChatModel_vertexAi_setsFrequencyAndPresencePenaltyFromCorrectConfigFields() {
- // GIVEN
- var providerConfig = new GoogleVertexAiGeminiProviderConfig(
- "test.json", "test-project", "us-central1", TEST_SERVICE_ACCOUNT_KEY
- );
- var chatModelConfig = GoogleVertexAiGeminiChatModelConfig.builder()
- .providerConfig(providerConfig)
- .modelId("gemini-2.0-flash")
- .frequencyPenalty(0.3)
- .presencePenalty(0.7)
- .build();
-
- // WHEN
- ChatModel chatModel = configurer.configureChatModel(chatModelConfig);
-
- // THEN
- var generationConfig = (GenerationConfig) ReflectionTestUtils.getField(chatModel, "generationConfig");
- assertThat(generationConfig.getFrequencyPenalty()).isEqualTo(0.3f);
- assertThat(generationConfig.getPresencePenalty()).isEqualTo(0.7f);
- }
-
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
index 5f7772ed8b..abd25769d6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
@@ -42,6 +42,8 @@ public sealed interface AiChatModelConfig> extend
C withMaxRetries(Integer maxRetries);
- boolean supportsJsonMode();
+ boolean supportsSchemalessJsonOutput();
+
+ boolean supportsJsonSchemaOutput();
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
index ee80230fee..6e23cecf53 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
@@ -49,8 +49,13 @@ public record AmazonBedrockChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
return false;
}
+ @Override
+ public boolean supportsJsonSchemaOutput() {
+ return true;
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
index 44b55c4c22..eccea88e7d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
@@ -50,8 +50,13 @@ public record AnthropicChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
return false;
}
+ @Override
+ public boolean supportsJsonSchemaOutput() {
+ return true;
+ }
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
index 1e0d6d0c18..7347a57d9a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
@@ -51,7 +51,12 @@ public record AzureOpenAiChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
index 606f30599b..821e569cb8 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
@@ -51,8 +51,13 @@ public record GitHubModelsChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
- return false;
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
+ return true;
}
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
index 422c92ba97..3f04da34c5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
@@ -52,7 +52,12 @@ public record GoogleAiGeminiChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
index 3e298e52e2..1211802f98 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
@@ -52,7 +52,12 @@ public record GoogleVertexAiGeminiChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
index 5713217fe9..ce85208945 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
@@ -51,7 +51,12 @@ public record MistralAiChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
index 3f1856f630..3e5c2a7ba0 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
@@ -51,7 +51,12 @@ public record OllamaChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
index f74292cd76..53c5eeb091 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
@@ -51,7 +51,12 @@ public record OpenAiChatModelConfig(
}
@Override
- public boolean supportsJsonMode() {
+ public boolean supportsSchemalessJsonOutput() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsJsonSchemaOutput() {
return true;
}
diff --git a/pom.xml b/pom.xml
index 396033c915..a0b36c68bf 100755
--- a/pom.xml
+++ b/pom.xml
@@ -142,8 +142,7 @@
4.0.2
1.7.5
3.8.0
- 1.8.0-TB
- 2.5.9
+ 1.15.1-TB1
2.38.0
1.24
1.11.0
@@ -1376,11 +1375,6 @@
postgresql
${postgresql.version}
-
- org.apache.opennlp
- opennlp-tools
- ${opennlp-tools.version}
-
commons-io
commons-io
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
index bff72beb9b..28cf510207 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
@@ -69,7 +69,6 @@ import java.util.Set;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbResponseFormatType;
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
@Slf4j
@@ -93,7 +92,7 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie
configDirective = "tbExternalNodeAiConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==",
ruleChainTypes = RuleChainType.CORE,
- docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/"
+ docUrl = "https://thingsboard.io/docs/reference/rule-engine/nodes/external/ai-request/"
)
public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
@@ -127,13 +126,11 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType, true);
}
AiChatModelConfig> chatModelConfig = (AiChatModelConfig>) model.getConfiguration();
- if (isJsonModeConfigured(config)) {
- if (!chatModelConfig.supportsJsonMode()) {
- throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] does not support '" + config.getResponseFormat().type() + "' response format", true);
- }
- // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT
- responseFormat = config.getResponseFormat().toLangChainResponseFormat();
+ TbResponseFormat tbResponseFormat = config.getResponseFormat();
+ if (!tbResponseFormat.isSupportedBy(chatModelConfig)) {
+ throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] does not support '" + tbResponseFormat.type() + "' response format", true);
}
+ responseFormat = tbResponseFormat.toLangChainResponseFormat();
if (config.getResourceIds() != null && !config.getResourceIds().isEmpty()) {
resourceIds = new HashSet<>(config.getResourceIds().size());
for (UUID resourceId : config.getResourceIds()) {
@@ -149,11 +146,6 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
super.forceAck = config.isForceAck() || super.forceAck; // force ack if node config says so, or if env variable (super.forceAck) says so
}
- private static boolean isJsonModeConfigured(TbAiNodeConfiguration config) {
- var responseFormatType = config.getResponseFormat().type();
- return responseFormatType == TbResponseFormatType.JSON || responseFormatType == TbResponseFormatType.JSON_SCHEMA;
- }
-
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var ackedMsg = ackIfNeeded(ctx, msg);
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java
index a32469be42..48aad594c6 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java
@@ -20,7 +20,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.chat.request.ResponseFormatType;
+import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
+import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.validation.ValidJsonSchema;
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
@@ -41,6 +43,9 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
TbResponseFormatType type();
+ boolean isSupportedBy(AiChatModelConfig> modelConfig);
+
+ @Nullable
ResponseFormat toLangChainResponseFormat();
enum TbResponseFormatType {
@@ -58,6 +63,11 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
return TbResponseFormatType.TEXT;
}
+ @Override
+ public boolean isSupportedBy(AiChatModelConfig> modelConfig) {
+ return true;
+ }
+
@Override
public ResponseFormat toLangChainResponseFormat() {
return ResponseFormat.TEXT;
@@ -72,6 +82,11 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
return TbResponseFormatType.JSON;
}
+ @Override
+ public boolean isSupportedBy(AiChatModelConfig> modelConfig) {
+ return modelConfig.supportsSchemalessJsonOutput();
+ }
+
@Override
public ResponseFormat toLangChainResponseFormat() {
return ResponseFormat.JSON;
@@ -86,6 +101,11 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
return TbResponseFormatType.JSON_SCHEMA;
}
+ @Override
+ public boolean isSupportedBy(AiChatModelConfig> modelConfig) {
+ return modelConfig.supportsJsonSchemaOutput();
+ }
+
@Override
public ResponseFormat toLangChainResponseFormat() {
return ResponseFormat.builder()
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java
index a8f2f9b021..a7113c784a 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java
@@ -38,8 +38,8 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.DirectListeningExecutor;
+import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonSchemaResponseFormat;
import org.thingsboard.rule.engine.ai.TbResponseFormat.TbTextResponseFormat;
@@ -53,8 +53,10 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.id.AiModelId;
@@ -400,6 +402,124 @@ class TbAiNodeTest {
.matches(e -> ((TbNodeException) e).isUnrecoverable());
}
+ @Test
+ void givenJsonSchemaResponseFormatAndModelSupportsIt_whenInit_thenDoesNotThrow() {
+ // GIVEN
+ var jsonSchema = """
+ {
+ "title": "Joke",
+ "type": "object",
+ "properties": {
+ "joke": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "joke"
+ ]
+ }
+ """;
+
+ config = constructValidConfig();
+ config.setResponseFormat(new TbJsonSchemaResponseFormat((ObjectNode) JacksonUtil.toJsonNode(jsonSchema)));
+
+ // Anthropic does not support schemaless JSON mode, but does support JSON Schema constrained output
+ modelConfig = AnthropicChatModelConfig.builder()
+ .providerConfig(new AnthropicProviderConfig("test-api-key"))
+ .modelId("claude-sonnet-4-5")
+ .build();
+
+ model = AiModel.builder()
+ .tenantId(tenantId)
+ .name("Test model")
+ .configuration(modelConfig)
+ .build();
+
+ model.setId(modelId);
+ model.setVersion(1L);
+ model.setCreatedTime(123L);
+
+ given(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).willReturn(Optional.of(model));
+
+ // WHEN-THEN
+ assertThatNoException()
+ .isThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))));
+ }
+
+ @Test
+ void givenJsonSchemaResponseFormatAndBedrockModel_whenInit_thenDoesNotThrow() {
+ // GIVEN
+ var jsonSchema = """
+ {
+ "title": "Joke",
+ "type": "object",
+ "properties": {
+ "joke": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "joke"
+ ]
+ }
+ """;
+
+ config = constructValidConfig();
+ config.setResponseFormat(new TbJsonSchemaResponseFormat((ObjectNode) JacksonUtil.toJsonNode(jsonSchema)));
+
+ // Bedrock does not support schemaless JSON mode, but does support JSON Schema constrained output (Converse API)
+ modelConfig = AmazonBedrockChatModelConfig.builder()
+ .providerConfig(new AmazonBedrockProviderConfig("us-east-1", "test-access-key", "test-secret-key"))
+ .modelId("anthropic.claude-sonnet-4-5")
+ .build();
+
+ model = AiModel.builder()
+ .tenantId(tenantId)
+ .name("Test model")
+ .configuration(modelConfig)
+ .build();
+
+ model.setId(modelId);
+ model.setVersion(1L);
+ model.setCreatedTime(123L);
+
+ given(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).willReturn(Optional.of(model));
+
+ // WHEN-THEN
+ assertThatNoException()
+ .isThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))));
+ }
+
+ @Test
+ void givenSchemalessJsonResponseFormatAndBedrockModel_whenInit_thenThrowsUnrecoverableTbNodeException() {
+ // GIVEN
+ config = constructValidConfig();
+ config.setResponseFormat(new TbJsonResponseFormat());
+
+ modelConfig = AmazonBedrockChatModelConfig.builder()
+ .providerConfig(new AmazonBedrockProviderConfig("us-east-1", "test-access-key", "test-secret-key"))
+ .modelId("anthropic.claude-sonnet-4-5")
+ .build();
+
+ model = AiModel.builder()
+ .tenantId(tenantId)
+ .name("Test model")
+ .configuration(modelConfig)
+ .build();
+
+ model.setId(modelId);
+ model.setVersion(1L);
+ model.setCreatedTime(123L);
+
+ given(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).willReturn(Optional.of(model));
+
+ // WHEN-THEN
+ assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
+ .isInstanceOf(TbNodeException.class)
+ .hasMessage("[" + tenantId + "] AI model with ID: [" + modelId + "] does not support 'JSON' response format")
+ .matches(e -> ((TbNodeException) e).isUnrecoverable());
+ }
+
@Test
void givenNotExistingResources_whenInit_thenThrowsException() {
// GIVEN
@@ -669,7 +789,7 @@ class TbAiNodeTest {
argThat(actualChatRequest -> {
assertThat(actualChatRequest.messages()).hasSize(2);
assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(systemPrompt));
- assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents())
+ assertThat(((UserMessage) actualChatRequest.messages().get(1)).contents())
.containsAll(List.of(new TextContent(userPrompt), new TextContent(textData),
new TextContent(xmlData), new ImageContent(Base64.getEncoder().encodeToString(PNG_IMAGE), "image/png")));
return true;
@@ -706,7 +826,7 @@ class TbAiNodeTest {
argThat(actualChatRequest -> {
assertThat(actualChatRequest.messages()).hasSize(2);
assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(config.getSystemPrompt()));
- assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents())
+ assertThat(((UserMessage) actualChatRequest.messages().get(1)).contents())
.containsAll(List.of(new TextContent(config.getUserPrompt())));
return true;
})
@@ -993,7 +1113,7 @@ class TbAiNodeTest {
then(aiChatModelServiceMock).should().sendChatRequestAsync(
any(),
argThat(actualChatRequest -> {
- assertThat(actualChatRequest.responseFormat()).isNull();
+ assertThat(actualChatRequest.responseFormat()).isEqualTo(ResponseFormat.builder().type(ResponseFormatType.TEXT).build());
return true;
})
);
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
index 3cf88a2463..4a12d06097 100644
--- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
+++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
@@ -91,9 +91,15 @@
{{ 'rule-node-config.ai.response-format' | translate }}
- {{ 'rule-node-config.ai.response-text' | translate }}
- {{ 'rule-node-config.ai.response-json' | translate }}
- {{ 'rule-node-config.ai.response-json-schema' | translate }}
+ @if (allowedResponseFormats.includes(responseFormat.TEXT)) {
+ {{ 'rule-node-config.ai.response-text' | translate }}
+ }
+ @if (allowedResponseFormats.includes(responseFormat.JSON)) {
+ {{ 'rule-node-config.ai.response-json' | translate }}
+ }
+ @if (allowedResponseFormats.includes(responseFormat.JSON_SCHEMA)) {
+ {{ 'rule-node-config.ai.response-json-schema' | translate }}
+ }
{
+ switch (provider) {
+ case AiProvider.ANTHROPIC:
+ case AiProvider.AMAZON_BEDROCK:
+ return [ResponseFormat.TEXT, ResponseFormat.JSON_SCHEMA];
+ default:
+ return [ResponseFormat.TEXT, ResponseFormat.JSON, ResponseFormat.JSON_SCHEMA];
+ }
+};
+
export interface AiModelWithUserMsg {
userMessage: {
contents: Array<{contentType: string; text: string}>;
diff --git a/ui-ngx/src/assets/locale/locale.constant-da_DK.json b/ui-ngx/src/assets/locale/locale.constant-da_DK.json
index 0992ccd6ff..0058dc7df6 100644
--- a/ui-ngx/src/assets/locale/locale.constant-da_DK.json
+++ b/ui-ngx/src/assets/locale/locale.constant-da_DK.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Sprog"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json
index d218d2b96b..03ec53a2ed 100644
--- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json
+++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Sprache"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json
index a12db1a589..f97ebf47f1 100644
--- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json
+++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Γλώσσα"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 039e7a33e7..710238dcdb 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -1107,8 +1107,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json
index cca6b2a6ff..0913972e47 100644
--- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json
+++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Idioma"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json
index 6406930aab..8934bb139c 100644
--- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json
+++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Langue"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json
index ac47330f30..3117d40df7 100644
--- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json
+++ b/ui-ngx/src/assets/locale/locale.constant-it_IT.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Lingua"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json
index b7b87fd579..526cabd1dd 100644
--- a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json
+++ b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "言語"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json
index c3ddb5af74..4069fe951f 100644
--- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json
+++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json
@@ -1106,8 +1106,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_NL.json b/ui-ngx/src/assets/locale/locale.constant-nl_NL.json
index f9adb7f86c..3281909806 100644
--- a/ui-ngx/src/assets/locale/locale.constant-nl_NL.json
+++ b/ui-ngx/src/assets/locale/locale.constant-nl_NL.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Taal"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-no_NO.json b/ui-ngx/src/assets/locale/locale.constant-no_NO.json
index c7d1a4d7f3..9a8ee78206 100644
--- a/ui-ngx/src/assets/locale/locale.constant-no_NO.json
+++ b/ui-ngx/src/assets/locale/locale.constant-no_NO.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Språk"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json
index f13ad03a3b..37e8bbae34 100644
--- a/ui-ngx/src/assets/locale/locale.constant-pt_BR.json
+++ b/ui-ngx/src/assets/locale/locale.constant-pt_BR.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Idioma"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json
index 66bc099e84..dfc2d1be39 100644
--- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json
+++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Dil"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
index 654f305ae0..d71e1039b2 100644
--- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
+++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "Мова"
}
-}
\ No newline at end of file
+}
diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json
index fb1c849057..7f11a953ad 100644
--- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json
+++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json
@@ -1097,8 +1097,8 @@
"ai-providers": {
"openai": "OpenAI",
"azure-openai": "Azure OpenAI",
- "google-ai-gemini": "Google AI Gemini",
- "google-vertex-ai-gemini": "Google Vertex AI Gemini",
+ "google-ai-gemini": "Google Gemini (Gemini API)",
+ "google-vertex-ai-gemini": "Google Gemini (Agent Platform - Vertex AI)",
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
@@ -9538,4 +9538,4 @@
"language": {
"language": "语言"
}
-}
\ No newline at end of file
+}