From ba7fbfffc15e19cbb76ff68170ee5effb3ca4337 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 3 Jun 2026 18:14:47 +0300 Subject: [PATCH] feat: support JSON Schema structured output across AI providers Upgrade LangChain4j to 1.15.1-TB1 and migrate GitHub Models to the official OpenAI integration (OpenAiOfficialChatModel), enabling JSON Schema (strict) structured output for GitHub Models and exposing JSON Schema as a response format for Anthropic and Amazon Bedrock. - Rename model capability flags to supportsSchemalessJsonOutput() and supportsJsonSchemaOutput(), declared explicitly per provider - Push response-format capability checks into TbResponseFormat and simplify response-format handling in TbAiNode - Remove obsolete manual JSON escaping for GitHub Models, which now double-escaped requests under the official OpenAI SDK - Drop the now-obsolete opennlp-tools version pin (the upgraded LangChain4j fork ships opennlp-tools >= 2.5.9) - Update the AI rule node UI to offer response formats per provider, refresh available model lists, and migrate toggle options to @if - Expand configurer tests to verify per-provider model configuration --- application/pom.xml | 14 +- .../service/ai/AiChatModelServiceImpl.java | 41 --- .../Langchain4jChatModelConfigurerImpl.java | 115 ++---- ...angchain4jChatModelConfigurerImplTest.java | 329 ++++++++++++++++-- .../data/ai/model/chat/AiChatModelConfig.java | 4 +- .../chat/AmazonBedrockChatModelConfig.java | 7 +- .../model/chat/AnthropicChatModelConfig.java | 7 +- .../chat/AzureOpenAiChatModelConfig.java | 7 +- .../chat/GitHubModelsChatModelConfig.java | 9 +- .../chat/GoogleAiGeminiChatModelConfig.java | 7 +- .../GoogleVertexAiGeminiChatModelConfig.java | 7 +- .../model/chat/MistralAiChatModelConfig.java | 7 +- .../ai/model/chat/OllamaChatModelConfig.java | 7 +- .../ai/model/chat/OpenAiChatModelConfig.java | 7 +- pom.xml | 8 +- .../thingsboard/rule/engine/ai/TbAiNode.java | 18 +- .../rule/engine/ai/TbResponseFormat.java | 20 ++ .../rule/engine/ai/TbAiNodeTest.java | 128 ++++++- .../external/ai-config.component.html | 12 +- .../rule-node/external/ai-config.component.ts | 19 +- .../src/app/shared/models/ai-model.models.ts | 32 +- .../assets/locale/locale.constant-da_DK.json | 6 +- .../assets/locale/locale.constant-de_DE.json | 6 +- .../assets/locale/locale.constant-el_GR.json | 6 +- .../assets/locale/locale.constant-en_US.json | 4 +- .../assets/locale/locale.constant-es_ES.json | 6 +- .../assets/locale/locale.constant-fr_FR.json | 6 +- .../assets/locale/locale.constant-it_IT.json | 6 +- .../assets/locale/locale.constant-ja_JP.json | 6 +- .../assets/locale/locale.constant-lt_LT.json | 4 +- .../assets/locale/locale.constant-nl_NL.json | 6 +- .../assets/locale/locale.constant-no_NO.json | 6 +- .../assets/locale/locale.constant-pt_BR.json | 6 +- .../assets/locale/locale.constant-tr_TR.json | 6 +- .../assets/locale/locale.constant-uk_UA.json | 6 +- .../assets/locale/locale.constant-zh_CN.json | 6 +- 36 files changed, 609 insertions(+), 282 deletions(-) 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 +}