diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java index 67bdae15714..a04d83f18d9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java @@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; @@ -46,6 +47,7 @@ * @author Thomas Vitale * @author Ilayaperumal Gopinathan * @author Jonghoon Park + * @author Nicolas Krier * @since 0.8.0 */ @AutoConfiguration(after = { OllamaApiAutoConfiguration.class, ToolCallingAutoConfiguration.class }) @@ -84,4 +86,10 @@ public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties return chatModel; } + @Bean + @ConfigurationPropertiesBinding + public static ThinkOptionConverter thinkOptionConverter() { + return new ThinkOptionConverter(); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverter.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverter.java new file mode 100644 index 00000000000..395fc38e4a4 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * 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 org.springframework.ai.model.ollama.autoconfigure; + +import org.springframework.ai.ollama.api.ThinkOption; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; + +/** + * {@link Converter} handling string to {@link ThinkOption} conversion. Both + * {@link ThinkOption.ThinkBoolean} and {@link ThinkOption.ThinkLevel} are supported. + * + * @author Nicolas Krier + * @since 1.1.1 + */ +public class ThinkOptionConverter implements Converter { + + @Override + public ThinkOption convert(@NonNull String source) { + return switch (source) { + case "enabled" -> ThinkOption.ThinkBoolean.ENABLED; + case "disabled" -> ThinkOption.ThinkBoolean.DISABLED; + case "low" -> ThinkOption.ThinkLevel.LOW; + case "medium" -> ThinkOption.ThinkLevel.MEDIUM; + case "high" -> ThinkOption.ThinkLevel.HIGH; + default -> throw new IllegalStateException("Unexpected think option value: " + source); + }; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java index 72a9d4e4925..93451a250ce 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,30 +18,33 @@ import org.junit.jupiter.api.Test; +import org.springframework.ai.ollama.api.ThinkOption; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov + * @author Nicolas Krier * @since 0.8.0 */ -public class OllamaChatAutoConfigurationTests { +class OllamaChatAutoConfigurationTests { @Test - public void propertiesTest() { - + void propertiesTest() { new ApplicationContextRunner().withPropertyValues( // @formatter:off - "spring.ai.ollama.base-url=TEST_BASE_URL", - "spring.ai.ollama.chat.options.model=MODEL_XYZ", - "spring.ai.ollama.chat.options.temperature=0.55", - "spring.ai.ollama.chat.options.topP=0.56", - "spring.ai.ollama.chat.options.topK=123") - // @formatter:on + "spring.ai.ollama.base-url=TEST_BASE_URL", + "spring.ai.ollama.chat.options.model=MODEL_XYZ", + "spring.ai.ollama.chat.options.temperature=0.55", + "spring.ai.ollama.chat.options.topP=0.56", + "spring.ai.ollama.chat.options.topK=123") + // @formatter:on .withConfiguration(BaseOllamaIT.ollamaAutoConfig(OllamaChatAutoConfiguration.class)) .run(context -> { + assertThat(context).hasNotFailed(); + var chatProperties = context.getBean(OllamaChatProperties.class); var connectionProperties = context.getBean(OllamaConnectionProperties.class); @@ -49,11 +52,49 @@ public void propertiesTest() { assertThat(chatProperties.getModel()).isEqualTo("MODEL_XYZ"); + assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - assertThat(chatProperties.getOptions().getTopK()).isEqualTo(123); }); } + @Test + void thinkBooleanPropertiesTest() { + new ApplicationContextRunner().withPropertyValues( + // @formatter:off + "spring.ai.ollama.chat.options.model=qwen3:4b-thinking", + "spring.ai.ollama.chat.options.think-option=enabled" + // @formatter:on + ).withConfiguration(BaseOllamaIT.ollamaAutoConfig(OllamaChatAutoConfiguration.class)).run(context -> { + assertThat(context).hasNotFailed(); + + var chatProperties = context.getBean(OllamaChatProperties.class); + + assertThat(chatProperties.getModel()).isEqualTo("qwen3:4b-thinking"); + + assertThat(chatProperties.getOptions().getModel()).isEqualTo("qwen3:4b-thinking"); + assertThat(chatProperties.getOptions().getThinkOption()).isEqualTo(ThinkOption.ThinkBoolean.ENABLED); + }); + } + + @Test + void thinkLevelPropertiesTest() { + new ApplicationContextRunner().withPropertyValues( + // @formatter:off + "spring.ai.ollama.chat.options.model=gpt-oss:latest", + "spring.ai.ollama.chat.options.think-option=low" + // @formatter:on + ).withConfiguration(BaseOllamaIT.ollamaAutoConfig(OllamaChatAutoConfiguration.class)).run(context -> { + assertThat(context).hasNotFailed(); + + var chatProperties = context.getBean(OllamaChatProperties.class); + + assertThat(chatProperties.getModel()).isEqualTo("gpt-oss:latest"); + + assertThat(chatProperties.getOptions().getModel()).isEqualTo("gpt-oss:latest"); + assertThat(chatProperties.getOptions().getThinkOption()).isEqualTo(ThinkOption.ThinkLevel.LOW); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverterTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverterTests.java new file mode 100644 index 00000000000..505db494905 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/ThinkOptionConverterTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * 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 org.springframework.ai.model.ollama.autoconfigure; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.ai.ollama.api.ThinkOption; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Nicolas Krier + * @since 1.1.1 + */ +class ThinkOptionConverterTests { + + private final ThinkOptionConverter converter = new ThinkOptionConverter(); + + @Test + void verifyUnsupportedConversion() { + assertThatIllegalStateException().isThrownBy(() -> this.converter.convert("ABC")) + .withMessage("Unexpected think option value: ABC"); + } + + @ParameterizedTest + @MethodSource("thinkBooleanConversionParameters") + void verifyThinkBooleanConversion(String source, ThinkOption.ThinkBoolean expectedTarget) { + assertThat(this.converter.convert(source)).isEqualTo(expectedTarget); + } + + @ParameterizedTest + @MethodSource("thinkLevelConversionParameters") + void verifyThinkLevelConversion(String source, ThinkOption.ThinkLevel expectedTarget) { + assertThat(this.converter.convert(source)).isEqualTo(expectedTarget); + } + + private static Stream thinkBooleanConversionParameters() { + return Stream.of(Arguments.of("enabled", ThinkOption.ThinkBoolean.ENABLED), + Arguments.of("disabled", ThinkOption.ThinkBoolean.DISABLED)); + } + + private static Stream thinkLevelConversionParameters() { + return Stream.of(Arguments.of("low", ThinkOption.ThinkLevel.LOW), + Arguments.of("medium", ThinkOption.ThinkLevel.MEDIUM), + Arguments.of("high", ThinkOption.ThinkLevel.HIGH)); + } + +}