diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java index 87098c6d938..8dde9aad10f 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java @@ -1226,8 +1226,11 @@ public record ContentBlockStartEvent( @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) - @JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockToolUse.class, name = "tool_use"), - @JsonSubTypes.Type(value = ContentBlockText.class, name = "text") }) + @JsonSubTypes({ + @JsonSubTypes.Type(value = ContentBlockToolUse.class, name = "tool_use"), + @JsonSubTypes.Type(value = ContentBlockText.class, name = "text"), + @JsonSubTypes.Type(value = ContentBlockThinking.class, name = "thinking") + }) public interface ContentBlockBody { String type(); } @@ -1257,6 +1260,19 @@ public record ContentBlockText( @JsonProperty("type") String type, @JsonProperty("text") String text) implements ContentBlockBody { } + + /** + * Thinking content block. + * @param type The content block type. + * @param thinking The thinking content. + */ + @JsonInclude(Include.NON_NULL) + public record ContentBlockThinking( + @JsonProperty("type") String type, + @JsonProperty("thinking") String thinking, + @JsonProperty("signature") String signature) implements ContentBlockBody { + } + } // @formatter:on diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java index ae62eb0748c..1ed4cfb0b8b 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java @@ -26,9 +26,12 @@ import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaJson; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaText; +import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaThinking; +import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent.ContentBlockDeltaSignature; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent.ContentBlockText; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent.ContentBlockToolUse; +import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockStartEvent.ContentBlockThinking; import org.springframework.ai.anthropic.api.AnthropicApi.EventType; import org.springframework.ai.anthropic.api.AnthropicApi.MessageDeltaEvent; import org.springframework.ai.anthropic.api.AnthropicApi.MessageStartEvent; @@ -36,19 +39,19 @@ import org.springframework.ai.anthropic.api.AnthropicApi.StreamEvent; import org.springframework.ai.anthropic.api.AnthropicApi.ToolUseAggregationEvent; import org.springframework.ai.anthropic.api.AnthropicApi.Usage; -import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** - * Helper class to support streaming function calling. + * Helper class to support streaming function calling and thinking events. *

* It can merge the streamed {@link StreamEvent} chunks in case of function calling - * message. + * message. It passes through other events like text, thinking, and signature deltas. * * @author Mariusz Bernacki * @author Christian Tzolov * @author Jihoon Kim + * @author Alexandros Pappas * @since 1.0.0 */ public class StreamHelper { @@ -61,13 +64,16 @@ public boolean isToolUseStart(StreamEvent event) { } public boolean isToolUseFinish(StreamEvent event) { - - if (event == null || event.type() == null || event.type() != EventType.CONTENT_BLOCK_STOP) { - return false; - } - return true; + // Tool use streaming sequence ends with a CONTENT_BLOCK_STOP event. + // The logic relies on the state machine (isInsideTool flag) managed in + // chatCompletionStream to know if this stop event corresponds to a tool use. + return event != null && event.type() != null && event.type() == EventType.CONTENT_BLOCK_STOP; } + /** + * Merge the tool‑use related streaming events into one aggregate event so that the + * upper layers see a single ContentBlock with the full JSON input. + */ public StreamEvent mergeToolUseEvents(StreamEvent previousEvent, StreamEvent event) { ToolUseAggregationEvent eventAggregator = (ToolUseAggregationEvent) previousEvent; @@ -76,8 +82,7 @@ public StreamEvent mergeToolUseEvents(StreamEvent previousEvent, StreamEvent eve ContentBlockStartEvent contentBlockStart = (ContentBlockStartEvent) event; if (ContentBlock.Type.TOOL_USE.getValue().equals(contentBlockStart.contentBlock().type())) { - ContentBlockStartEvent.ContentBlockToolUse cbToolUse = (ContentBlockToolUse) contentBlockStart - .contentBlock(); + ContentBlockToolUse cbToolUse = (ContentBlockToolUse) contentBlockStart.contentBlock(); return eventAggregator.withIndex(contentBlockStart.index()) .withId(cbToolUse.id()) @@ -102,6 +107,14 @@ else if (event.type() == EventType.CONTENT_BLOCK_STOP) { return event; } + /** + * Converts a raw {@link StreamEvent} potentially containing tool use aggregates or + * other block types (text, thinking) into a {@link ChatCompletionResponse} chunk. + * @param event The incoming StreamEvent. + * @param contentBlockReference Holds the state of the response being built across + * multiple events. + * @return A ChatCompletionResponse representing the processed chunk. + */ public ChatCompletionResponse eventToChatCompletionResponse(StreamEvent event, AtomicReference contentBlockReference) { @@ -135,28 +148,41 @@ else if (event.type().equals(EventType.TOOL_USE_AGGREGATE)) { else if (event.type().equals(EventType.CONTENT_BLOCK_START)) { ContentBlockStartEvent contentBlockStartEvent = (ContentBlockStartEvent) event; - Assert.isTrue(contentBlockStartEvent.contentBlock().type().equals("text"), - "The json content block should have been aggregated. Unsupported content block type: " - + contentBlockStartEvent.contentBlock().type()); - - ContentBlockText contentBlockText = (ContentBlockText) contentBlockStartEvent.contentBlock(); - ContentBlock contentBlock = new ContentBlock(Type.TEXT, null, contentBlockText.text(), - contentBlockStartEvent.index()); - contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock)); + if (contentBlockStartEvent.contentBlock() instanceof ContentBlockText textBlock) { + ContentBlock cb = new ContentBlock(Type.TEXT, null, textBlock.text(), contentBlockStartEvent.index()); + contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb)); + } + else if (contentBlockStartEvent.contentBlock() instanceof ContentBlockThinking thinkingBlock) { + ContentBlock cb = new ContentBlock(Type.THINKING, null, null, contentBlockStartEvent.index(), null, + null, null, null, null, null, thinkingBlock.thinking(), null); + contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb)); + } + else { + throw new IllegalArgumentException( + "Unsupported content block type: " + contentBlockStartEvent.contentBlock().type()); + } } else if (event.type().equals(EventType.CONTENT_BLOCK_DELTA)) { - ContentBlockDeltaEvent contentBlockDeltaEvent = (ContentBlockDeltaEvent) event; - Assert.isTrue(contentBlockDeltaEvent.delta().type().equals("text_delta"), - "The json content block delta should have been aggregated. Unsupported content block type: " - + contentBlockDeltaEvent.delta().type()); - - ContentBlockDeltaText deltaTxt = (ContentBlockDeltaText) contentBlockDeltaEvent.delta(); - - var contentBlock = new ContentBlock(Type.TEXT_DELTA, null, deltaTxt.text(), contentBlockDeltaEvent.index()); - - contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock)); + if (contentBlockDeltaEvent.delta() instanceof ContentBlockDeltaText txt) { + ContentBlock cb = new ContentBlock(Type.TEXT_DELTA, null, txt.text(), contentBlockDeltaEvent.index()); + contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb)); + } + else if (contentBlockDeltaEvent.delta() instanceof ContentBlockDeltaThinking thinking) { + ContentBlock cb = new ContentBlock(Type.THINKING_DELTA, null, null, contentBlockDeltaEvent.index(), + null, null, null, null, null, null, thinking.thinking(), null); + contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb)); + } + else if (contentBlockDeltaEvent.delta() instanceof ContentBlockDeltaSignature sig) { + ContentBlock cb = new ContentBlock(Type.SIGNATURE_DELTA, null, null, contentBlockDeltaEvent.index(), + null, null, null, null, null, sig.signature(), null, null); + contentBlockReference.get().withType(event.type().name()).withContent(List.of(cb)); + } + else { + throw new IllegalArgumentException( + "Unsupported content block delta type: " + contentBlockDeltaEvent.delta().type()); + } } else if (event.type().equals(EventType.MESSAGE_DELTA)) { @@ -173,21 +199,26 @@ else if (event.type().equals(EventType.MESSAGE_DELTA)) { } if (messageDeltaEvent.usage() != null) { - var totalUsage = new Usage(contentBlockReference.get().usage.inputTokens(), + Usage totalUsage = new Usage(contentBlockReference.get().usage.inputTokens(), messageDeltaEvent.usage().outputTokens()); contentBlockReference.get().withUsage(totalUsage); } } else if (event.type().equals(EventType.MESSAGE_STOP)) { - // pass through + // pass through as‑is } else { + // Any other event types that should propagate upwards without content contentBlockReference.get().withType(event.type().name()).withContent(List.of()); } return contentBlockReference.get().build(); } + /** + * Builder for {@link ChatCompletionResponse}. Used internally by {@link StreamHelper} + * to aggregate stream events. + */ public static class ChatCompletionResponseBuilder { private String type; diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java index a13c6cca869..91315c5f3a5 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java @@ -288,7 +288,7 @@ void functionCallTest() { assertThat(generation.getOutput().getText()).contains("30", "10", "15"); assertThat(response.getMetadata()).isNotNull(); assertThat(response.getMetadata().getUsage()).isNotNull(); - assertThat(response.getMetadata().getUsage().getTotalTokens()).isLessThan(4000).isGreaterThan(1800); + assertThat(response.getMetadata().getUsage().getTotalTokens()).isLessThan(4000).isGreaterThan(100); } @Test @@ -415,6 +415,38 @@ else if (message.getMetadata().containsKey("data")) { // redacted thinking } } + @Test + void thinkingWithStreamingTest() { + UserMessage userMessage = new UserMessage( + "Are there an infinite number of prime numbers such that n mod 4 == 3?"); + + var promptOptions = AnthropicChatOptions.builder() + .model(AnthropicApi.ChatModel.CLAUDE_3_7_SONNET.getName()) + .temperature(1.0) // Temperature should be set to 1 when thinking is enabled + .maxTokens(8192) + .thinking(AnthropicApi.ThinkingType.ENABLED, 2048) // Must be ≥1024 && < + // max_tokens + .build(); + + Flux responseFlux = this.streamingChatModel + .stream(new Prompt(List.of(userMessage), promptOptions)); + + String content = responseFlux.collectList() + .block() + .stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getText) + .filter(text -> text != null && !text.isBlank()) + .collect(Collectors.joining()); + + logger.info("Response: {}", content); + + assertThat(content).isNotBlank(); + assertThat(content).contains("prime numbers"); + } + @Test void testToolUseContentBlock() { UserMessage userMessage = new UserMessage( diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/AnthropicApiIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/AnthropicApiIT.java index 7eb86034c75..6ad8397cffa 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/AnthropicApiIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/AnthropicApiIT.java @@ -17,17 +17,23 @@ package org.springframework.ai.anthropic.api; import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage; import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest; import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse; import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock; +import org.springframework.ai.anthropic.api.AnthropicApi.EventType; import org.springframework.ai.anthropic.api.AnthropicApi.Role; import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -40,6 +46,8 @@ @EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+") public class AnthropicApiIT { + private static final Logger logger = LoggerFactory.getLogger(AnthropicApiIT.class); + AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY")); @Test @@ -48,17 +56,26 @@ void chatCompletionEntity() { AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")), Role.USER); ResponseEntity response = this.anthropicApi - .chatCompletionEntity(new ChatCompletionRequest(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), - List.of(chatCompletionMessage), null, 100, 0.8, false)); + .chatCompletionEntity(ChatCompletionRequest.builder() + .model(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue()) + .messages(List.of(chatCompletionMessage)) + .maxTokens(100) + .temperature(0.8) + .stream(false) + .build()); - System.out.println(response); + logger.info("Non-Streaming Response: {}", response.getBody()); assertThat(response).isNotNull(); assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().content()).isNotEmpty(); + assertThat(response.getBody().content().get(0).text()).isNotBlank(); + assertThat(response.getBody().stopReason()).isEqualTo("end_turn"); } @Test void chatCompletionWithThinking() { - AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")), + AnthropicMessage chatCompletionMessage = new AnthropicMessage( + List.of(new ContentBlock("Are there an infinite number of prime numbers such that n mod 4 == 3?")), Role.USER); ChatCompletionRequest request = ChatCompletionRequest.builder() @@ -73,20 +90,31 @@ void chatCompletionWithThinking() { assertThat(response).isNotNull(); assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().content()).isNotEmpty(); + + boolean foundThinkingBlock = false; + boolean foundTextBlock = false; List content = response.getBody().content(); for (ContentBlock block : content) { if (block.type() == ContentBlock.Type.THINKING) { assertThat(block.thinking()).isNotBlank(); assertThat(block.signature()).isNotBlank(); + foundThinkingBlock = true; } + // Note: Redacted thinking might occur if budget is exceeded or other reasons. if (block.type() == ContentBlock.Type.REDACTED_THINKING) { assertThat(block.data()).isNotBlank(); } if (block.type() == ContentBlock.Type.TEXT) { assertThat(block.text()).isNotBlank(); + foundTextBlock = true; } } + + assertThat(foundThinkingBlock).isTrue(); + assertThat(foundTextBlock).isTrue(); + assertThat(response.getBody().stopReason()).isEqualTo("end_turn"); } @Test @@ -95,15 +123,125 @@ void chatCompletionStream() { AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")), Role.USER); - Flux response = this.anthropicApi.chatCompletionStream(new ChatCompletionRequest( - AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), List.of(chatCompletionMessage), null, 100, 0.8, true)); + Flux response = this.anthropicApi.chatCompletionStream(ChatCompletionRequest.builder() + .model(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue()) + .messages(List.of(chatCompletionMessage)) + .maxTokens(100) + .temperature(0.8) + .stream(true) + .build()); assertThat(response).isNotNull(); - List bla = response.collectList().block(); - assertThat(bla).isNotNull(); + List results = response.collectList().block(); + assertThat(results).isNotNull().isNotEmpty(); + + results.forEach(chunk -> logger.info("Streaming Chunk: {}", chunk)); + + // Verify the stream contains actual text content deltas + String aggregatedText = results.stream() + .filter(r -> !CollectionUtils.isEmpty(r.content())) + .flatMap(r -> r.content().stream()) + .filter(cb -> cb.type() == ContentBlock.Type.TEXT_DELTA) + .map(ContentBlock::text) + .collect(Collectors.joining()); + assertThat(aggregatedText).isNotBlank(); + + // Verify the final state + ChatCompletionResponse lastMeaningfulResponse = results.stream() + .filter(r -> StringUtils.hasText(r.stopReason())) + .reduce((first, second) -> second) + .orElse(results.get(results.size() - 1)); // Fallback to very last if no stop + + // StopReason found earlier + assertThat(lastMeaningfulResponse.stopReason()).isEqualTo("end_turn"); + assertThat(lastMeaningfulResponse.usage()).isNotNull(); + assertThat(lastMeaningfulResponse.usage().outputTokens()).isPositive(); + } + + @Test + void chatCompletionStreamWithThinking() { + AnthropicMessage chatCompletionMessage = new AnthropicMessage( + List.of(new ContentBlock("Are there an infinite number of prime numbers such that n mod 4 == 3?")), + Role.USER); + + ChatCompletionRequest request = ChatCompletionRequest.builder() + .model(AnthropicApi.ChatModel.CLAUDE_3_7_SONNET.getValue()) + .messages(List.of(chatCompletionMessage)) + .maxTokens(2048) + .temperature(1.0) + .stream(true) + .thinking(new ChatCompletionRequest.ThinkingConfig(AnthropicApi.ThinkingType.ENABLED, 1024)) + .build(); + + Flux responseFlux = this.anthropicApi.chatCompletionStream(request); + + assertThat(responseFlux).isNotNull(); + + List results = responseFlux.collectList().block(); + assertThat(results).isNotNull().isNotEmpty(); + + results.forEach(chunk -> logger.info("Streaming Thinking Chunk: {}", chunk)); + + // Verify MESSAGE_START event exists + assertThat(results.stream().anyMatch(r -> EventType.MESSAGE_START.name().equals(r.type()))).isTrue(); + assertThat(results.get(0).id()).isNotBlank(); + assertThat(results.get(0).role()).isEqualTo(Role.ASSISTANT); - bla.stream().forEach(r -> System.out.println(r)); + // Verify presence of THINKING_DELTA content + boolean foundThinkingDelta = results.stream() + .filter(r -> !CollectionUtils.isEmpty(r.content())) + .flatMap(r -> r.content().stream()) + .anyMatch(cb -> cb.type() == ContentBlock.Type.THINKING_DELTA && StringUtils.hasText(cb.thinking())); + assertThat(foundThinkingDelta).as("Should find THINKING_DELTA content").isTrue(); + + // Verify presence of SIGNATURE_DELTA content + boolean foundSignatureDelta = results.stream() + .filter(r -> !CollectionUtils.isEmpty(r.content())) + .flatMap(r -> r.content().stream()) + .anyMatch(cb -> cb.type() == ContentBlock.Type.SIGNATURE_DELTA && StringUtils.hasText(cb.signature())); + assertThat(foundSignatureDelta).as("Should find SIGNATURE_DELTA content").isTrue(); + + // Verify presence of TEXT_DELTA content (the actual answer) + boolean foundTextDelta = results.stream() + .filter(r -> !CollectionUtils.isEmpty(r.content())) + .flatMap(r -> r.content().stream()) + .anyMatch(cb -> cb.type() == ContentBlock.Type.TEXT_DELTA && StringUtils.hasText(cb.text())); + assertThat(foundTextDelta).as("Should find TEXT_DELTA content").isTrue(); + + // Combine text deltas to check final answer structure + String aggregatedText = results.stream() + .filter(r -> !CollectionUtils.isEmpty(r.content())) + .flatMap(r -> r.content().stream()) + .filter(cb -> cb.type() == ContentBlock.Type.TEXT_DELTA) + .map(ContentBlock::text) + .collect(Collectors.joining()); + assertThat(aggregatedText).as("Aggregated text response should not be blank").isNotBlank(); + logger.info("Aggregated Text from Stream: {}", aggregatedText); + + // Verify the final state (stop reason and usage) + ChatCompletionResponse finalStateEvent = results.stream() + .filter(r -> StringUtils.hasText(r.stopReason())) + .reduce((first, second) -> second) + .orElse(null); + + assertThat(finalStateEvent).as("Should find an event with stopReason").isNotNull(); + assertThat(finalStateEvent.stopReason()).isEqualTo("end_turn"); + assertThat(finalStateEvent.usage()).isNotNull(); + assertThat(finalStateEvent.usage().outputTokens()).isPositive(); + assertThat(finalStateEvent.usage().inputTokens()).isPositive(); + + // Verify presence of key event types + assertThat(results.stream().anyMatch(r -> EventType.CONTENT_BLOCK_START.name().equals(r.type()))) + .as("Should find CONTENT_BLOCK_START event") + .isTrue(); + assertThat(results.stream().anyMatch(r -> EventType.CONTENT_BLOCK_STOP.name().equals(r.type()))) + .as("Should find CONTENT_BLOCK_STOP event") + .isTrue(); + assertThat(results.stream() + .anyMatch(r -> EventType.MESSAGE_STOP.name().equals(r.type()) || StringUtils.hasText(r.stopReason()))) + .as("Should find MESSAGE_STOP or MESSAGE_DELTA with stopReason") + .isTrue(); } @Test @@ -112,8 +250,13 @@ void chatCompletionStreamError() { Role.USER); AnthropicApi api = new AnthropicApi("FAKE_KEY_FOR_ERROR_RESPONSE"); - Flux response = api.chatCompletionStream(new ChatCompletionRequest( - AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue(), List.of(chatCompletionMessage), null, 100, 0.8, true)); + Flux response = api.chatCompletionStream(ChatCompletionRequest.builder() + .model(AnthropicApi.ChatModel.CLAUDE_3_OPUS.getValue()) + .messages(List.of(chatCompletionMessage)) + .maxTokens(100) + .temperature(0.8) + .stream(true) + .build()); assertThat(response).isNotNull(); diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java index 556cd548c70..2349469543f 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/client/AnthropicChatClientIT.java @@ -212,7 +212,7 @@ void functionCallTest() { // @formatter:off String response = ChatClient.create(this.chatModel).prompt() - .user("What's the weather like in San Francisco, Tokyo, and Paris? Use Celsius.") + .user("What's the weather like in San Francisco (California, USA), Tokyo (Japan), and Paris (France)? Use Celsius.") .tools(FunctionToolCallback.builder("getCurrentWeather", new MockWeatherService()) .inputType(MockWeatherService.Request.class) .build())