|
51 | 51 | import com.anthropic.models.messages.TextBlock; |
52 | 52 | import com.anthropic.models.messages.ThinkingBlock; |
53 | 53 | import com.anthropic.models.messages.ToolResultBlockParam; |
| 54 | +import com.anthropic.models.messages.ToolUnion; |
54 | 55 | import com.anthropic.models.messages.ToolUseBlock; |
55 | 56 | import com.anthropic.models.messages.Usage; |
56 | 57 | import com.anthropic.services.async.MessageServiceAsync; |
@@ -994,6 +995,141 @@ void streamingAttachesRateLimitHeadersToResponse() { |
994 | 995 | assertThat(rateLimit.getTokensRemaining()).isEqualTo(49000L); |
995 | 996 | } |
996 | 997 |
|
| 998 | + @Test |
| 999 | + void citationDocumentsAreSentOnlyInFirstUserMessage() { |
| 1000 | + Message mockResponse = createMockMessage("Answer", StopReason.END_TURN); |
| 1001 | + given(this.messageService.create(any(MessageCreateParams.class))).willReturn(mockResponse); |
| 1002 | + |
| 1003 | + AnthropicCitationDocument document = AnthropicCitationDocument.builder() |
| 1004 | + .plainText("Reference material") |
| 1005 | + .title("Reference") |
| 1006 | + .citationsEnabled(true) |
| 1007 | + .build(); |
| 1008 | + AnthropicChatOptions options = AnthropicChatOptions.builder().citationDocuments(document).build(); |
| 1009 | + |
| 1010 | + UserMessage user1 = new UserMessage("First question"); |
| 1011 | + AssistantMessage assistant1 = new AssistantMessage("First answer"); |
| 1012 | + UserMessage user2 = new UserMessage("Second question"); |
| 1013 | + |
| 1014 | + this.chatModel.call(new Prompt(List.of(user1, assistant1, user2), options)); |
| 1015 | + |
| 1016 | + ArgumentCaptor<MessageCreateParams> captor = ArgumentCaptor.forClass(MessageCreateParams.class); |
| 1017 | + verify(this.messageService).create(captor.capture()); |
| 1018 | + |
| 1019 | + List<MessageParam> messages = captor.getValue().messages(); |
| 1020 | + assertThat(messages).hasSize(3); |
| 1021 | + |
| 1022 | + List<ContentBlockParam> firstUserBlocks = messages.get(0).content().blockParams().orElseThrow(); |
| 1023 | + assertThat(firstUserBlocks).hasSize(2); |
| 1024 | + assertThat(firstUserBlocks.get(0).isDocument()).isTrue(); |
| 1025 | + assertThat(firstUserBlocks.get(1).asText().text()).isEqualTo("First question"); |
| 1026 | + |
| 1027 | + assertThat(messages.get(2).content().string()).contains("Second question"); |
| 1028 | + assertThat(messages.get(2).content().blockParams()).isEmpty(); |
| 1029 | + |
| 1030 | + long documentBlocks = messages.stream() |
| 1031 | + .flatMap(message -> message.content().blockParams().stream().flatMap(List::stream)) |
| 1032 | + .filter(ContentBlockParam::isDocument) |
| 1033 | + .count(); |
| 1034 | + assertThat(documentBlocks).isEqualTo(1); |
| 1035 | + } |
| 1036 | + |
| 1037 | + @Test |
| 1038 | + void citationDocumentCacheBreakpointPerStrategy() { |
| 1039 | + assertThat(lastDocumentHasCacheControl(AnthropicCacheStrategy.NONE)).isFalse(); |
| 1040 | + assertThat(lastDocumentHasCacheControl(AnthropicCacheStrategy.TOOLS_ONLY)).isFalse(); |
| 1041 | + assertThat(lastDocumentHasCacheControl(AnthropicCacheStrategy.SYSTEM_ONLY)).isTrue(); |
| 1042 | + assertThat(lastDocumentHasCacheControl(AnthropicCacheStrategy.SYSTEM_AND_TOOLS)).isTrue(); |
| 1043 | + assertThat(lastDocumentHasCacheControl(AnthropicCacheStrategy.CONVERSATION_HISTORY)).isTrue(); |
| 1044 | + } |
| 1045 | + |
| 1046 | + @Test |
| 1047 | + void citationDocumentCacheBreakpointGoesOnLastDocumentOnly() { |
| 1048 | + AnthropicCitationDocument first = AnthropicCitationDocument.builder().plainText("First").build(); |
| 1049 | + AnthropicCitationDocument second = AnthropicCitationDocument.builder().plainText("Second").build(); |
| 1050 | + AnthropicChatOptions options = AnthropicChatOptions.builder() |
| 1051 | + .citationDocuments(first, second) |
| 1052 | + .cacheOptions(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.SYSTEM_ONLY).build()) |
| 1053 | + .build(); |
| 1054 | + |
| 1055 | + MessageCreateParams request = this.chatModel.createRequest(new Prompt("Question", options), false); |
| 1056 | + |
| 1057 | + List<ContentBlockParam> blocks = request.messages().get(0).content().blockParams().orElseThrow(); |
| 1058 | + assertThat(blocks).hasSize(3); |
| 1059 | + assertThat(blocks.get(0).asDocument().cacheControl()).isEmpty(); |
| 1060 | + assertThat(blocks.get(1).asDocument().cacheControl()).isPresent(); |
| 1061 | + // SYSTEM_ONLY does not cache the user text |
| 1062 | + assertThat(blocks.get(2).asText().cacheControl()).isEmpty(); |
| 1063 | + } |
| 1064 | + |
| 1065 | + @Test |
| 1066 | + void citationDocumentAndUserTextBothCachedUnderConversationHistory() { |
| 1067 | + // Batch Q&A over the same document: each request is a fresh single-turn |
| 1068 | + // prompt, so the document breakpoint is what produces cache hits across |
| 1069 | + // requests while the user text breakpoint changes every time. |
| 1070 | + AnthropicCitationDocument document = AnthropicCitationDocument.builder().plainText("Reference").build(); |
| 1071 | + AnthropicChatOptions options = AnthropicChatOptions.builder() |
| 1072 | + .citationDocuments(document) |
| 1073 | + .cacheOptions(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.CONVERSATION_HISTORY).build()) |
| 1074 | + .build(); |
| 1075 | + |
| 1076 | + MessageCreateParams request = this.chatModel.createRequest(new Prompt("Question", options), false); |
| 1077 | + |
| 1078 | + List<ContentBlockParam> blocks = request.messages().get(0).content().blockParams().orElseThrow(); |
| 1079 | + assertThat(blocks).hasSize(2); |
| 1080 | + assertThat(blocks.get(0).asDocument().cacheControl()).isPresent(); |
| 1081 | + assertThat(blocks.get(1).asText().cacheControl()).isPresent(); |
| 1082 | + } |
| 1083 | + |
| 1084 | + @Test |
| 1085 | + void citationDocumentBreakpointTakesPrecedenceOverToolDefinitions() { |
| 1086 | + // System, document, last user text, and last tool result each take a |
| 1087 | + // breakpoint, which is all four. Tool definitions are resolved last and get |
| 1088 | + // none. That is harmless: tools precede the system prompt in the request and |
| 1089 | + // are inside the prefix cached by the system breakpoint. |
| 1090 | + AnthropicCitationDocument document = AnthropicCitationDocument.builder().plainText("Reference").build(); |
| 1091 | + AnthropicCacheOptions cacheOptions = AnthropicCacheOptions.builder() |
| 1092 | + .strategy(AnthropicCacheStrategy.CONVERSATION_HISTORY) |
| 1093 | + .cacheToolResults(true) |
| 1094 | + .build(); |
| 1095 | + AnthropicChatOptions options = AnthropicChatOptions.builder() |
| 1096 | + .citationDocuments(document) |
| 1097 | + .cacheOptions(cacheOptions) |
| 1098 | + .toolCallbacks(List.of(new TestToolCallback("getWeather"))) |
| 1099 | + .build(); |
| 1100 | + |
| 1101 | + List<org.springframework.ai.chat.messages.Message> messages = new java.util.ArrayList<>(); |
| 1102 | + messages.add(new SystemMessage("You are a helpful assistant.")); |
| 1103 | + messages.addAll(toolCallingConversation()); |
| 1104 | + |
| 1105 | + MessageCreateParams request = this.chatModel.createRequest(new Prompt(messages, options), false); |
| 1106 | + |
| 1107 | + assertThat(request.system().orElseThrow().asTextBlockParams().get(0).cacheControl()).isPresent(); |
| 1108 | + |
| 1109 | + List<ContentBlockParam> firstUserBlocks = request.messages().get(0).content().blockParams().orElseThrow(); |
| 1110 | + assertThat(firstUserBlocks.get(0).asDocument().cacheControl()).isPresent(); |
| 1111 | + assertThat(firstUserBlocks.get(1).asText().cacheControl()).isPresent(); |
| 1112 | + |
| 1113 | + assertThat(lastToolResultBlock(request).cacheControl()).isPresent(); |
| 1114 | + |
| 1115 | + List<ToolUnion> tools = request.tools().orElseThrow(); |
| 1116 | + assertThat(tools).hasSize(1); |
| 1117 | + assertThat(tools.get(0).asTool().cacheControl()).isEmpty(); |
| 1118 | + } |
| 1119 | + |
| 1120 | + private boolean lastDocumentHasCacheControl(AnthropicCacheStrategy strategy) { |
| 1121 | + AnthropicCitationDocument document = AnthropicCitationDocument.builder().plainText("Reference").build(); |
| 1122 | + AnthropicChatOptions options = AnthropicChatOptions.builder() |
| 1123 | + .citationDocuments(document) |
| 1124 | + .cacheOptions(AnthropicCacheOptions.builder().strategy(strategy).build()) |
| 1125 | + .build(); |
| 1126 | + |
| 1127 | + MessageCreateParams request = this.chatModel.createRequest(new Prompt("Question", options), false); |
| 1128 | + |
| 1129 | + List<ContentBlockParam> blocks = request.messages().get(0).content().blockParams().orElseThrow(); |
| 1130 | + return blocks.get(0).asDocument().cacheControl().isPresent(); |
| 1131 | + } |
| 1132 | + |
997 | 1133 | static class TestToolCallback implements ToolCallback { |
998 | 1134 |
|
999 | 1135 | private final ToolDefinition toolDefinition; |
|
0 commit comments