Skip to content

Commit 0f75982

Browse files
adase11tzolov
andcommitted
feat(anthropic): add tool choice support for chat model (#4637)
Add support for Anthropic's tool_choice parameter, allowing fine-grained control over how the model uses provided tools. This feature enables users to force, prevent, or influence tool usage behavior. - Add ToolChoice interface with four implementations: ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, and ToolChoiceNone - Add toolChoice field to AnthropicChatOptions with builder support - Add toolChoice parameter to ChatCompletionRequest - Create StringToToolChoiceConverter for property binding from JSON strings - Add integration tests for all tool choice modes - Add documentation with usage examples Signed-off-by: Austin Dase <[email protected]> Co-authored-by: Christian Tzolov <[email protected]>
1 parent c03a5ca commit 0f75982

File tree

8 files changed

+498
-7
lines changed

8 files changed

+498
-7
lines changed

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
3838
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3939
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.Import;
4041
import org.springframework.retry.support.RetryTemplate;
4142
import org.springframework.web.client.ResponseErrorHandler;
4243
import org.springframework.web.client.RestClient;
@@ -57,6 +58,7 @@
5758
@ConditionalOnClass(AnthropicApi.class)
5859
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC,
5960
matchIfMissing = true)
61+
@Import(StringToToolChoiceConverter.class)
6062
public class AnthropicChatAutoConfiguration {
6163

6264
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.anthropic.autoconfigure;
18+
19+
import org.springframework.ai.anthropic.api.AnthropicApi;
20+
import org.springframework.ai.model.ModelOptionsUtils;
21+
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
22+
import org.springframework.core.convert.converter.Converter;
23+
import org.springframework.stereotype.Component;
24+
25+
/**
26+
* Converter to deserialize JSON string into {@link AnthropicApi.ToolChoice}. This
27+
* converter is used by Spring Boot's configuration properties binding to convert string
28+
* values from application properties into ToolChoice objects.
29+
*
30+
* @author Christian Tzolov
31+
* @since 1.0.0
32+
*/
33+
@Component
34+
@ConfigurationPropertiesBinding
35+
public class StringToToolChoiceConverter implements Converter<String, AnthropicApi.ToolChoice> {
36+
37+
@Override
38+
public AnthropicApi.ToolChoice convert(String source) {
39+
return ModelOptionsUtils.jsonToObject(source, AnthropicApi.ToolChoice.class);
40+
}
41+
42+
}

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.junit.jupiter.api.Test;
2020

2121
import org.springframework.ai.anthropic.AnthropicChatModel;
22+
import org.springframework.ai.anthropic.api.AnthropicApi.ToolChoiceTool;
2223
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
2324
import org.springframework.boot.autoconfigure.AutoConfigurations;
2425
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
@@ -75,7 +76,9 @@ public void chatOptionsTest() {
7576

7677
"spring.ai.anthropic.chat.options.temperature=0.55",
7778
"spring.ai.anthropic.chat.options.top-p=0.56",
78-
"spring.ai.anthropic.chat.options.top-k=100"
79+
"spring.ai.anthropic.chat.options.top-k=100",
80+
81+
"spring.ai.anthropic.chat.options.toolChoice={\"name\":\"toolChoiceFunctionName\",\"type\":\"tool\"}"
7982
)
8083
// @formatter:on
8184
.withConfiguration(BaseAnthropicIT.anthropicAutoConfig(AnthropicChatAutoConfiguration.class))
@@ -93,6 +96,11 @@ public void chatOptionsTest() {
9396
assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100);
9497

9598
assertThat(chatProperties.getOptions().getMetadata().userId()).isEqualTo("MyUserId");
99+
100+
assertThat(chatProperties.getOptions().getToolChoice()).isNotNull();
101+
assertThat(chatProperties.getOptions().getToolChoice().type()).isEqualTo("tool");
102+
assertThat(((ToolChoiceTool) chatProperties.getOptions().getToolChoice()).name())
103+
.isEqualTo("toolChoiceFunctionName");
96104
});
97105
}
98106

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
6060
private @JsonProperty("temperature") Double temperature;
6161
private @JsonProperty("top_p") Double topP;
6262
private @JsonProperty("top_k") Integer topK;
63+
private @JsonProperty("tool_choice") AnthropicApi.ToolChoice toolChoice;
6364
private @JsonProperty("thinking") ChatCompletionRequest.ThinkingConfig thinking;
6465

6566
@JsonIgnore
@@ -117,6 +118,7 @@ public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions)
117118
.temperature(fromOptions.getTemperature())
118119
.topP(fromOptions.getTopP())
119120
.topK(fromOptions.getTopK())
121+
.toolChoice(fromOptions.getToolChoice())
120122
.thinking(fromOptions.getThinking())
121123
.toolCallbacks(
122124
fromOptions.getToolCallbacks() != null ? new ArrayList<>(fromOptions.getToolCallbacks()) : null)
@@ -190,6 +192,14 @@ public void setTopK(Integer topK) {
190192
this.topK = topK;
191193
}
192194

195+
public AnthropicApi.ToolChoice getToolChoice() {
196+
return this.toolChoice;
197+
}
198+
199+
public void setToolChoice(AnthropicApi.ToolChoice toolChoice) {
200+
this.toolChoice = toolChoice;
201+
}
202+
193203
public ChatCompletionRequest.ThinkingConfig getThinking() {
194204
return this.thinking;
195205
}
@@ -291,7 +301,8 @@ public boolean equals(Object o) {
291301
&& Objects.equals(this.metadata, that.metadata)
292302
&& Objects.equals(this.stopSequences, that.stopSequences)
293303
&& Objects.equals(this.temperature, that.temperature) && Objects.equals(this.topP, that.topP)
294-
&& Objects.equals(this.topK, that.topK) && Objects.equals(this.thinking, that.thinking)
304+
&& Objects.equals(this.topK, that.topK) && Objects.equals(this.toolChoice, that.toolChoice)
305+
&& Objects.equals(this.thinking, that.thinking)
295306
&& Objects.equals(this.toolCallbacks, that.toolCallbacks)
296307
&& Objects.equals(this.toolNames, that.toolNames)
297308
&& Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled)
@@ -303,8 +314,8 @@ public boolean equals(Object o) {
303314
@Override
304315
public int hashCode() {
305316
return Objects.hash(this.model, this.maxTokens, this.metadata, this.stopSequences, this.temperature, this.topP,
306-
this.topK, this.thinking, this.toolCallbacks, this.toolNames, this.internalToolExecutionEnabled,
307-
this.toolContext, this.httpHeaders, this.cacheOptions);
317+
this.topK, this.toolChoice, this.thinking, this.toolCallbacks, this.toolNames,
318+
this.internalToolExecutionEnabled, this.toolContext, this.httpHeaders, this.cacheOptions);
308319
}
309320

310321
public static final class Builder {
@@ -351,6 +362,11 @@ public Builder topK(Integer topK) {
351362
return this;
352363
}
353364

365+
public Builder toolChoice(AnthropicApi.ToolChoice toolChoice) {
366+
this.options.toolChoice = toolChoice;
367+
return this;
368+
}
369+
354370
public Builder thinking(ChatCompletionRequest.ThinkingConfig thinking) {
355371
this.options.thinking = thinking;
356372
return this;

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ public interface StreamEvent {
512512
* return tool_use content blocks that represent the model's use of those tools. You
513513
* can then run those tools using the tool input generated by the model and then
514514
* optionally return results back to the model using tool_result content blocks.
515+
* @param toolChoice How the model should use the provided tools. The model can use a
516+
* specific tool, any available tool, decide by itself, or not use tools at all.
515517
* @param thinking Configuration for the model's thinking mode. When enabled, the
516518
* model can perform more in-depth reasoning before responding to a query.
517519
*/
@@ -529,17 +531,19 @@ public record ChatCompletionRequest(
529531
@JsonProperty("top_p") Double topP,
530532
@JsonProperty("top_k") Integer topK,
531533
@JsonProperty("tools") List<Tool> tools,
534+
@JsonProperty("tool_choice") ToolChoice toolChoice,
532535
@JsonProperty("thinking") ThinkingConfig thinking) {
533536
// @formatter:on
534537

535538
public ChatCompletionRequest(String model, List<AnthropicMessage> messages, Object system, Integer maxTokens,
536539
Double temperature, Boolean stream) {
537-
this(model, messages, system, maxTokens, null, null, stream, temperature, null, null, null, null);
540+
this(model, messages, system, maxTokens, null, null, stream, temperature, null, null, null, null, null);
538541
}
539542

540543
public ChatCompletionRequest(String model, List<AnthropicMessage> messages, Object system, Integer maxTokens,
541544
List<String> stopSequences, Double temperature, Boolean stream) {
542-
this(model, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null, null, null);
545+
this(model, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null, null, null,
546+
null);
543547
}
544548

545549
public static ChatCompletionRequestBuilder builder() {
@@ -613,6 +617,8 @@ public static final class ChatCompletionRequestBuilder {
613617

614618
private List<Tool> tools;
615619

620+
private ToolChoice toolChoice;
621+
616622
private ChatCompletionRequest.ThinkingConfig thinking;
617623

618624
private ChatCompletionRequestBuilder() {
@@ -630,6 +636,7 @@ private ChatCompletionRequestBuilder(ChatCompletionRequest request) {
630636
this.topP = request.topP;
631637
this.topK = request.topK;
632638
this.tools = request.tools;
639+
this.toolChoice = request.toolChoice;
633640
this.thinking = request.thinking;
634641
}
635642

@@ -693,6 +700,11 @@ public ChatCompletionRequestBuilder tools(List<Tool> tools) {
693700
return this;
694701
}
695702

703+
public ChatCompletionRequestBuilder toolChoice(ToolChoice toolChoice) {
704+
this.toolChoice = toolChoice;
705+
return this;
706+
}
707+
696708
public ChatCompletionRequestBuilder thinking(ChatCompletionRequest.ThinkingConfig thinking) {
697709
this.thinking = thinking;
698710
return this;
@@ -705,7 +717,8 @@ public ChatCompletionRequestBuilder thinking(ThinkingType type, Integer budgetTo
705717

706718
public ChatCompletionRequest build() {
707719
return new ChatCompletionRequest(this.model, this.messages, this.system, this.maxTokens, this.metadata,
708-
this.stopSequences, this.stream, this.temperature, this.topP, this.topK, this.tools, this.thinking);
720+
this.stopSequences, this.stream, this.temperature, this.topP, this.topK, this.tools,
721+
this.toolChoice, this.thinking);
709722
}
710723

711724
}
@@ -1135,6 +1148,126 @@ public Tool(String name, String description, Map<String, Object> inputSchema) {
11351148

11361149
}
11371150

1151+
/**
1152+
* Base interface for tool choice options.
1153+
*/
1154+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
1155+
visible = true)
1156+
@JsonSubTypes({ @JsonSubTypes.Type(value = ToolChoiceAuto.class, name = "auto"),
1157+
@JsonSubTypes.Type(value = ToolChoiceAny.class, name = "any"),
1158+
@JsonSubTypes.Type(value = ToolChoiceTool.class, name = "tool"),
1159+
@JsonSubTypes.Type(value = ToolChoiceNone.class, name = "none") })
1160+
public interface ToolChoice {
1161+
1162+
@JsonProperty("type")
1163+
String type();
1164+
1165+
}
1166+
1167+
/**
1168+
* Auto tool choice - the model will automatically decide whether to use tools.
1169+
*
1170+
* @param type The type of tool choice, always "auto".
1171+
* @param disableParallelToolUse Whether to disable parallel tool use. Defaults to
1172+
* false. If set to true, the model will output at most one tool use.
1173+
*/
1174+
@JsonInclude(Include.NON_NULL)
1175+
public record ToolChoiceAuto(@JsonProperty("type") String type,
1176+
@JsonProperty("disable_parallel_tool_use") Boolean disableParallelToolUse) implements ToolChoice {
1177+
1178+
/**
1179+
* Create an auto tool choice with default settings.
1180+
*/
1181+
public ToolChoiceAuto() {
1182+
this("auto", null);
1183+
}
1184+
1185+
/**
1186+
* Create an auto tool choice with specific parallel tool use setting.
1187+
* @param disableParallelToolUse Whether to disable parallel tool use.
1188+
*/
1189+
public ToolChoiceAuto(Boolean disableParallelToolUse) {
1190+
this("auto", disableParallelToolUse);
1191+
}
1192+
1193+
}
1194+
1195+
/**
1196+
* Any tool choice - the model will use any available tools.
1197+
*
1198+
* @param type The type of tool choice, always "any".
1199+
* @param disableParallelToolUse Whether to disable parallel tool use. Defaults to
1200+
* false. If set to true, the model will output exactly one tool use.
1201+
*/
1202+
@JsonInclude(Include.NON_NULL)
1203+
public record ToolChoiceAny(@JsonProperty("type") String type,
1204+
@JsonProperty("disable_parallel_tool_use") Boolean disableParallelToolUse) implements ToolChoice {
1205+
1206+
/**
1207+
* Create an any tool choice with default settings.
1208+
*/
1209+
public ToolChoiceAny() {
1210+
this("any", null);
1211+
}
1212+
1213+
/**
1214+
* Create an any tool choice with specific parallel tool use setting.
1215+
* @param disableParallelToolUse Whether to disable parallel tool use.
1216+
*/
1217+
public ToolChoiceAny(Boolean disableParallelToolUse) {
1218+
this("any", disableParallelToolUse);
1219+
}
1220+
1221+
}
1222+
1223+
/**
1224+
* Tool choice - the model will use the specified tool.
1225+
*
1226+
* @param type The type of tool choice, always "tool".
1227+
* @param name The name of the tool to use.
1228+
* @param disableParallelToolUse Whether to disable parallel tool use. Defaults to
1229+
* false. If set to true, the model will output exactly one tool use.
1230+
*/
1231+
@JsonInclude(Include.NON_NULL)
1232+
public record ToolChoiceTool(@JsonProperty("type") String type, @JsonProperty("name") String name,
1233+
@JsonProperty("disable_parallel_tool_use") Boolean disableParallelToolUse) implements ToolChoice {
1234+
1235+
/**
1236+
* Create a tool choice for a specific tool.
1237+
* @param name The name of the tool to use.
1238+
*/
1239+
public ToolChoiceTool(String name) {
1240+
this("tool", name, null);
1241+
}
1242+
1243+
/**
1244+
* Create a tool choice for a specific tool with parallel tool use setting.
1245+
* @param name The name of the tool to use.
1246+
* @param disableParallelToolUse Whether to disable parallel tool use.
1247+
*/
1248+
public ToolChoiceTool(String name, Boolean disableParallelToolUse) {
1249+
this("tool", name, disableParallelToolUse);
1250+
}
1251+
1252+
}
1253+
1254+
/**
1255+
* None tool choice - the model will not be allowed to use tools.
1256+
*
1257+
* @param type The type of tool choice, always "none".
1258+
*/
1259+
@JsonInclude(Include.NON_NULL)
1260+
public record ToolChoiceNone(@JsonProperty("type") String type) implements ToolChoice {
1261+
1262+
/**
1263+
* Create a none tool choice.
1264+
*/
1265+
public ToolChoiceNone() {
1266+
this("none");
1267+
}
1268+
1269+
}
1270+
11381271
// CB START EVENT
11391272

11401273
/**

0 commit comments

Comments
 (0)