From e9bfcf41b5ea42bc2ee83ac626d339d7cf2ebcd9 Mon Sep 17 00:00:00 2001 From: guanxu <1510424541@qq.com> Date: Mon, 3 Aug 2026 19:46:47 +0800 Subject: [PATCH] Add FIM (fill-in-the-middle) completion support for DeepSeek Signed-off-by: guanxu <1510424541@qq.com> --- .../autoconfigure/DeepSeekChatProperties.java | 42 +++++ .../DeepSeekPropertiesTests.java | 11 +- .../ai/deepseek/DeepSeekChatModel.java | 6 + .../ai/deepseek/DeepSeekChatOptions.java | 58 ++++++- .../ai/deepseek/api/DeepSeekApi.java | 39 ++++- .../ai/deepseek/DeepSeekChatOptionsTests.java | 151 ++++++++++++++++++ .../ai/deepseek/chat/DeepSeekChatModelIT.java | 19 +++ 7 files changed, 312 insertions(+), 14 deletions(-) diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatProperties.java index f9ded0dbdd..f34e1d7942 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatProperties.java @@ -76,6 +76,10 @@ public class DeepSeekChatProperties extends DeepSeekParentProperties { private @Nullable ReasoningEffort reasoningEffort; + private @Nullable Boolean echo; + + private @Nullable String suffix; + public boolean isEnabled() { return this.enabled; } @@ -196,6 +200,22 @@ public void setReasoningEffort(@Nullable ReasoningEffort reasoningEffort) { this.reasoningEffort = reasoningEffort; } + public @Nullable Boolean getEcho() { + return this.echo; + } + + public void setEcho(@Nullable Boolean echo) { + this.echo = echo; + } + + public @Nullable String getSuffix() { + return this.suffix; + } + + public void setSuffix(@Nullable String suffix) { + this.suffix = suffix; + } + public DeepSeekChatOptions toOptions() { return DeepSeekChatOptions.builder() .model(this.model) @@ -210,6 +230,8 @@ public DeepSeekChatOptions toOptions() { .topLogprobs(this.topLogprobs) .thinking(this.thinking) .reasoningEffort(this.reasoningEffort) + .echo(this.echo) + .suffix(this.suffix) .build(); } @@ -347,6 +369,26 @@ public void setReasoningEffort(@Nullable ReasoningEffort reasoningEffort) { DeepSeekChatProperties.this.setReasoningEffort(reasoningEffort); } + @DeprecatedConfigurationProperty(replacement = "spring.ai.deepseek.chat.echo") + @Deprecated(since = "2.0.0", forRemoval = true) + public @Nullable Boolean getEcho() { + return DeepSeekChatProperties.this.getEcho(); + } + + public void setEcho(@Nullable Boolean echo) { + DeepSeekChatProperties.this.setEcho(echo); + } + + @DeprecatedConfigurationProperty(replacement = "spring.ai.deepseek.chat.suffix") + @Deprecated(since = "2.0.0", forRemoval = true) + public @Nullable String getSuffix() { + return DeepSeekChatProperties.this.getSuffix(); + } + + public void setSuffix(@Nullable String suffix) { + DeepSeekChatProperties.this.setSuffix(suffix); + } + } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java index 2bf6b824fd..cdf6a8c63a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java @@ -119,7 +119,9 @@ public void chatOptionsTest() { "spring.ai.deepseek.chat.top-p=0.56", "spring.ai.deepseek.chat.user=userXYZ", "spring.ai.deepseek.chat.thinking.type=disabled", - "spring.ai.deepseek.chat.reasoning-effort=max" + "spring.ai.deepseek.chat.reasoning-effort=max", + "spring.ai.deepseek.chat.echo=true", + "spring.ai.deepseek.chat.suffix=return x" ) // @formatter:on .withConfiguration(AutoConfigurations.of(DeepSeekChatAutoConfiguration.class, @@ -141,6 +143,13 @@ public void chatOptionsTest() { assertThat(chatProperties.getTopP()).isEqualTo(0.56); assertThat(chatProperties.getThinking()).isEqualTo(Thinking.DISABLED); assertThat(chatProperties.getReasoningEffort()).isEqualTo(ReasoningEffort.MAX); + + assertThat(chatProperties.getEcho()).isTrue(); + assertThat(chatProperties.getSuffix()).isEqualTo("return x"); + + var chatOptions = chatProperties.toOptions(); + assertThat(chatOptions.getEcho()).isTrue(); + assertThat(chatOptions.getSuffix()).isEqualTo("return x"); }); } diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java index 0989b5dc1c..34dab7db66 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java @@ -437,6 +437,12 @@ else if (message.getMessageType() == MessageType.TOOL) { if (options.getReasoningEffort() != null) { requestBuilder.reasoningEffort(options.getReasoningEffort()); } + if (options.getEcho() != null) { + requestBuilder.echo(options.getEcho()); + } + if (options.getSuffix() != null) { + requestBuilder.suffix(options.getSuffix()); + } // Add the tool definitions to the request's tools parameter. List toolDefinitions = this.toolCallingManager.resolveToolDefinitions(options); diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatOptions.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatOptions.java index e7b17a7fa6..9d9ef28a46 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatOptions.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatOptions.java @@ -142,6 +142,16 @@ public class DeepSeekChatOptions implements ToolCallingChatOptions { */ private final @Nullable ReasoningEffort reasoningEffort; + /** + * Echo back the prompt in addition to the completion. + */ + private final @Nullable Boolean echo; + + /** + * The suffix that comes after a completion of inserted text. + */ + private final @Nullable String suffix; + /** * Tool Function Callbacks to register with the ChatModel. * For Prompt Options the toolCallbacks are automatically enabled for the duration of the prompt execution. @@ -158,7 +168,8 @@ protected DeepSeekChatOptions(@Nullable String model, @Nullable Double frequency @Nullable List stop, @Nullable Double temperature, @Nullable Double topP, @Nullable Boolean logprobs, @Nullable Integer topLogprobs, @Nullable List tools, @Nullable Object toolChoice, @Nullable Thinking thinking, @Nullable ReasoningEffort reasoningEffort, - @Nullable List toolCallbacks, @Nullable Map toolContext) { + @Nullable Boolean echo, @Nullable String suffix, @Nullable List toolCallbacks, + @Nullable Map toolContext) { this.model = model != null ? model : DeepSeekApi.DEFAULT_CHAT_MODEL.getValue(); this.frequencyPenalty = frequencyPenalty; this.maxTokens = maxTokens; @@ -173,6 +184,8 @@ protected DeepSeekChatOptions(@Nullable String model, @Nullable Double frequency this.toolChoice = toolChoice; this.thinking = thinking; this.reasoningEffort = reasoningEffort; + this.echo = echo; + this.suffix = suffix; this.toolCallbacks = toolCallbacks != null ? List.copyOf(toolCallbacks) : null; this.toolContext = toolContext != null ? Map.copyOf(toolContext) : null; } @@ -253,6 +266,14 @@ public String getModel() { return this.topLogprobs; } + public @Nullable Boolean getEcho() { + return this.echo; + } + + public @Nullable String getSuffix() { + return this.suffix; + } + @Override public @Nullable Integer getTopK() { return null; @@ -285,14 +306,17 @@ public Builder mutate() { .tools(this.tools) .toolChoice(this.toolChoice) .thinking(this.thinking) - .reasoningEffort(this.reasoningEffort); + .reasoningEffort(this.reasoningEffort) + .echo(this.echo) + .suffix(this.suffix); } @Override public int hashCode() { return Objects.hash(this.model, this.frequencyPenalty, this.logprobs, this.topLogprobs, this.maxTokens, this.presencePenalty, this.responseFormat, this.stop, this.temperature, this.topP, this.tools, - this.toolChoice, this.thinking, this.reasoningEffort, this.toolCallbacks, this.toolContext); + this.toolChoice, this.thinking, this.reasoningEffort, this.echo, this.suffix, this.toolCallbacks, + this.toolContext); } @Override @@ -312,8 +336,8 @@ public boolean equals(@Nullable Object o) { && Objects.equals(this.temperature, other.temperature) && Objects.equals(this.topP, other.topP) && Objects.equals(this.tools, other.tools) && Objects.equals(this.toolChoice, other.toolChoice) && Objects.equals(this.thinking, other.thinking) - && Objects.equals(this.reasoningEffort, other.reasoningEffort) - && Objects.equals(this.toolCallbacks, other.toolCallbacks) + && Objects.equals(this.reasoningEffort, other.reasoningEffort) && Objects.equals(this.echo, other.echo) + && Objects.equals(this.suffix, other.suffix) && Objects.equals(this.toolCallbacks, other.toolCallbacks) && Objects.equals(this.toolContext, other.toolContext); } @@ -347,6 +371,10 @@ public B clone() { protected @Nullable ReasoningEffort reasoningEffort; + protected @Nullable Boolean echo; + + protected @Nullable String suffix; + public B model(DeepSeekApi.@Nullable ChatModel deepseekAiChatModel) { if (deepseekAiChatModel == null) { this.model = null; @@ -416,6 +444,16 @@ public B reasoningEffortMax() { return self(); } + public B echo(@Nullable Boolean echo) { + this.echo = echo; + return self(); + } + + public B suffix(@Nullable String suffix) { + this.suffix = suffix; + return self(); + } + public B combineWith(ChatOptions.Builder other) { super.combineWith(other); if (other instanceof AbstractBuilder that) { @@ -447,6 +485,12 @@ public B combineWith(ChatOptions.Builder other) { if (that.reasoningEffort != null) { this.reasoningEffort = that.reasoningEffort; } + if (that.echo != null) { + this.echo = that.echo; + } + if (that.suffix != null) { + this.suffix = that.suffix; + } } return self(); } @@ -455,8 +499,8 @@ public B combineWith(ChatOptions.Builder other) { public DeepSeekChatOptions build() { return new DeepSeekChatOptions(this.model, this.frequencyPenalty, this.maxTokens, this.presencePenalty, this.responseFormat, this.stopSequences, this.temperature, this.topP, this.logprobs, - this.topLogprobs, this.tools, this.toolChoice, this.thinking, this.reasoningEffort, - this.toolCallbacks, this.toolContext); + this.topLogprobs, this.tools, this.toolChoice, this.thinking, this.reasoningEffort, this.echo, + this.suffix, this.toolCallbacks, this.toolContext); } } diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekApi.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekApi.java index e95dd6eb49..bc921e0801 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekApi.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekApi.java @@ -42,6 +42,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -218,7 +219,7 @@ private String getEndpoint(ChatCompletionRequest request) { .map(ChatCompletionMessage::prefix) .filter(Objects::nonNull) .anyMatch(prefix -> prefix); - String endpointPrefix = isPrefix ? this.betaPrefixPath : ""; + String endpointPrefix = isPrefix || StringUtils.hasText(request.suffix) ? this.betaPrefixPath : ""; return endpointPrefix + this.completionsPath; } @@ -517,7 +518,9 @@ public record ChatCompletionRequest(// @formatter:off @JsonProperty("tools") @Nullable List tools, @JsonProperty("tool_choice") @Nullable Object toolChoice, @JsonProperty("thinking") @Nullable Thinking thinking, - @JsonProperty("reasoning_effort") @Nullable ReasoningEffort reasoningEffort) { + @JsonProperty("reasoning_effort") @Nullable ReasoningEffort reasoningEffort, + @JsonProperty("echo") @Nullable Boolean echo, + @JsonProperty("suffix") @Nullable String suffix) { /** * Create a new {@link ChatCompletionRequest} builder. @@ -537,7 +540,7 @@ public static Builder builder() { public ChatCompletionRequest(List messages, Boolean stream) { this(messages, null, null, null, null, null, null, stream, null, null, null, null, null, - null, null, null); + null, null, null, null, null); } /** @@ -550,7 +553,7 @@ public ChatCompletionRequest(List messages, Boolean strea public ChatCompletionRequest(List messages, String model, Double temperature) { this(messages, model, null, null, null, null, null, false, temperature, null, - null, null, null, null, null, null); + null, null, null, null, null, null, null, null); } /** @@ -565,7 +568,7 @@ public ChatCompletionRequest(List messages, String model, public ChatCompletionRequest(List messages, String model, Double temperature, boolean stream) { this(messages, model, null, null, null, null, null, stream, temperature, null, - null, null, null, null, null, null); + null, null, null, null, null, null, null, null); } /** @@ -683,6 +686,10 @@ public static class Builder { private @Nullable Thinking thinking; + private @Nullable Boolean echo; + + private @Nullable String suffix; + /** * Set the messages comprising the conversation so far. * @param messages the messages. @@ -843,6 +850,26 @@ public Builder thinking(@Nullable Thinking thinking) { return this; } + /** + * Set whether to echo the prompt. + * @param echo whether to echo the prompt. + * @return this builder. + */ + public Builder echo(@Nullable Boolean echo) { + this.echo = echo; + return this; + } + + /** + * Set the suffix. + * @param suffix the suffix. + * @return this builder. + */ + public Builder suffix(@Nullable String suffix) { + this.suffix = suffix; + return this; + } + /** * Build the {@link ChatCompletionRequest}. * @return a new {@link ChatCompletionRequest}. @@ -852,7 +879,7 @@ public ChatCompletionRequest build() { return new ChatCompletionRequest(this.messages, this.model, this.frequencyPenalty, this.maxTokens, this.presencePenalty, this.responseFormat, this.stop, this.stream, this.temperature, this.topP, this.logprobs, this.topLogprobs, this.tools, this.toolChoice, this.thinking, - this.reasoningEffort); + this.reasoningEffort, this.echo, this.suffix); } } diff --git a/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/DeepSeekChatOptionsTests.java b/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/DeepSeekChatOptionsTests.java index 4d80990218..5aa6473d8b 100644 --- a/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/DeepSeekChatOptionsTests.java +++ b/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/DeepSeekChatOptionsTests.java @@ -18,12 +18,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.ai.deepseek.DeepSeekChatOptions.Builder; import org.springframework.ai.deepseek.api.DeepSeekApi; +import org.springframework.ai.deepseek.api.MockWeatherService; +import org.springframework.ai.deepseek.api.ResponseFormat; import org.springframework.ai.test.options.AbstractChatOptionsTests; +import org.springframework.ai.tool.function.FunctionToolCallback; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +35,7 @@ * Tests for {@link DeepSeekChatOptions}. * * @author Geng Rong + * @author guan xu */ class DeepSeekChatOptionsTests extends AbstractChatOptionsTests { @@ -79,4 +84,150 @@ void cloneHandlesNullToolsList() { assertThat(DeepSeekChatOptions.builder().clone().build().getTools()).isNull(); } + @Test + void testGetters() { + DeepSeekChatOptions options = fullyPopulatedBuilder().build(); + + assertThat(options.getModel()).isEqualTo("deepseek-chat"); + assertThat(options.getFrequencyPenalty()).isEqualTo(0.5); + assertThat(options.getMaxTokens()).isEqualTo(128); + assertThat(options.getPresencePenalty()).isEqualTo(0.3); + assertThat(options.getResponseFormat()) + .isEqualTo(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()); + assertThat(options.getStop()).containsExactly("foo", "bar"); + assertThat(options.getStopSequences()).containsExactly("foo", "bar"); + assertThat(options.getTemperature()).isEqualTo(0.2); + assertThat(options.getTopP()).isEqualTo(0.9); + assertThat(options.getLogprobs()).isTrue(); + assertThat(options.getTopLogprobs()).isEqualTo(5); + assertThat(options.getEcho()).isTrue(); + assertThat(options.getSuffix()).isEqualTo("return result"); + assertThat(options.getTools()).hasSize(1); + assertThat(options.getToolChoice()).isEqualTo("auto"); + assertThat(options.getToolCallbacks()).hasSize(1); + assertThat(options.getToolContext()).containsEntry("locale", "en-US"); + assertThat(options.getTopK()).isNull(); + } + + @Test + void testMutate() { + DeepSeekChatOptions options = fullyPopulatedBuilder().build(); + DeepSeekChatOptions mutated = options.mutate().build(); + + assertThat(mutated).isEqualTo(options); + assertThat(mutated).isNotSameAs(options); + assertThat(mutated.getModel()).isEqualTo(options.getModel()); + assertThat(mutated.getFrequencyPenalty()).isEqualTo(options.getFrequencyPenalty()); + assertThat(mutated.getMaxTokens()).isEqualTo(options.getMaxTokens()); + assertThat(mutated.getPresencePenalty()).isEqualTo(options.getPresencePenalty()); + assertThat(mutated.getResponseFormat()).isEqualTo(options.getResponseFormat()); + assertThat(mutated.getStop()).isEqualTo(options.getStop()); + assertThat(mutated.getTemperature()).isEqualTo(options.getTemperature()); + assertThat(mutated.getTopP()).isEqualTo(options.getTopP()); + assertThat(mutated.getLogprobs()).isEqualTo(options.getLogprobs()); + assertThat(mutated.getTopLogprobs()).isEqualTo(options.getTopLogprobs()); + assertThat(mutated.getEcho()).isEqualTo(options.getEcho()); + assertThat(mutated.getSuffix()).isEqualTo(options.getSuffix()); + assertThat(mutated.getTools()).isEqualTo(options.getTools()); + assertThat(mutated.getToolChoice()).isEqualTo(options.getToolChoice()); + assertThat(mutated.getToolCallbacks()).isEqualTo(options.getToolCallbacks()); + assertThat(mutated.getToolContext()).isEqualTo(options.getToolContext()); + } + + @Test + void combineWithOverridesScalars() { + DeepSeekChatOptions base = fullyPopulatedBuilder().build(); + DeepSeekChatOptions override = DeepSeekChatOptions.builder() + .model("deepseek-reasoner") + .frequencyPenalty(1.0) + .maxTokens(256) + .presencePenalty(0.8) + .responseFormat(ResponseFormat.builder().type(ResponseFormat.Type.TEXT).build()) + .stop(List.of("baz")) + .temperature(0.9) + .topP(0.1) + .logprobs(false) + .topLogprobs(2) + .echo(false) + .suffix("other suffix") + .toolChoice("none") + .build(); + + DeepSeekChatOptions merged = base.mutate().combineWith(override.mutate()).build(); + + assertThat(merged.getModel()).isEqualTo("deepseek-reasoner"); + assertThat(merged.getFrequencyPenalty()).isEqualTo(1.0); + assertThat(merged.getMaxTokens()).isEqualTo(256); + assertThat(merged.getPresencePenalty()).isEqualTo(0.8); + assertThat(merged.getResponseFormat()) + .isEqualTo(ResponseFormat.builder().type(ResponseFormat.Type.TEXT).build()); + assertThat(merged.getTemperature()).isEqualTo(0.9); + assertThat(merged.getTopP()).isEqualTo(0.1); + assertThat(merged.getLogprobs()).isFalse(); + assertThat(merged.getTopLogprobs()).isEqualTo(2); + assertThat(merged.getEcho()).isFalse(); + assertThat(merged.getSuffix()).isEqualTo("other suffix"); + assertThat(merged.getToolChoice()).isEqualTo("none"); + assertThat(merged.getStop()).containsExactly("foo", "bar", "baz"); + } + + @Test + void combineWithKeepsBaseValuesWhenOverrideIsNull() { + DeepSeekChatOptions base = fullyPopulatedBuilder().build(); + DeepSeekChatOptions override = DeepSeekChatOptions.builder().build(); + + DeepSeekChatOptions merged = base.mutate().combineWith(override.mutate()).build(); + + assertThat(merged.getFrequencyPenalty()).isEqualTo(0.5); + assertThat(merged.getMaxTokens()).isEqualTo(128); + assertThat(merged.getPresencePenalty()).isEqualTo(0.3); + assertThat(merged.getTemperature()).isEqualTo(0.2); + assertThat(merged.getTopP()).isEqualTo(0.9); + assertThat(merged.getLogprobs()).isTrue(); + assertThat(merged.getTopLogprobs()).isEqualTo(5); + assertThat(merged.getEcho()).isTrue(); + assertThat(merged.getSuffix()).isEqualTo("return result"); + assertThat(merged.getResponseFormat()) + .isEqualTo(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()); + assertThat(merged.getToolChoice()).isEqualTo("auto"); + } + + @Test + void testEqualsAndHashCode() { + DeepSeekChatOptions a = fullyPopulatedBuilder().build(); + DeepSeekChatOptions b = fullyPopulatedBuilder().build(); + + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + private Builder fullyPopulatedBuilder() { + return DeepSeekChatOptions.builder() + .model("deepseek-chat") + .frequencyPenalty(0.5) + .maxTokens(128) + .presencePenalty(0.3) + .responseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()) + .stop(List.of("foo", "bar")) + .temperature(0.2) + .topP(0.9) + .logprobs(true) + .topLogprobs(5) + .echo(true) + .suffix("return result") + .tools(List.of(FUNCTION_TOOL)) + .toolChoice("auto") + .toolCallbacks(List.of(WEATHER_TOOL_CALLBACK)) + .toolContext(Map.of("locale", "en-US")); + } + + private static final DeepSeekApi.FunctionTool FUNCTION_TOOL = new DeepSeekApi.FunctionTool( + DeepSeekApi.FunctionTool.Type.FUNCTION, new DeepSeekApi.FunctionTool.Function("function", "desc", "{}")); + + private static final FunctionToolCallback WEATHER_TOOL_CALLBACK = FunctionToolCallback + .builder("getCurrentWeather", new MockWeatherService()) + .description("Get the weather in location") + .inputType(MockWeatherService.Request.class) + .build(); + } diff --git a/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/chat/DeepSeekChatModelIT.java b/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/chat/DeepSeekChatModelIT.java index 85e2e3b76c..24c0adac28 100644 --- a/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/chat/DeepSeekChatModelIT.java +++ b/models/spring-ai-deepseek/src/test/java/org/springframework/ai/deepseek/chat/DeepSeekChatModelIT.java @@ -309,7 +309,26 @@ void reasoningEffortHighTest() { assertThat(deepSeekAssistantMessage.getText()).isNotEmpty(); } + @Test + void suffixCompletionTest() { + // DeepSeek FIM (fill-in-the-middle) completion API usage: + // prompt="def fib(a):" + // suffix=" return fib(a-1) + fib(a-2)" + String promptContent = "def fib(a):"; + String suffix = " return fib(a-1) + fib(a-2)"; + Prompt prompt = Prompt.builder() + .messages(UserMessage.builder().text(promptContent).build()) + .chatOptions(DeepSeekChatOptions.builder().echo(true).suffix(suffix).maxTokens(128).build()) + .build(); + ChatResponse response = this.chatModel.call(prompt); + + String output = response.getResult().getOutput().getText(); + assertThat(output).isNotEmpty(); + assertThat(output).contains(promptContent); + } + record ActorsFilmsRecord(String actor, List movies) { + } }