From 8834fcab2558ac11e34230ef4aae63427f442969 Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 09:47:09 +0200 Subject: [PATCH 1/8] 1018 - added connection setup steps for NanoGPT --- .../nano/gpt/connection/NanoGptConnection.java | 4 +++- .../nano-gpt/src/main/resources/connection.mdx | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 server/libs/modules/components/ai/llm/router/nano-gpt/src/main/resources/connection.mdx diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/connection/NanoGptConnection.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/connection/NanoGptConnection.java index 378aac8ab4f..c65c7fc2684 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/connection/NanoGptConnection.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/connection/NanoGptConnection.java @@ -27,7 +27,9 @@ public final class NanoGptConnection { public static final ModifiableConnectionDefinition CONNECTION_DEFINITION = - RouterConnection.connectionDefinition(BASE_URL); + RouterConnection.connectionDefinition(BASE_URL) + .version(1) + .help("", "https://docs.bytechef.io/reference/components/nano-gpt_v1#connection-setup"); private NanoGptConnection() { } diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/resources/connection.mdx b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/resources/connection.mdx new file mode 100644 index 00000000000..ea0a91a9379 --- /dev/null +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/resources/connection.mdx @@ -0,0 +1,15 @@ +## Connection Setup + +1. Navigate to [NanoGPT console](https://nano-gpt.com/conversation/new). +2. Click on **API**. +3. Click on **Create API Key**. +4. Enter name of your new key. +5. Click on **Create**. +6. Here is your new API key. +7. Done 🚀. + +
+ +
+ + From b2d66491e5628a192e3e5a050da3d948e5f61df9 Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 11:30:02 +0200 Subject: [PATCH 2/8] 1018 - added getters to the NanoGptModel, set variables to protected --- .../gpt/action/NanoGptCreateSpeechAction.java | 8 ++--- .../nano/gpt/model/NanoGptChatModel.java | 36 +++++++++++++++++++ .../nano/gpt/model/NanoGptImageModel.java | 36 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java index 15a8cadda80..35a814e0c09 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java @@ -54,11 +54,11 @@ */ public class NanoGptCreateSpeechAction { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final int MAX_POLL_ATTEMPTS = 40; - private static final int POLL_INTERVAL_MS = 3000; + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + protected static final int MAX_POLL_ATTEMPTS = 40; + protected static final int POLL_INTERVAL_MS = 3000; - private record AudioFetchResult(byte[] bytes, String extension, String audioUrl) { + protected record AudioFetchResult(byte[] bytes, String extension, String audioUrl) { } public static final ModifiableActionDefinition ACTION_DEFINITION = action(CREATE_SPEECH) diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptChatModel.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptChatModel.java index 3a4f4d49bc2..174948dad90 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptChatModel.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptChatModel.java @@ -96,6 +96,42 @@ protected void addProviderSpecificParams(Map body) { } } + public Double getMinP() { + return minP; + } + + public Integer getMinTokens() { + return minTokens; + } + + public Integer getMirostatMode() { + return mirostatMode; + } + + public Double getMirostatTau() { + return mirostatTau; + } + + public Double getMirostatEta() { + return mirostatEta; + } + + public Double getRepetitionPenalty() { + return repetitionPenalty; + } + + public Double getTfs() { + return tfs; + } + + public Double getTopA() { + return topA; + } + + public Double getTypicalP() { + return typicalP; + } + public static class Builder extends RouterChatModel.Builder { private Double minP; diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptImageModel.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptImageModel.java index eedda3ff7e4..d446cda6cde 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptImageModel.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/model/NanoGptImageModel.java @@ -49,6 +49,42 @@ public class NanoGptImageModel implements ImageModel { private final Double strength; private final Integer numInferenceSteps; + public String getModel() { + return model; + } + + public String getSize() { + return size; + } + + public String getResponseFormat() { + return responseFormat; + } + + public String getUser() { + return user; + } + + public Integer getN() { + return n; + } + + public Integer getSeed() { + return seed; + } + + public Double getGuidanceScale() { + return guidanceScale; + } + + public Double getStrength() { + return strength; + } + + public Integer getNumInferenceSteps() { + return numInferenceSteps; + } + private NanoGptImageModel(Builder builder) { this.restClient = ModelUtils.getRestClientBuilder() .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + builder.apiKey) From 1dde2a56929f6a3a1ff7e81f2178f4c9c5ac1566 Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 11:30:20 +0200 Subject: [PATCH 3/8] 1018 - added tests for NanoGpt --- .../gpt/action/NanoGptChatActionTest.java | 107 ++++++++ .../action/NanoGptCreateImageActionTest.java | 69 ++++++ .../action/NanoGptCreateSpeechActionTest.java | 230 ++++++++++++++++++ .../NanoGptCreateTranscriptionActionTest.java | 124 ++++++++++ 4 files changed, 530 insertions(+) create mode 100644 server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptChatActionTest.java create mode 100644 server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java create mode 100644 server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java create mode 100644 server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptChatActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptChatActionTest.java new file mode 100644 index 00000000000..09717ecbe2b --- /dev/null +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptChatActionTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.llm.router.nano.gpt.action; + +import static com.bytechef.component.ai.llm.constant.LLMConstants.FREQUENCY_PENALTY; +import static com.bytechef.component.ai.llm.constant.LLMConstants.LOGIT_BIAS; +import static com.bytechef.component.ai.llm.constant.LLMConstants.MAX_TOKENS; +import static com.bytechef.component.ai.llm.constant.LLMConstants.MODEL; +import static com.bytechef.component.ai.llm.constant.LLMConstants.PRESENCE_PENALTY; +import static com.bytechef.component.ai.llm.constant.LLMConstants.REASONING; +import static com.bytechef.component.ai.llm.constant.LLMConstants.SEED; +import static com.bytechef.component.ai.llm.constant.LLMConstants.STOP; +import static com.bytechef.component.ai.llm.constant.LLMConstants.TEMPERATURE; +import static com.bytechef.component.ai.llm.constant.LLMConstants.TOP_K; +import static com.bytechef.component.ai.llm.constant.LLMConstants.TOP_P; +import static com.bytechef.component.ai.llm.constant.LLMConstants.USER; +import static com.bytechef.component.ai.llm.constant.LLMConstants.VERBOSITY; +import static com.bytechef.component.ai.llm.router.constant.RouterConstants.LOGPROBS; +import static com.bytechef.component.ai.llm.router.constant.RouterConstants.MAX_COMPLETION_TOKENS; +import static com.bytechef.component.ai.llm.router.constant.RouterConstants.TOP_LOGPROBS; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.MIN_P; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.MIN_TOKENS; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.MIROSTAT_ETA; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.MIROSTAT_MODE; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.MIROSTAT_TAU; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.REPETITION_PENALTY; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.TFS; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.TOP_A; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.TYPICAL_P; +import static com.bytechef.component.definition.Authorization.TOKEN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.bytechef.component.ai.llm.router.nano.gpt.model.NanoGptChatModel; +import com.bytechef.component.definition.Parameters; +import com.bytechef.component.test.definition.MockParametersFactory; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * @author Nikolina Spehar + */ +class NanoGptChatActionTest { + + private final Parameters mockedParameters = MockParametersFactory.create( + Map.ofEntries( + Map.entry(TOKEN, "token"), Map.entry(MODEL, "model"), + Map.entry(FREQUENCY_PENALTY, 1.0), Map.entry(LOGIT_BIAS, Map.of()), + Map.entry(LOGPROBS, false), Map.entry(MAX_COMPLETION_TOKENS, 1), + Map.entry(MAX_TOKENS, 1), Map.entry(PRESENCE_PENALTY, 0.0), + Map.entry(REASONING, "reasoning"), Map.entry(SEED, 4), + Map.entry(STOP, List.of()), Map.entry(TEMPERATURE, 0.0), + Map.entry(TOP_K, 0.0), Map.entry(TOP_LOGPROBS, 1), Map.entry(TOP_P, 0.0), + Map.entry(VERBOSITY, "verbosity"), Map.entry(USER, "user"), + Map.entry(MIN_P, 0.0), Map.entry(MIN_TOKENS, 1), Map.entry(MIROSTAT_MODE, 1), + Map.entry(MIROSTAT_TAU, 0.0), Map.entry(MIROSTAT_ETA, 0.0), Map.entry(REPETITION_PENALTY, 0.0), + Map.entry(TFS, 0.0), Map.entry(TOP_A, 0.0), Map.entry(TYPICAL_P, 0.0))); + + @Test + void testPerform() { + NanoGptChatModel nanoGptChatModel = (NanoGptChatModel) NanoGptChatAction.CHAT_MODEL.createChatModel( + mockedParameters, mockedParameters, false); + + assertNotNull(nanoGptChatModel); + + assertEquals("model", nanoGptChatModel.getModel()); + assertEquals(1.0, nanoGptChatModel.getFrequencyPenalty()); + assertEquals(Map.of(), nanoGptChatModel.getLogitBias()); + assertEquals(false, nanoGptChatModel.getLogprobs()); + assertEquals(1, nanoGptChatModel.getMaxCompletionTokens()); + assertEquals(1, nanoGptChatModel.getMaxTokens()); + assertEquals(0.0, nanoGptChatModel.getPresencePenalty()); + assertEquals("reasoning", nanoGptChatModel.getReasoning()); + assertEquals(4, nanoGptChatModel.getSeed()); + assertEquals(List.of(), nanoGptChatModel.getStop()); + assertEquals(0.0, nanoGptChatModel.getTemperature()); + assertEquals(0.0, nanoGptChatModel.getTopK()); + assertEquals(1, nanoGptChatModel.getTopLogprobs()); + assertEquals(0.0, nanoGptChatModel.getTopP()); + assertEquals("verbosity", nanoGptChatModel.getVerbosity()); + assertEquals("user", nanoGptChatModel.getUser()); + assertEquals(0.0, nanoGptChatModel.getMinP()); + assertEquals(1, nanoGptChatModel.getMinTokens()); + assertEquals(1, nanoGptChatModel.getMirostatMode()); + assertEquals(0.0, nanoGptChatModel.getMirostatTau()); + assertEquals(0.0, nanoGptChatModel.getMirostatEta()); + assertEquals(0.0, nanoGptChatModel.getRepetitionPenalty()); + assertEquals(0.0, nanoGptChatModel.getTfs()); + assertEquals(0.0, nanoGptChatModel.getTopA()); + assertEquals(0.0, nanoGptChatModel.getTypicalP()); + } +} diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java new file mode 100644 index 00000000000..989a329bd7d --- /dev/null +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.llm.router.nano.gpt.action; + +import static com.bytechef.component.ai.llm.constant.LLMConstants.MODEL; +import static com.bytechef.component.ai.llm.constant.LLMConstants.N; +import static com.bytechef.component.ai.llm.constant.LLMConstants.RESPONSE_FORMAT; +import static com.bytechef.component.ai.llm.constant.LLMConstants.SEED; +import static com.bytechef.component.ai.llm.constant.LLMConstants.SIZE; +import static com.bytechef.component.ai.llm.constant.LLMConstants.USER; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.GUIDANCE_SCALE; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.NUM_INFERENCE_STEPS; +import static com.bytechef.component.ai.llm.router.nano.gpt.constant.NanoGptConstants.STRENGTH; +import static com.bytechef.component.definition.Authorization.TOKEN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.bytechef.component.ai.llm.router.nano.gpt.model.NanoGptImageModel; +import com.bytechef.component.definition.Parameters; +import com.bytechef.component.test.definition.MockParametersFactory; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * @author Nikolina Spehar + */ +class NanoGptCreateImageActionTest { + + private final Parameters mockedParameters = MockParametersFactory.create( + Map.ofEntries(Map.entry(MODEL, "model"), Map.entry(SIZE, "1024x1024"), + Map.entry(RESPONSE_FORMAT, "url"), Map.entry(N, 1), Map.entry(SEED, 1), + Map.entry(GUIDANCE_SCALE, 0.0), Map.entry(USER, "user"), Map.entry(STRENGTH, 0.0), + Map.entry(NUM_INFERENCE_STEPS, 1))); + + private final Parameters mockedConnectionParameters = MockParametersFactory.create(Map.of(TOKEN, "token")); + + @Test + void testCreateImageModel() { + NanoGptImageModel nanoGptImageModel = + (NanoGptImageModel) NanoGptCreateImageAction.IMAGE_MODEL.createImageModel( + mockedParameters, mockedConnectionParameters); + + assertNotNull(nanoGptImageModel); + + assertEquals("model", nanoGptImageModel.getModel()); + assertEquals("1024x1024", nanoGptImageModel.getSize()); + assertEquals("url", nanoGptImageModel.getResponseFormat()); + assertEquals(1, nanoGptImageModel.getN()); + assertEquals(1, nanoGptImageModel.getSeed()); + assertEquals(0.0, nanoGptImageModel.getGuidanceScale()); + assertEquals(0.0, nanoGptImageModel.getStrength()); + assertEquals(1, nanoGptImageModel.getNumInferenceSteps()); + assertEquals("user", nanoGptImageModel.getUser()); + } +} diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java new file mode 100644 index 00000000000..0533dad2a9f --- /dev/null +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.llm.router.nano.gpt.action; + +import static com.bytechef.component.ai.llm.constant.LLMConstants.INPUT; +import static com.bytechef.component.ai.llm.constant.LLMConstants.MODEL; +import static com.bytechef.component.ai.llm.constant.LLMConstants.RESPONSE_FORMAT; +import static com.bytechef.component.ai.llm.constant.LLMConstants.SPEED; +import static com.bytechef.component.ai.llm.constant.LLMConstants.VOICE; +import static com.bytechef.component.definition.Authorization.TOKEN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.bytechef.component.ai.llm.util.ModelUtils; +import com.bytechef.component.definition.ActionContext; +import com.bytechef.component.definition.Context.ContextFunction; +import com.bytechef.component.definition.Context.File; +import com.bytechef.component.definition.Context.Http.Executor; +import com.bytechef.component.definition.FileEntry; +import com.bytechef.component.definition.Parameters; +import com.bytechef.component.test.definition.MockParametersFactory; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.RequestBodyUriSpec; + +/** + * @author Nikolina Spehar + */ +class NanoGptCreateSpeechActionTest { + + @SuppressWarnings("unchecked") + private final ArgumentCaptor> fileFunctionArgumentCaptor = + forClass(ContextFunction.class); + private final ArgumentCaptor inputStreamArgumentCaptor = forClass(InputStream.class); + private final ArgumentCaptor mediaTypeArgumentCaptor = forClass(MediaType.class); + private final byte[] mockedAudioBytes = new byte[] { + (byte) 0xFF, (byte) 0xFB, 0x10, 0x00 + }; + private final RestClient.Builder mockedBuilder = mock(RestClient.Builder.class); + private final ActionContext mockedContext = mock(ActionContext.class); + private final File mockedFile = mock(File.class); + private final FileEntry mockedFileEntry = mock(FileEntry.class); + private final Parameters mockedParameters = MockParametersFactory.create( + Map.of( + INPUT, "Hello world", MODEL, "tts-model", RESPONSE_FORMAT, "mp3", VOICE, "alloy", + SPEED, 1.0, TOKEN, "test-api-key")); + private final RequestBodySpec mockedRequestBodySpec = mock(RequestBodySpec.class); + private final RequestBodyUriSpec mockedRequestBodyUriSpec = mock(RequestBodyUriSpec.class); + private final RestClient mockedRestClient = mock(RestClient.class); + private final ArgumentCaptor objectArgumentCaptor = forClass(Object.class); + private final ArgumentCaptor stringArgumentCaptor = forClass(String.class); + + @Test + void testPerform() { + try (MockedStatic modelUtilsMockedStatic = Mockito.mockStatic(ModelUtils.class)) { + modelUtilsMockedStatic.when(ModelUtils::getRestClientBuilder) + .thenReturn(mockedBuilder); + + when(mockedBuilder.defaultHeader(stringArgumentCaptor.capture(), stringArgumentCaptor.capture())) + .thenReturn(mockedBuilder); + when(mockedBuilder.build()) + .thenReturn(mockedRestClient); + + when(mockedRestClient.post()) + .thenReturn(mockedRequestBodyUriSpec); + when(mockedRequestBodyUriSpec.uri(stringArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.contentType(mediaTypeArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.body(objectArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + + when(mockedRequestBodySpec.exchange(any())) + .thenAnswer(inv -> { + + org.springframework.http.client.ClientHttpResponse httpResponse = + mock(org.springframework.http.client.ClientHttpResponse.class); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("audio/mpeg")); + when(httpResponse.getHeaders()).thenReturn(headers); + + InputStream bodyStream = new java.io.ByteArrayInputStream(mockedAudioBytes); + when(httpResponse.getBody()).thenReturn(bodyStream); + + return mockedAudioBytes; + }); + + when(mockedContext.file(fileFunctionArgumentCaptor.capture())) + .thenAnswer(inv -> { + ContextFunction fn = fileFunctionArgumentCaptor.getValue(); + return fn.apply(mockedFile); + }); + + when(mockedFile.storeContent(stringArgumentCaptor.capture(), inputStreamArgumentCaptor.capture())) + .thenReturn(mockedFileEntry); + + Object rawResult = NanoGptCreateSpeechAction.perform(mockedParameters, mockedParameters, mockedContext); + + assertInstanceOf(Map.class, rawResult); + + @SuppressWarnings("unchecked") + Map result = (Map) rawResult; + + assertEquals(mockedFileEntry, result.get("file")); + assertNull(result.get("audioUrl")); + + String storedFilename = stringArgumentCaptor.getAllValues() + .stream() + .filter(s -> s.startsWith("speech.")) + .findFirst() + .orElse(null); + assertNotNull(storedFilename); + assertEquals("speech.mp3", storedFilename); + + java.util.List capturedStrings = stringArgumentCaptor.getAllValues(); + int apiKeyIndex = capturedStrings.indexOf("x-api-key"); + assertNotNull(apiKeyIndex >= 0 ? capturedStrings.get(apiKeyIndex + 1) : null, + "API key header value should be present"); + assertEquals("test-api-key", capturedStrings.get(apiKeyIndex + 1)); + + Object body = objectArgumentCaptor.getValue(); + assertInstanceOf(Map.class, body); + + @SuppressWarnings("unchecked") + Map requestBody = (Map) body; + + assertEquals("Hello world", requestBody.get("text")); + assertEquals("tts-model", requestBody.get("model")); + assertEquals("alloy", requestBody.get("voice")); + assertEquals("mp3", requestBody.get("response_format")); + assertEquals(1.0, requestBody.get("speed")); + + assertEquals(MediaType.APPLICATION_JSON, mediaTypeArgumentCaptor.getValue()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + void testPerformWithoutOptionalParams() throws IOException { + Parameters minimalParameters = MockParametersFactory.create( + Map.of( + INPUT, "Minimal input", + MODEL, "tts-model", + TOKEN, "test-api-key")); + + try (MockedStatic modelUtilsMockedStatic = Mockito.mockStatic(ModelUtils.class)) { + modelUtilsMockedStatic.when(ModelUtils::getRestClientBuilder) + .thenReturn(mockedBuilder); + + when(mockedBuilder.defaultHeader(any(), any())) + .thenReturn(mockedBuilder); + when(mockedBuilder.build()) + .thenReturn(mockedRestClient); + + when(mockedRestClient.post()) + .thenReturn(mockedRequestBodyUriSpec); + when(mockedRequestBodyUriSpec.uri(any(String.class))) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.contentType(any())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.body(objectArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + + when(mockedRequestBodySpec.exchange(any())) + .thenReturn(mockedAudioBytes); + + when(mockedContext.file(fileFunctionArgumentCaptor.capture())) + .thenAnswer(inv -> { + ContextFunction fn = fileFunctionArgumentCaptor.getValue(); + return fn.apply(mockedFile); + }); + + when(mockedFile.storeContent(any(String.class), any(InputStream.class))) + .thenReturn(mockedFileEntry); + + Object rawResult = NanoGptCreateSpeechAction.perform(minimalParameters, minimalParameters, mockedContext); + + assertInstanceOf(Map.class, rawResult); + + @SuppressWarnings("unchecked") + Map result = (Map) rawResult; + + assertEquals(mockedFileEntry, result.get("file")); + + Object body = objectArgumentCaptor.getValue(); + assertInstanceOf(Map.class, body); + + @SuppressWarnings("unchecked") + Map requestBody = (Map) body; + + assertEquals("Minimal input", requestBody.get("text")); + assertEquals("tts-model", requestBody.get("model")); + assertEquals("mp3", requestBody.get("response_format")); + assertNotNull(requestBody.containsKey("voice") + ? null + : "voice absent as expected"); + } + } +} diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java new file mode 100644 index 00000000000..8cc24e16000 --- /dev/null +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.component.ai.llm.router.nano.gpt.action; + +import static com.bytechef.component.ai.llm.constant.LLMConstants.FILE; +import static com.bytechef.component.ai.llm.constant.LLMConstants.LANGUAGE; +import static com.bytechef.component.ai.llm.constant.LLMConstants.MODEL; +import static com.bytechef.component.definition.Authorization.TOKEN; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.bytechef.component.ai.llm.util.ModelUtils; +import com.bytechef.component.definition.ActionContext; +import com.bytechef.component.definition.FileEntry; +import com.bytechef.component.definition.Parameters; +import com.bytechef.component.test.definition.MockParametersFactory; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.RequestBodyUriSpec; +import org.springframework.web.client.RestClient.ResponseSpec; + +/** + * @author Nikolina Spehar + */ +class NanoGptCreateTranscriptionActionTest { + + private final ArgumentCaptor mediaTypeArgumentCaptor = forClass(MediaType.class); + private final RestClient.Builder mockedBuilder = mock(RestClient.Builder.class); + private final byte[] mockedByteArray = new byte[] { + 1, 1 + }; + private final ActionContext mockedContext = mock(ActionContext.class); + private final FileEntry mockedFileEntry = mock(FileEntry.class); + private final Parameters mockedParameters = MockParametersFactory.create( + Map.of(FILE, mockedFileEntry, MODEL, "model", LANGUAGE, "hr", TOKEN, "token")); + private final RequestBodySpec mockedRequestBodySpec = mock(RequestBodySpec.class); + private final RequestBodyUriSpec mockedRequestBodyUriSpec = mock(RequestBodyUriSpec.class); + private final ResponseSpec mockedResponseSpec = mock(ResponseSpec.class); + private final RestClient mockedRestClient = mock(RestClient.class); + private final ArgumentCaptor objectArgumentCaptor = forClass(Object.class); + private final ArgumentCaptor stringArgumentCaptor = forClass(String.class); + + @Test + void testPerform() { + when(mockedContext.file(any())) + .thenReturn(mockedByteArray); + + when(mockedFileEntry.getName()) + .thenReturn("fileName"); + + try (MockedStatic modelUtilsMockedStatic = Mockito.mockStatic(ModelUtils.class)) { + modelUtilsMockedStatic.when(ModelUtils::getRestClientBuilder) + .thenReturn(mockedBuilder); + + when(mockedBuilder.defaultHeader(stringArgumentCaptor.capture(), stringArgumentCaptor.capture())) + .thenReturn(mockedBuilder); + when(mockedBuilder.build()) + .thenReturn(mockedRestClient); + + when(mockedRestClient.post()) + .thenReturn(mockedRequestBodyUriSpec); + when(mockedRequestBodyUriSpec.uri(stringArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.contentType(mediaTypeArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.body(objectArgumentCaptor.capture())) + .thenReturn(mockedRequestBodySpec); + when(mockedRequestBodySpec.retrieve()) + .thenReturn(mockedResponseSpec); + when(mockedResponseSpec.body(any(ParameterizedTypeReference.class))) + .thenReturn(Map.of("transcription", "transcription")); + + String result = NanoGptCreateTranscriptionAction.perform( + mockedParameters, mockedParameters, mockedContext); + + assertEquals("transcription", result); + + assertEquals( + List.of("x-api-key", "token", "https://nano-gpt.com/api/transcribe"), + stringArgumentCaptor.getAllValues()); + + assertEquals(MediaType.MULTIPART_FORM_DATA, mediaTypeArgumentCaptor.getValue()); + + Map body = (Map) objectArgumentCaptor.getValue(); + + ByteArrayResource expectedAudio = new ByteArrayResource(mockedByteArray) { + @Override + public String getFilename() { + return "fileName"; + } + }; + + assertEquals(List.of(expectedAudio), body.get("audio")); + assertEquals(List.of("model"), body.get("model")); + assertEquals(List.of("hr"), body.get("language")); + } + } +} From e4a46f40a5038bf8999d746f2fdde3c2082dc3cc Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 11:43:17 +0200 Subject: [PATCH 4/8] 1018 - removed access to modifiable object reference --- .../nano/gpt/action/NanoGptCreateSpeechAction.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java index 35a814e0c09..ee33244e1cd 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java @@ -59,6 +59,17 @@ public class NanoGptCreateSpeechAction { protected static final int POLL_INTERVAL_MS = 3000; protected record AudioFetchResult(byte[] bytes, String extension, String audioUrl) { + + protected AudioFetchResult(byte[] bytes, String extension, String audioUrl) { + this.bytes = bytes == null ? null : bytes.clone(); + this.extension = extension; + this.audioUrl = audioUrl; + } + + @Override + public byte[] bytes() { + return bytes == null ? null : bytes.clone(); + } } public static final ModifiableActionDefinition ACTION_DEFINITION = action(CREATE_SPEECH) From 692d109301486660070b3547b816ea518a6d5d1d Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 11:45:38 +0200 Subject: [PATCH 5/8] 1018 - json - generated --- .../nano-gpt/src/test/resources/definition/nano-gpt_v1.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/resources/definition/nano-gpt_v1.json b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/resources/definition/nano-gpt_v1.json index 78c58f03ebd..26564dfde52 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/resources/definition/nano-gpt_v1.json +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/resources/definition/nano-gpt_v1.json @@ -3094,7 +3094,10 @@ "type": "BEARER_TOKEN" } ], "baseUri": { }, - "help": null, + "help": { + "body": "", + "learnMoreUrl": "https://docs.bytechef.io/reference/components/nano-gpt_v1#connection-setup" + }, "processErrorResponse": null, "properties": null, "test": null, From 213b16f7a7060fe67b9750fb21033bd585b76fb8 Mon Sep 17 00:00:00 2001 From: Nikolina Spehar Date: Wed, 3 Jun 2026 11:45:44 +0200 Subject: [PATCH 6/8] 1018 - docs - generated --- .../docs/reference/components/nano-gpt_v1.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/content/docs/reference/components/nano-gpt_v1.mdx b/docs/content/docs/reference/components/nano-gpt_v1.mdx index 1308e1d2043..1a9b7575f97 100644 --- a/docs/content/docs/reference/components/nano-gpt_v1.mdx +++ b/docs/content/docs/reference/components/nano-gpt_v1.mdx @@ -29,6 +29,21 @@ Version: 1 +## Connection Setup + +1. Navigate to [NanoGPT console](https://nano-gpt.com/conversation/new). +2. Click on **API**. +3. Click on **Create API Key**. +4. Enter name of your new key. +5. Click on **Create**. +6. Here is your new API key. +7. Done 🚀. + +
+ +
+ + From f954a94957cadaec3cd84d11ec7450c2071931ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Monika=20Ku=C5=A1ter?= Date: Wed, 3 Jun 2026 14:02:49 +0200 Subject: [PATCH 7/8] 1018 refactor --- .../gpt/action/NanoGptCreateSpeechAction.java | 6 +- .../NanoGptCreateTranscriptionAction.java | 7 +- .../action/NanoGptCreateImageActionTest.java | 5 +- .../action/NanoGptCreateSpeechActionTest.java | 76 ++++++------------- .../NanoGptCreateTranscriptionActionTest.java | 35 ++++++--- .../OpenRouterCreateSpeechActionTest.java | 15 +--- ...enRouterCreateTranscriptionActionTest.java | 13 +--- 7 files changed, 63 insertions(+), 94 deletions(-) diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java index ee33244e1cd..7db9f05a787 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechAction.java @@ -122,12 +122,12 @@ public static Object perform(Parameters inputParameters, Parameters connectionPa Map requestBody = new HashMap<>(); requestBody.put("text", inputParameters.getRequiredString(INPUT)); - requestBody.put("model", inputParameters.getRequiredString(MODEL)); + requestBody.put(MODEL, inputParameters.getRequiredString(MODEL)); String voice = inputParameters.getString(VOICE); if (voice != null && !voice.isBlank()) { - requestBody.put("voice", voice); + requestBody.put(VOICE, voice); } String responseFormat = inputParameters.getString(RESPONSE_FORMAT, "mp3"); @@ -137,7 +137,7 @@ public static Object perform(Parameters inputParameters, Parameters connectionPa Double speed = inputParameters.getDouble(SPEED); if (speed != null) { - requestBody.put("speed", speed); + requestBody.put(SPEED, speed); } RestClient restClient = ModelUtils.getRestClientBuilder() diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionAction.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionAction.java index db91a011985..efe69ac8d30 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionAction.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/main/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionAction.java @@ -55,8 +55,7 @@ public class NanoGptCreateTranscriptionAction { TRANSCRIPTION_MODEL_PROPERTY, fileEntry(FILE) .label("File") - .description( - "The audio file to transcribe. Supported formats: MP3, WAV, M4A, OGG, AAC (max 3MB).") + .description("The audio file to transcribe. Supported formats: MP3, WAV, M4A, OGG, AAC (max 3MB).") .required(true), TRANSCRIPTION_LANGUAGE_PROPERTY) .output(outputSchema(string())) @@ -80,12 +79,12 @@ public String getFilename() { } }); - formData.add("model", inputParameters.getRequiredString(MODEL)); + formData.add(MODEL, inputParameters.getRequiredString(MODEL)); String language = inputParameters.getString(LANGUAGE); if (language != null) { - formData.add("language", language); + formData.add(LANGUAGE, language); } RestClient restClient = ModelUtils.getRestClientBuilder() diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java index 989a329bd7d..a2e1fb8e576 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateImageActionTest.java @@ -50,9 +50,8 @@ class NanoGptCreateImageActionTest { @Test void testCreateImageModel() { - NanoGptImageModel nanoGptImageModel = - (NanoGptImageModel) NanoGptCreateImageAction.IMAGE_MODEL.createImageModel( - mockedParameters, mockedConnectionParameters); + NanoGptImageModel nanoGptImageModel = (NanoGptImageModel) NanoGptCreateImageAction.IMAGE_MODEL.createImageModel( + mockedParameters, mockedConnectionParameters); assertNotNull(nanoGptImageModel); diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java index 0533dad2a9f..86707392ad7 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateSpeechActionTest.java @@ -41,6 +41,7 @@ import com.bytechef.component.test.definition.MockParametersFactory; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -80,7 +81,7 @@ class NanoGptCreateSpeechActionTest { private final ArgumentCaptor stringArgumentCaptor = forClass(String.class); @Test - void testPerform() { + void testPerform() throws IOException { try (MockedStatic modelUtilsMockedStatic = Mockito.mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(ModelUtils::getRestClientBuilder) .thenReturn(mockedBuilder); @@ -118,6 +119,7 @@ void testPerform() { when(mockedContext.file(fileFunctionArgumentCaptor.capture())) .thenAnswer(inv -> { ContextFunction fn = fileFunctionArgumentCaptor.getValue(); + return fn.apply(mockedFile); }); @@ -133,75 +135,48 @@ void testPerform() { assertEquals(mockedFileEntry, result.get("file")); assertNull(result.get("audioUrl")); - - String storedFilename = stringArgumentCaptor.getAllValues() - .stream() - .filter(s -> s.startsWith("speech.")) - .findFirst() - .orElse(null); - assertNotNull(storedFilename); - assertEquals("speech.mp3", storedFilename); - - java.util.List capturedStrings = stringArgumentCaptor.getAllValues(); - int apiKeyIndex = capturedStrings.indexOf("x-api-key"); - assertNotNull(apiKeyIndex >= 0 ? capturedStrings.get(apiKeyIndex + 1) : null, - "API key header value should be present"); - assertEquals("test-api-key", capturedStrings.get(apiKeyIndex + 1)); - - Object body = objectArgumentCaptor.getValue(); - assertInstanceOf(Map.class, body); - - @SuppressWarnings("unchecked") - Map requestBody = (Map) body; - - assertEquals("Hello world", requestBody.get("text")); - assertEquals("tts-model", requestBody.get("model")); - assertEquals("alloy", requestBody.get("voice")); - assertEquals("mp3", requestBody.get("response_format")); - assertEquals(1.0, requestBody.get("speed")); - + assertEquals( + List.of("x-api-key", "test-api-key", "https://nano-gpt.com/api/v1/tts", "speech.mp3"), + stringArgumentCaptor.getAllValues()); + assertEquals( + Map.of("text", "Hello world", MODEL, "tts-model", VOICE, "alloy", SPEED, 1.0, "response_format", "mp3"), + objectArgumentCaptor.getValue()); assertEquals(MediaType.APPLICATION_JSON, mediaTypeArgumentCaptor.getValue()); - } catch (IOException e) { - throw new RuntimeException(e); } } @Test void testPerformWithoutOptionalParams() throws IOException { Parameters minimalParameters = MockParametersFactory.create( - Map.of( - INPUT, "Minimal input", - MODEL, "tts-model", - TOKEN, "test-api-key")); + Map.of(INPUT, "Minimal input", MODEL, "tts-model", TOKEN, "test-api-key")); try (MockedStatic modelUtilsMockedStatic = Mockito.mockStatic(ModelUtils.class)) { modelUtilsMockedStatic.when(ModelUtils::getRestClientBuilder) .thenReturn(mockedBuilder); - when(mockedBuilder.defaultHeader(any(), any())) + when(mockedBuilder.defaultHeader(stringArgumentCaptor.capture(), stringArgumentCaptor.capture())) .thenReturn(mockedBuilder); when(mockedBuilder.build()) .thenReturn(mockedRestClient); when(mockedRestClient.post()) .thenReturn(mockedRequestBodyUriSpec); - when(mockedRequestBodyUriSpec.uri(any(String.class))) + when(mockedRequestBodyUriSpec.uri(stringArgumentCaptor.capture())) .thenReturn(mockedRequestBodySpec); - when(mockedRequestBodySpec.contentType(any())) + when(mockedRequestBodySpec.contentType(mediaTypeArgumentCaptor.capture())) .thenReturn(mockedRequestBodySpec); when(mockedRequestBodySpec.body(objectArgumentCaptor.capture())) .thenReturn(mockedRequestBodySpec); when(mockedRequestBodySpec.exchange(any())) .thenReturn(mockedAudioBytes); - when(mockedContext.file(fileFunctionArgumentCaptor.capture())) .thenAnswer(inv -> { ContextFunction fn = fileFunctionArgumentCaptor.getValue(); + return fn.apply(mockedFile); }); - - when(mockedFile.storeContent(any(String.class), any(InputStream.class))) + when(mockedFile.storeContent(stringArgumentCaptor.capture(), inputStreamArgumentCaptor.capture())) .thenReturn(mockedFileEntry); Object rawResult = NanoGptCreateSpeechAction.perform(minimalParameters, minimalParameters, mockedContext); @@ -212,19 +187,14 @@ void testPerformWithoutOptionalParams() throws IOException { Map result = (Map) rawResult; assertEquals(mockedFileEntry, result.get("file")); - - Object body = objectArgumentCaptor.getValue(); - assertInstanceOf(Map.class, body); - - @SuppressWarnings("unchecked") - Map requestBody = (Map) body; - - assertEquals("Minimal input", requestBody.get("text")); - assertEquals("tts-model", requestBody.get("model")); - assertEquals("mp3", requestBody.get("response_format")); - assertNotNull(requestBody.containsKey("voice") - ? null - : "voice absent as expected"); + assertEquals( + List.of("x-api-key", "test-api-key", "https://nano-gpt.com/api/v1/tts", "speech.mp3"), + stringArgumentCaptor.getAllValues()); + assertEquals( + Map.of("text", "Minimal input", MODEL, "tts-model", "response_format", "mp3"), + objectArgumentCaptor.getValue()); + assertEquals(MediaType.APPLICATION_JSON, mediaTypeArgumentCaptor.getValue()); + assertNotNull(fileFunctionArgumentCaptor.getValue()); } } } diff --git a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java index 8cc24e16000..566d780af6e 100644 --- a/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java +++ b/server/libs/modules/components/ai/llm/router/nano-gpt/src/test/java/com/bytechef/component/ai/llm/router/nano/gpt/action/NanoGptCreateTranscriptionActionTest.java @@ -21,6 +21,7 @@ import static com.bytechef.component.ai.llm.constant.LLMConstants.MODEL; import static com.bytechef.component.definition.Authorization.TOKEN; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -28,9 +29,13 @@ import com.bytechef.component.ai.llm.util.ModelUtils; import com.bytechef.component.definition.ActionContext; +import com.bytechef.component.definition.Context; +import com.bytechef.component.definition.Context.ContextFunction; +import com.bytechef.component.definition.Context.File; import com.bytechef.component.definition.FileEntry; import com.bytechef.component.definition.Parameters; import com.bytechef.component.test.definition.MockParametersFactory; +import java.io.IOException; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -50,12 +55,16 @@ */ class NanoGptCreateTranscriptionActionTest { + @SuppressWarnings("unchecked") + private final ArgumentCaptor> fileFunctionArgumentCaptor = + forClass(ContextFunction.class); private final ArgumentCaptor mediaTypeArgumentCaptor = forClass(MediaType.class); private final RestClient.Builder mockedBuilder = mock(RestClient.Builder.class); private final byte[] mockedByteArray = new byte[] { 1, 1 }; private final ActionContext mockedContext = mock(ActionContext.class); + private final File mockedFile = mock(File.class); private final FileEntry mockedFileEntry = mock(FileEntry.class); private final Parameters mockedParameters = MockParametersFactory.create( Map.of(FILE, mockedFileEntry, MODEL, "model", LANGUAGE, "hr", TOKEN, "token")); @@ -65,10 +74,18 @@ class NanoGptCreateTranscriptionActionTest { private final RestClient mockedRestClient = mock(RestClient.class); private final ArgumentCaptor objectArgumentCaptor = forClass(Object.class); private final ArgumentCaptor stringArgumentCaptor = forClass(String.class); + private final ArgumentCaptor fileEntryArgumentCaptor = forClass(FileEntry.class); @Test - void testPerform() { - when(mockedContext.file(any())) + void testPerform() throws IOException { + when(mockedContext.file(fileFunctionArgumentCaptor.capture())) + .thenAnswer(inv -> { + ContextFunction fn = fileFunctionArgumentCaptor.getValue(); + + return fn.apply(mockedFile); + }); + + when(mockedFile.readAllBytes(fileEntryArgumentCaptor.capture())) .thenReturn(mockedByteArray); when(mockedFileEntry.getName()) @@ -96,19 +113,17 @@ void testPerform() { when(mockedResponseSpec.body(any(ParameterizedTypeReference.class))) .thenReturn(Map.of("transcription", "transcription")); - String result = NanoGptCreateTranscriptionAction.perform( - mockedParameters, mockedParameters, mockedContext); + String result = NanoGptCreateTranscriptionAction.perform(mockedParameters, mockedParameters, mockedContext); assertEquals("transcription", result); - + assertNotNull(fileFunctionArgumentCaptor.getValue()); + assertEquals(mockedFileEntry, fileEntryArgumentCaptor.getValue()); assertEquals( List.of("x-api-key", "token", "https://nano-gpt.com/api/transcribe"), stringArgumentCaptor.getAllValues()); assertEquals(MediaType.MULTIPART_FORM_DATA, mediaTypeArgumentCaptor.getValue()); - Map body = (Map) objectArgumentCaptor.getValue(); - ByteArrayResource expectedAudio = new ByteArrayResource(mockedByteArray) { @Override public String getFilename() { @@ -116,9 +131,9 @@ public String getFilename() { } }; - assertEquals(List.of(expectedAudio), body.get("audio")); - assertEquals(List.of("model"), body.get("model")); - assertEquals(List.of("hr"), body.get("language")); + assertEquals( + Map.of("audio", List.of(expectedAudio), "model", List.of("model"), "language", List.of("hr")), + objectArgumentCaptor.getValue()); } } } diff --git a/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateSpeechActionTest.java b/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateSpeechActionTest.java index 3fe2f1625e6..bfe842cfa12 100644 --- a/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateSpeechActionTest.java +++ b/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateSpeechActionTest.java @@ -128,18 +128,9 @@ void testPerform() throws IOException { assertInstanceOf(ByteArrayInputStream.class, inputStreamArgumentCaptorValue); assertArrayEquals(mockedByteArray, inputStreamArgumentCaptorValue.readAllBytes()); - - Object value = objectArgumentCaptor.getValue(); - assertInstanceOf(Map.class, value); - - @SuppressWarnings("unchecked") - Map body = (Map) value; - - assertEquals("input", body.get("input")); - assertEquals("model", body.get("model")); - assertEquals("voice", body.get("voice")); - assertEquals("pcm", body.get("response_format")); - assertEquals(0.0, body.get("speed")); + assertEquals( + Map.of("input", "input", "model", "model", "voice", "voice", "response_format", "pcm", SPEED, 0.0), + objectArgumentCaptor.getValue()); } } } diff --git a/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateTranscriptionActionTest.java b/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateTranscriptionActionTest.java index 4a5715c760e..6eb29606aed 100644 --- a/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateTranscriptionActionTest.java +++ b/server/libs/modules/components/ai/llm/router/open-router/src/test/java/com/bytechef/component/ai/llm/router/open/router/action/OpenRouterCreateTranscriptionActionTest.java @@ -105,19 +105,14 @@ void testPerform() { mockedParameters, mockedParameters, mockedContext); assertEquals("transcription", result); - assertEquals( List.of("https://openrouter.ai/api/v1", "Authorization", "Bearer token", "/audio/transcriptions"), stringArgumentCaptor.getAllValues()); - assertEquals(MediaType.APPLICATION_JSON, mediaTypeArgumentCaptor.getValue()); - - Map body = (Map) objectArgumentCaptor.getValue(); - - assertEquals(Map.of("data", "base64data", "format", "wav"), body.get("input_audio")); - assertEquals("model", body.get("model")); - assertEquals("language", body.get("language")); - assertEquals(0.5, body.get("temperature")); + assertEquals( + Map.of("input_audio", Map.of("data", "base64data", "format", "wav"), "model", "model", "language", + "language", "temperature", 0.5), + objectArgumentCaptor.getValue()); } } } From 144f46b817b5411dbcf654550ac8e6a225c2ab88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Monika=20Ku=C5=A1ter?= Date: Wed, 3 Jun 2026 15:29:35 +0200 Subject: [PATCH 8/8] 1018 spotbugsMain --- .../component/ai/llm/router/model/RouterChatModel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/libs/modules/components/ai/llm/router/src/main/java/com/bytechef/component/ai/llm/router/model/RouterChatModel.java b/server/libs/modules/components/ai/llm/router/src/main/java/com/bytechef/component/ai/llm/router/model/RouterChatModel.java index 246caebff8a..1e5204f5fe5 100644 --- a/server/libs/modules/components/ai/llm/router/src/main/java/com/bytechef/component/ai/llm/router/model/RouterChatModel.java +++ b/server/libs/modules/components/ai/llm/router/src/main/java/com/bytechef/component/ai/llm/router/model/RouterChatModel.java @@ -321,7 +321,7 @@ public Double getTemperature() { } public List getStop() { - return stop; + return stop == null ? null : new ArrayList<>(stop); } public Integer getSeed() { @@ -353,7 +353,7 @@ public Boolean getLogprobs() { } public Map getLogitBias() { - return logitBias; + return logitBias == null ? null : new HashMap<>(logitBias); } public Double getFrequencyPenalty() {