Skip to content

Commit 5600f8d

Browse files
authored
Merge branch 'spring-projects:main' into main
2 parents bee4480 + f907196 commit 5600f8d

File tree

9 files changed

+438
-16
lines changed

9 files changed

+438
-16
lines changed

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import io.modelcontextprotocol.client.McpAsyncClient;
2929
import io.modelcontextprotocol.client.McpSyncClient;
3030
import io.modelcontextprotocol.server.McpServerFeatures;
31-
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
3231
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
3332
import io.modelcontextprotocol.server.McpSyncServerExchange;
3433
import io.modelcontextprotocol.spec.McpSchema;
@@ -222,8 +221,10 @@ public static McpStatelessServerFeatures.SyncToolSpecification toStatelessSyncTo
222221

223222
var sharedSpec = toSharedSyncToolSpecification(toolCallback, mimeType);
224223

225-
return new McpStatelessServerFeatures.SyncToolSpecification(sharedSpec.tool(),
226-
(exchange, request) -> sharedSpec.sharedHandler().apply(exchange, request));
224+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
225+
.tool(sharedSpec.tool())
226+
.callHandler((exchange, request) -> sharedSpec.sharedHandler().apply(exchange, request))
227+
.build();
227228
}
228229

229230
private static SharedSyncToolSpecification toSharedSyncToolSpecification(ToolCallback toolCallback,
@@ -241,9 +242,9 @@ private static SharedSyncToolSpecification toSharedSyncToolSpecification(ToolCal
241242
String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request.arguments()),
242243
new ToolContext(Map.of(TOOL_CONTEXT_MCP_EXCHANGE_KEY, exchangeOrContext)));
243244
if (mimeType != null && mimeType.toString().startsWith("image")) {
244-
return new McpSchema.CallToolResult(List
245-
.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, callResult, mimeType.toString())),
246-
false);
245+
McpSchema.Annotations annotations = new McpSchema.Annotations(List.of(Role.ASSISTANT), null);
246+
return new McpSchema.CallToolResult(
247+
List.of(new McpSchema.ImageContent(annotations, callResult, mimeType.toString())), false);
247248
}
248249
return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false);
249250
}
@@ -353,10 +354,13 @@ public static McpServerFeatures.AsyncToolSpecification toAsyncToolSpecification(
353354

354355
McpServerFeatures.SyncToolSpecification syncToolSpecification = toSyncToolSpecification(toolCallback, mimeType);
355356

356-
return new AsyncToolSpecification(syncToolSpecification.tool(),
357-
(exchange, map) -> Mono
358-
.fromCallable(() -> syncToolSpecification.call().apply(new McpSyncServerExchange(exchange), map))
359-
.subscribeOn(Schedulers.boundedElastic()));
357+
return McpServerFeatures.AsyncToolSpecification.builder()
358+
.tool(syncToolSpecification.tool())
359+
.callHandler((exchange, request) -> Mono
360+
.fromCallable(
361+
() -> syncToolSpecification.callHandler().apply(new McpSyncServerExchange(exchange), request))
362+
.subscribeOn(Schedulers.boundedElastic()))
363+
.build();
360364
}
361365

362366
public static McpStatelessServerFeatures.AsyncToolSpecification toStatelessAsyncToolSpecification(

mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsTests.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification;
2727
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
2828
import io.modelcontextprotocol.server.McpSyncServerExchange;
29+
import io.modelcontextprotocol.spec.McpSchema;
2930
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
3031
import io.modelcontextprotocol.spec.McpSchema.Implementation;
3132
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
@@ -267,7 +268,9 @@ void toAsyncToolSpecificationShouldConvertSingleCallback() {
267268
assertThat(toolSpecification).isNotNull();
268269
assertThat(toolSpecification.tool().name()).isEqualTo("test");
269270

270-
StepVerifier.create(toolSpecification.call().apply(mock(McpAsyncServerExchange.class), Map.of()))
271+
StepVerifier
272+
.create(toolSpecification.callHandler()
273+
.apply(mock(McpAsyncServerExchange.class), mock(McpSchema.CallToolRequest.class)))
271274
.assertNext(result -> {
272275
TextContent content = (TextContent) result.content().get(0);
273276
assertThat(content.text()).isEqualTo("success");
@@ -283,7 +286,9 @@ void toAsyncToolSpecificationShouldHandleError() {
283286
AsyncToolSpecification toolSpecification = McpToolUtils.toAsyncToolSpecification(callback);
284287

285288
assertThat(toolSpecification).isNotNull();
286-
StepVerifier.create(toolSpecification.call().apply(mock(McpAsyncServerExchange.class), Map.of()))
289+
StepVerifier
290+
.create(toolSpecification.callHandler()
291+
.apply(mock(McpAsyncServerExchange.class), mock(McpSchema.CallToolRequest.class)))
287292
.assertNext(result -> {
288293
TextContent content = (TextContent) result.content().get(0);
289294
assertThat(content.text()).isEqualTo("error");

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureEmbeddingsOptionsTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.ai.embedding.EmbeddingRequest;
3131

3232
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
3334

3435
/**
3536
* @author Christian Tzolov
@@ -202,4 +203,41 @@ public void shouldValidateInputListIsNotModified() {
202203
assertThat(inputsCopy).isEqualTo(originalInputs);
203204
}
204205

206+
@Test
207+
public void shouldHandleNullInputList() {
208+
var requestOptions = this.client.toEmbeddingOptions(new EmbeddingRequest(null, null));
209+
assertThat(requestOptions.getInput()).isNull();
210+
}
211+
212+
@Test
213+
public void shouldHandleNullEmbeddingRequest() {
214+
assertThatThrownBy(() -> this.client.toEmbeddingOptions(null)).isInstanceOf(NullPointerException.class);
215+
}
216+
217+
@Test
218+
public void shouldHandlePartialOptionsOverride() {
219+
var partialOptions = AzureOpenAiEmbeddingOptions.builder()
220+
.deploymentName("CUSTOM_MODEL")
221+
// user is not set, should use default
222+
.build();
223+
224+
var requestOptions = this.client
225+
.toEmbeddingOptions(new EmbeddingRequest(List.of("Test content"), partialOptions));
226+
227+
assertThat(requestOptions.getModel()).isEqualTo("CUSTOM_MODEL");
228+
assertThat(requestOptions.getUser()).isEqualTo("USER_TEST"); // from default
229+
}
230+
231+
@Test
232+
public void shouldHandleDefaultOptionsOnlyClient() {
233+
var clientWithMinimalDefaults = new AzureOpenAiEmbeddingModel(this.mockClient, MetadataMode.EMBED,
234+
AzureOpenAiEmbeddingOptions.builder().deploymentName("MINIMAL_MODEL").build());
235+
236+
var requestOptions = clientWithMinimalDefaults
237+
.toEmbeddingOptions(new EmbeddingRequest(List.of("Test content"), null));
238+
239+
assertThat(requestOptions.getModel()).isEqualTo("MINIMAL_MODEL");
240+
assertThat(requestOptions.getUser()).isNull();
241+
}
242+
205243
}

models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/aot/GoogleGenAiRuntimeHintsTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,32 @@ void registerHints() {
5454
assertThat(registeredTypes.contains(TypeReference.of(GoogleGenAiChatOptions.class))).isTrue();
5555
}
5656

57+
@Test
58+
void registerHintsWithNullClassLoader() {
59+
RuntimeHints runtimeHints = new RuntimeHints();
60+
GoogleGenAiRuntimeHints googleGenAiRuntimeHints = new GoogleGenAiRuntimeHints();
61+
62+
googleGenAiRuntimeHints.registerHints(runtimeHints, null);
63+
64+
assertThat(runtimeHints.reflection().typeHints().count()).isGreaterThan(0);
65+
}
66+
67+
@Test
68+
void verifyNoProxyHintsAreRegistered() {
69+
RuntimeHints runtimeHints = new RuntimeHints();
70+
GoogleGenAiRuntimeHints googleGenAiRuntimeHints = new GoogleGenAiRuntimeHints();
71+
googleGenAiRuntimeHints.registerHints(runtimeHints, null);
72+
73+
assertThat(runtimeHints.proxies().jdkProxyHints().count()).isEqualTo(0);
74+
}
75+
76+
@Test
77+
void verifyNoSerializationHintsAreRegistered() {
78+
RuntimeHints runtimeHints = new RuntimeHints();
79+
GoogleGenAiRuntimeHints googleGenAiRuntimeHints = new GoogleGenAiRuntimeHints();
80+
googleGenAiRuntimeHints.registerHints(runtimeHints, null);
81+
82+
assertThat(runtimeHints.serialization().javaSerializationHints().count()).isEqualTo(0);
83+
}
84+
5785
}

models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaApiHelper.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525

2626
/**
2727
* @author Christian Tzolov
28+
* @author Sun Yuhan
2829
* @since 1.0.0
2930
*/
3031
public final class OllamaApiHelper {
@@ -81,12 +82,20 @@ public static ChatResponse merge(ChatResponse previous, ChatResponse current) {
8182
private static OllamaApi.Message merge(OllamaApi.Message previous, OllamaApi.Message current) {
8283

8384
String content = mergeContent(previous, current);
85+
String thinking = mergeThinking(previous, current);
8486
OllamaApi.Message.Role role = (current.role() != null ? current.role() : previous.role());
8587
role = (role != null ? role : OllamaApi.Message.Role.ASSISTANT);
8688
List<String> images = mergeImages(previous, current);
8789
List<OllamaApi.Message.ToolCall> toolCalls = mergeToolCall(previous, current);
88-
89-
return OllamaApi.Message.builder(role).content(content).images(images).toolCalls(toolCalls).build();
90+
String toolName = mergeToolName(previous, current);
91+
92+
return OllamaApi.Message.builder(role)
93+
.content(content)
94+
.thinking(thinking)
95+
.images(images)
96+
.toolCalls(toolCalls)
97+
.toolName(toolName)
98+
.build();
9099
}
91100

92101
private static Instant merge(Instant previous, Instant current) {
@@ -145,6 +154,28 @@ private static List<OllamaApi.Message.ToolCall> mergeToolCall(OllamaApi.Message
145154
return merge(previous.toolCalls(), current.toolCalls());
146155
}
147156

157+
private static String mergeThinking(OllamaApi.Message previous, OllamaApi.Message current) {
158+
if (previous == null || previous.thinking() == null) {
159+
return (current != null ? current.thinking() : null);
160+
}
161+
if (current == null || current.thinking() == null) {
162+
return (previous.thinking());
163+
}
164+
165+
return previous.thinking() + current.thinking();
166+
}
167+
168+
private static String mergeToolName(OllamaApi.Message previous, OllamaApi.Message current) {
169+
if (previous == null || previous.toolName() == null) {
170+
return (current != null ? current.toolName() : null);
171+
}
172+
if (current == null || current.toolName() == null) {
173+
return (previous.toolName());
174+
}
175+
176+
return previous.toolName() + current.toolName();
177+
}
178+
148179
private static List<String> mergeImages(OllamaApi.Message previous, OllamaApi.Message current) {
149180
if (previous == null) {
150181
return (current != null ? current.images() : null);

0 commit comments

Comments
 (0)