Skip to content

Commit 7d387cf

Browse files
committed
feat: Enhance Anthropic integration with Thinking
- The `thinking` option is added to `AnthropicChatOptions` and `ChatCompletionRequest`. - The `AnthropicApi` and `AnthropicChatModel` now handle `THINKING` and `REDACTED_THINKING` content blocks in responses. New tests verify parsing of these blocks. - Updated method signatures on ChatCompletionRequestBuilder, deprecating old builders with `with*` prefix in favor of those without. Signed-off-by: Alexandros Pappas <[email protected]>
1 parent 6cb15e4 commit 7d387cf

File tree

7 files changed

+413
-65
lines changed

7 files changed

+413
-65
lines changed

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

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Base64;
21+
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.Set;
@@ -379,46 +380,49 @@ private ChatResponse toChatResponse(ChatCompletionResponse chatCompletion, Usage
379380
return new ChatResponse(List.of());
380381
}
381382

382-
List<Generation> generations = chatCompletion.content()
383-
.stream()
384-
.filter(content -> content.type() != ContentBlock.Type.TOOL_USE)
385-
.map(content -> new Generation(new AssistantMessage(content.text(), Map.of()),
386-
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build()))
387-
.toList();
388-
389-
List<Generation> allGenerations = new ArrayList<>(generations);
383+
List<Generation> generations = new ArrayList<>();
384+
List<AssistantMessage.ToolCall> toolCalls = new ArrayList<>();
385+
for (ContentBlock content : chatCompletion.content()) {
386+
switch (content.type()) {
387+
case TEXT:
388+
generations.add(new Generation(new AssistantMessage(content.text(), Map.of()),
389+
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build()));
390+
break;
391+
case THINKING:
392+
Map<String, Object> thinkingProperties = new HashMap<>();
393+
thinkingProperties.put("signature", content.signature());
394+
generations.add(new Generation(new AssistantMessage(content.thinking(), thinkingProperties),
395+
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build()));
396+
break;
397+
case REDACTED_THINKING:
398+
Map<String, Object> redactedProperties = new HashMap<>();
399+
redactedProperties.put("data", content.data());
400+
generations.add(new Generation(new AssistantMessage(null, redactedProperties),
401+
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build()));
402+
break;
403+
case TOOL_USE:
404+
var functionCallId = content.id();
405+
var functionName = content.name();
406+
var functionArguments = JsonParser.toJson(content.input());
407+
toolCalls.add(
408+
new AssistantMessage.ToolCall(functionCallId, "function", functionName, functionArguments));
409+
break;
410+
}
411+
}
390412

391413
if (chatCompletion.stopReason() != null && generations.isEmpty()) {
392414
Generation generation = new Generation(new AssistantMessage(null, Map.of()),
393415
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build());
394-
allGenerations.add(generation);
416+
generations.add(generation);
395417
}
396418

397-
List<ContentBlock> toolToUseList = chatCompletion.content()
398-
.stream()
399-
.filter(c -> c.type() == ContentBlock.Type.TOOL_USE)
400-
.toList();
401-
402-
if (!CollectionUtils.isEmpty(toolToUseList)) {
403-
List<AssistantMessage.ToolCall> toolCalls = new ArrayList<>();
404-
405-
for (ContentBlock toolToUse : toolToUseList) {
406-
407-
var functionCallId = toolToUse.id();
408-
var functionName = toolToUse.name();
409-
var functionArguments = JsonParser.toJson(toolToUse.input());
410-
411-
toolCalls
412-
.add(new AssistantMessage.ToolCall(functionCallId, "function", functionName, functionArguments));
413-
}
414-
419+
if (!CollectionUtils.isEmpty(toolCalls)) {
415420
AssistantMessage assistantMessage = new AssistantMessage("", Map.of(), toolCalls);
416421
Generation toolCallGeneration = new Generation(assistantMessage,
417422
ChatGenerationMetadata.builder().finishReason(chatCompletion.stopReason()).build());
418-
allGenerations.add(toolCallGeneration);
423+
generations.add(toolCallGeneration);
419424
}
420-
421-
return new ChatResponse(allGenerations, this.from(chatCompletion, usage));
425+
return new ChatResponse(generations, this.from(chatCompletion, usage));
422426
}
423427

424428
private ChatResponseMetadata from(AnthropicApi.ChatCompletionResponse result) {
@@ -575,7 +579,7 @@ else if (message.getMessageType() == MessageType.TOOL) {
575579
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
576580
if (!CollectionUtils.isEmpty(toolDefinitions)) {
577581
request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class);
578-
request = ChatCompletionRequest.from(request).withTools(getFunctionTools(toolDefinitions)).build();
582+
request = ChatCompletionRequest.from(request).tools(getFunctionTools(toolDefinitions)).build();
579583
}
580584

581585
return request;

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class AnthropicChatOptions implements ToolCallingChatOptions {
5656
private @JsonProperty("temperature") Double temperature;
5757
private @JsonProperty("top_p") Double topP;
5858
private @JsonProperty("top_k") Integer topK;
59+
private @JsonProperty("thinking") ChatCompletionRequest.ThinkingConfig thinking;
5960

6061
/**
6162
* Collection of {@link ToolCallback}s to be used for tool calling in the chat
@@ -94,6 +95,7 @@ public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions)
9495
.temperature(fromOptions.getTemperature())
9596
.topP(fromOptions.getTopP())
9697
.topK(fromOptions.getTopK())
98+
.thinking(fromOptions.getThinking())
9799
.toolCallbacks(fromOptions.getToolCallbacks())
98100
.toolNames(fromOptions.getToolNames())
99101
.internalToolExecutionEnabled(fromOptions.isInternalToolExecutionEnabled())
@@ -163,6 +165,14 @@ public void setTopK(Integer topK) {
163165
this.topK = topK;
164166
}
165167

168+
public ChatCompletionRequest.ThinkingConfig getThinking() {
169+
return this.thinking;
170+
}
171+
172+
public void setThinking(ChatCompletionRequest.ThinkingConfig thinking) {
173+
this.thinking = thinking;
174+
}
175+
166176
@Override
167177
@JsonIgnore
168178
public List<FunctionCallback> getToolCallbacks() {
@@ -319,6 +329,16 @@ public Builder topK(Integer topK) {
319329
return this;
320330
}
321331

332+
public Builder thinking(ChatCompletionRequest.ThinkingConfig thinking) {
333+
this.options.thinking = thinking;
334+
return this;
335+
}
336+
337+
public Builder thinking(AnthropicApi.ThinkingType type, Integer budgetTokens) {
338+
this.options.thinking = new ChatCompletionRequest.ThinkingConfig(type, budgetTokens);
339+
return this;
340+
}
341+
322342
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
323343
this.options.setToolCallbacks(toolCallbacks);
324344
return this;

0 commit comments

Comments
 (0)