diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java index 29715e21f3..629a3cf14f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java @@ -61,6 +61,8 @@ public class OpenAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "spring.ai.openai", name = { "api-key", "chat.api-key" }, + matchIfMissing = false) public OpenAiApi openAiApi(OpenAiConnectionProperties commonProperties, OpenAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, ObjectProvider webClientBuilderProvider, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java index 7aa172c290..3b84988a0d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java @@ -386,4 +386,24 @@ void openAiApiBean() { }); } + @Test + void imageOnlyApiKeyDoesNotFailChatAutoConfiguration() { + // Regression test for https://github.com/spring-projects/spring-ai/issues/1818 + // When only spring.ai.openai.image.api-key is set (no chat or common api-key), + // OpenAiChatAutoConfiguration must not throw an IllegalArgumentException. + // The OpenAiApi bean for chat should simply not be created. + new ApplicationContextRunner() + .withPropertyValues("spring.ai.openai.image.api-key=IMAGE_API_KEY", + "spring.ai.openai.base-url=TEST_BASE_URL") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, RestClientAutoConfiguration.class, + SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class, + WebClientAutoConfiguration.class)) + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); + }); + } + } diff --git a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/McpPredicates.java b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/McpPredicates.java index 7904e92438..eb77a273e4 100644 --- a/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/McpPredicates.java +++ b/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/common/McpPredicates.java @@ -91,14 +91,31 @@ private static boolean hasBidirectionalParameters(Method method) { return false; } + private static String bidirectionalParamNames(Method method) { + StringBuilder sb = new StringBuilder(); + for (Class paramType : method.getParameterTypes()) { + if (McpSyncRequestContext.class.isAssignableFrom(paramType) + || McpAsyncRequestContext.class.isAssignableFrom(paramType) + || McpSyncServerExchange.class.isAssignableFrom(paramType) + || McpAsyncServerExchange.class.isAssignableFrom(paramType)) { + if (!sb.isEmpty()) { + sb.append(", "); + } + sb.append(paramType.getSimpleName()); + } + } + return sb.toString(); + } + public static Predicate filterMethodWithBidirectionalParameters() { return method -> { if (!hasBidirectionalParameters(method)) { return true; } - logger.warn( - "Stateless servers doesn't support bidirectional parameters. Skipping method {} with bidirectional parameters", - method); + logger + .warn("Skipping MCP tool method '{}.{}' — stateless servers do not support bidirectional parameters ({}). " + + "To use this tool, switch to a stateful server (McpServerTransportProvider with session support).", + method.getDeclaringClass().getSimpleName(), method.getName(), bidirectionalParamNames(method)); return false; }; } diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java index 315343d07d..40c653a81f 100644 --- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java +++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java @@ -230,7 +230,13 @@ private String unescapeStringValue(String in) { @Override public Filter.Operand visitIntegerConstant(FiltersParser.IntegerConstantContext ctx) { - return new Filter.Value(Integer.valueOf(ctx.getText())); + String text = ctx.getText(); + try { + return new Filter.Value(Integer.parseInt(text)); + } + catch (NumberFormatException e) { + return new Filter.Value(Long.parseLong(text)); + } } @Override diff --git a/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java b/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java index bf227923a7..02756b957a 100644 --- a/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java +++ b/spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java @@ -203,6 +203,20 @@ public void testLong() { assertThat(exp3).isEqualTo(new Expression(EQ, new Key("biz_id"), new Value(-5L))); } + @Test + public void testLargeIntegerFallsBackToLong() { + // Values exceeding Integer.MAX_VALUE (2147483647) should be parsed as Long + Expression exp1 = this.parser.parse("id == 9223372036854775807"); + assertThat(exp1).isEqualTo(new Expression(EQ, new Key("id"), new Value(Long.MAX_VALUE))); + + Expression exp2 = this.parser.parse("id == 2147483648"); + assertThat(exp2).isEqualTo(new Expression(EQ, new Key("id"), new Value(2147483648L))); + + // Values within Integer range should still parse as Integer + Expression exp3 = this.parser.parse("year == 2020"); + assertThat(exp3).isEqualTo(new Expression(EQ, new Key("year"), new Value(2020))); + } + @Test public void testIdentifiers() { Expression exp = this.parser.parse("'country.1' == 'BG'");