functions) {
+ this.functions = functions;
+ }
+
+ @Override
+ @JsonIgnore
+ public Float getFrequencyPenalty() {
+ return null;
+ }
+
+ @Override
+ @JsonIgnore
+ public Float getPresencePenalty() {
+ return null;
+ }
+
+ @Override
+ public VertexAiAnthropicChatOptions copy() {
+ return fromOptions(this);
+ }
+
+ public static VertexAiAnthropicChatOptions fromOptions(VertexAiAnthropicChatOptions fromOptions) {
+ return builder().withStopSequences(fromOptions.getStopSequences())
+ .withTemperature(fromOptions.getTemperature())
+ .withTopP(fromOptions.getTopP())
+ .withTopK(fromOptions.getTopK())
+ .withMaxTokens(fromOptions.getMaxTokens())
+ .withAnthropicVersion(fromOptions.getAnthropicVersion())
+ .withModel(fromOptions.getModel())
+ .withFunctionCallbacks(fromOptions.getFunctionCallbacks())
+ .withFunctions(fromOptions.getFunctions())
+ .build();
+ }
+
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/aot/VertexAiAnthropicRuntimeHints.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/aot/VertexAiAnthropicRuntimeHints.java
new file mode 100644
index 00000000000..d97207e4b43
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/aot/VertexAiAnthropicRuntimeHints.java
@@ -0,0 +1,27 @@
+package org.springframework.ai.vertexai.anthropic.aot;
+
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+
+import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
+
+/**
+ * The VertexAiAnthropicRuntimeHints class is responsible for registering runtime hints
+ * for Vertex AI Anthropic API classes.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public class VertexAiAnthropicRuntimeHints implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+ var mcs = MemberCategory.values();
+ for (var tr : findJsonAnnotatedClassesInPackage(VertexAiAnthropicChatModel.class)) {
+ hints.reflection().registerType(tr, mcs);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/StreamHelper.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/StreamHelper.java
new file mode 100644
index 00000000000..83228894207
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/StreamHelper.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.api;
+
+import org.springframework.ai.vertexai.anthropic.model.ApiUsage;
+import org.springframework.ai.vertexai.anthropic.model.ChatCompletionResponse;
+import org.springframework.ai.vertexai.anthropic.model.ContentBlock;
+import org.springframework.ai.vertexai.anthropic.model.Role;
+import org.springframework.ai.vertexai.anthropic.model.stream.*;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class to support streaming function calling.
+ *
+ * It can merge the streamed {@link StreamEvent} chunks in case of function calling
+ * message.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public class StreamHelper {
+
+ /**
+ * Checks if the given event is a tool use start event.
+ * @param event the StreamEvent to check
+ * @return true if the event is a tool use start event, false otherwise
+ */
+ public boolean isToolUseStart(StreamEvent event) {
+ if (event == null || event.type() == null || event.type() != EventType.CONTENT_BLOCK_START) {
+ return false;
+ }
+ return ContentBlock.Type.TOOL_USE.getValue().equals(((ContentBlockStartEvent) event).contentBlock().type());
+ }
+
+ /**
+ * Checks if the given event is a tool use start event.
+ * @param event the StreamEvent to check
+ * @return true if the event is a tool use start event, false otherwise
+ */
+ public boolean isToolUseFinish(StreamEvent event) {
+
+ if (event == null || event.type() == null || event.type() != EventType.CONTENT_BLOCK_STOP) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Merges tool use events into a single aggregated event.
+ * @param previousEvent the previous StreamEvent, expected to be a
+ * ToolUseAggregationEvent
+ * @param event the current StreamEvent to merge
+ * @return the merged StreamEvent, or the original event if no merging is performed
+ */
+ public StreamEvent mergeToolUseEvents(StreamEvent previousEvent, StreamEvent event) {
+
+ ToolUseAggregationEvent eventAggregator = (ToolUseAggregationEvent) previousEvent;
+
+ if (event.type() == EventType.CONTENT_BLOCK_START) {
+ ContentBlockStartEvent contentBlockStart = (ContentBlockStartEvent) event;
+
+ if (ContentBlock.Type.TOOL_USE.getValue().equals(contentBlockStart.contentBlock().type())) {
+ ContentBlockStartEvent.ContentBlockToolUse cbToolUse = (ContentBlockStartEvent.ContentBlockToolUse) contentBlockStart
+ .contentBlock();
+
+ return eventAggregator.withIndex(contentBlockStart.index())
+ .withId(cbToolUse.id())
+ .withName(cbToolUse.name())
+ .appendPartialJson("");
+ }
+ }
+ else if (event.type() == EventType.CONTENT_BLOCK_DELTA) {
+ ContentBlockDeltaEvent contentBolckDelta = (ContentBlockDeltaEvent) event;
+ if (ContentBlock.Type.INPUT_JSON_DELTA.getValue().equals(contentBolckDelta.delta().type())) {
+ return eventAggregator.appendPartialJson(
+ ((ContentBlockDeltaEvent.ContentBlockDeltaJson) contentBolckDelta.delta()).partialJson());
+ }
+ }
+ else if (event.type() == EventType.CONTENT_BLOCK_STOP) {
+ if (!eventAggregator.isEmpty()) {
+ eventAggregator.squashIntoContentBlock();
+ return eventAggregator;
+ }
+ }
+
+ return event;
+ }
+
+ /**
+ * Converts a StreamEvent to a ChatCompletionResponse.
+ * @param event the StreamEvent to convert
+ * @param contentBlockReference a reference to the ChatCompletionResponseBuilder
+ * @return the constructed ChatCompletionResponse
+ */
+ public ChatCompletionResponse eventToChatCompletionResponse(StreamEvent event,
+ AtomicReference contentBlockReference) {
+
+ if (event.type().equals(EventType.MESSAGE_START)) {
+ contentBlockReference.set(new ChatCompletionResponseBuilder());
+
+ MessageStartEvent messageStartEvent = (MessageStartEvent) event;
+
+ contentBlockReference.get()
+ .withType(event.type().name())
+ .withId(messageStartEvent.message().id())
+ .withRole(messageStartEvent.message().role())
+ .withModel(messageStartEvent.message().model())
+ .withUsage(messageStartEvent.message().usage())
+ .withContent(new ArrayList<>());
+ }
+ else if (event.type().equals(EventType.TOOL_USE_AGGREGATE)) {
+ ToolUseAggregationEvent eventToolUseBuilder = (ToolUseAggregationEvent) event;
+
+ if (!CollectionUtils.isEmpty(eventToolUseBuilder.getToolContentBlocks())) {
+
+ List content = eventToolUseBuilder.getToolContentBlocks()
+ .stream()
+ .map(tooToUse -> new ContentBlock(ContentBlock.Type.TOOL_USE, tooToUse.id(), tooToUse.name(),
+ tooToUse.input()))
+ .toList();
+ contentBlockReference.get().withContent(content);
+ }
+ }
+ 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());
+
+ ContentBlockStartEvent.ContentBlockText contentBlockText = (ContentBlockStartEvent.ContentBlockText) contentBlockStartEvent
+ .contentBlock();
+ ContentBlock contentBlock = new ContentBlock(ContentBlock.Type.TEXT, null, contentBlockText.text(),
+ contentBlockStartEvent.index());
+ contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock));
+ }
+ 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());
+
+ ContentBlockDeltaEvent.ContentBlockDeltaText deltaTxt = (ContentBlockDeltaEvent.ContentBlockDeltaText) contentBlockDeltaEvent
+ .delta();
+
+ var contentBlock = new ContentBlock(ContentBlock.Type.TEXT_DELTA, null, deltaTxt.text(),
+ contentBlockDeltaEvent.index());
+
+ contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock));
+ }
+ else if (event.type().equals(EventType.MESSAGE_DELTA)) {
+
+ contentBlockReference.get().withType(event.type().name());
+
+ MessageDeltaEvent messageDeltaEvent = (MessageDeltaEvent) event;
+
+ if (StringUtils.hasText(messageDeltaEvent.delta().stopReason())) {
+ contentBlockReference.get().withStopReason(messageDeltaEvent.delta().stopReason());
+ }
+
+ if (StringUtils.hasText(messageDeltaEvent.delta().stopSequence())) {
+ contentBlockReference.get().withStopSequence(messageDeltaEvent.delta().stopSequence());
+ }
+
+ if (messageDeltaEvent.usage() != null) {
+ var totalUsage = new ApiUsage(contentBlockReference.get().usage.inputTokens(),
+ messageDeltaEvent.usage().outputTokens());
+ contentBlockReference.get().withUsage(totalUsage);
+ }
+ }
+ else if (event.type().equals(EventType.MESSAGE_STOP)) {
+ }
+ else {
+ contentBlockReference.get().withType(event.type().name()).withContent(List.of());
+ }
+
+ return contentBlockReference.get().build();
+ }
+
+ /**
+ * Builder class for constructing ChatCompletionResponse objects.
+ */
+ public static class ChatCompletionResponseBuilder {
+
+ private String type;
+
+ private String id;
+
+ private Role role;
+
+ private List content;
+
+ private String model;
+
+ private String stopReason;
+
+ private String stopSequence;
+
+ private ApiUsage usage;
+
+ public ChatCompletionResponseBuilder() {
+ }
+
+ public ChatCompletionResponseBuilder withType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withRole(Role role) {
+ this.role = role;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withContent(List content) {
+ this.content = content;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withModel(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withStopReason(String stopReason) {
+ this.stopReason = stopReason;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withStopSequence(String stopSequence) {
+ this.stopSequence = stopSequence;
+ return this;
+ }
+
+ public ChatCompletionResponseBuilder withUsage(ApiUsage usage) {
+ this.usage = usage;
+ return this;
+ }
+
+ public ChatCompletionResponse build() {
+ return new ChatCompletionResponse(this.id, this.type, this.role, this.content, this.model, this.stopReason,
+ this.stopSequence, this.usage);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/VertexAiAnthropicApi.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/VertexAiAnthropicApi.java
new file mode 100644
index 00000000000..ed8913e48ea
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/api/VertexAiAnthropicApi.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.api;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.ai.vertexai.anthropic.model.ChatCompletionRequest;
+import org.springframework.ai.vertexai.anthropic.model.ChatCompletionResponse;
+import org.springframework.ai.vertexai.anthropic.model.stream.EventType;
+import org.springframework.ai.vertexai.anthropic.model.stream.StreamEvent;
+import org.springframework.ai.vertexai.anthropic.model.stream.ToolUseAggregationEvent;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Vertex AI Anthropic API client.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public class VertexAiAnthropicApi {
+
+ private final String projectId;
+
+ private final String location;
+
+ private final GoogleCredentials credentials;
+
+ private final RestClient restClient;
+
+ private final WebClient webClient;
+
+ private final StreamHelper streamHelper = new StreamHelper();
+
+ private static final String VERTEXAI_BASE_URL = "https://%s-aiplatform.googleapis.com";
+
+ private static final String VERTEXAI_ANTHROPIC_ENDPOINT = "/v1/projects/%s/locations/%s/publishers/anthropic/models/%s";
+
+ private static final Predicate SSE_DONE_PREDICATE = "[DONE]"::equals;
+
+ public VertexAiAnthropicApi(String projectId, String location, GoogleCredentials credentials, RestClient restClient,
+ WebClient webClient) {
+ this.projectId = projectId;
+ this.location = location;
+ this.credentials = credentials;
+
+ Consumer jsonContentHeaders = headers -> {
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ };
+
+ if (restClient != null) {
+ this.restClient = restClient;
+ }
+ else {
+ this.restClient = RestClient.builder()
+ .baseUrl(VERTEXAI_BASE_URL.formatted(location))
+ .defaultHeaders(jsonContentHeaders)
+ .defaultStatusHandler(RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER)
+ .build();
+ }
+
+ if (webClient != null) {
+ this.webClient = webClient;
+ }
+ else {
+ this.webClient = WebClient.builder()
+ .baseUrl(VERTEXAI_BASE_URL.formatted(location))
+ .defaultHeaders(jsonContentHeaders)
+ .defaultStatusHandler(HttpStatusCode::isError,
+ resp -> Mono.just(new RuntimeException("Response exception, Status: [" + resp.statusCode()
+ + "], Body:[" + resp.bodyToMono(java.lang.String.class) + "]")))
+ .build();
+ }
+ }
+
+ /**
+ * Builder for {@link VertexAiAnthropicApi}.
+ */
+ public static class Builder {
+
+ private String projectId;
+
+ private String location;
+
+ private GoogleCredentials credentials;
+
+ private RestClient restClient;
+
+ private WebClient webClient;
+
+ public Builder projectId(String projectId) {
+ this.projectId = projectId;
+ return this;
+ }
+
+ public Builder location(String location) {
+ this.location = location;
+ return this;
+ }
+
+ public Builder credentials(GoogleCredentials credentials) {
+ this.credentials = credentials;
+ return this;
+ }
+
+ public Builder restClient(RestClient restClient) {
+ this.restClient = restClient;
+ return this;
+ }
+
+ public Builder webClient(WebClient webClient) {
+ this.webClient = webClient;
+ return this;
+ }
+
+ public VertexAiAnthropicApi build() {
+ return new VertexAiAnthropicApi(projectId, location, credentials, restClient, webClient);
+ }
+
+ }
+
+ public ResponseEntity chatCompletion(ChatCompletionRequest chatRequest, String model) {
+
+ Assert.notNull(chatRequest, "The request body can not be null.");
+ Assert.isTrue(!chatRequest.stream(), "Request must set the steam property to false.");
+
+ return this.restClient.post()
+ .uri(VERTEXAI_ANTHROPIC_ENDPOINT.formatted(projectId, location, model) + ":rawPredict")
+ .headers(headers -> headers.setBearerAuth(this.getBearerToken(credentials)))
+ .body(ModelOptionsUtils.toJsonString(chatRequest))
+ .retrieve()
+ .toEntity(ChatCompletionResponse.class);
+ }
+
+ /**
+ * Creates a streaming chat response for the given chat conversation.
+ * @param chatRequest The chat completion request. Must have the stream property set
+ * to true.
+ * @return Returns a {@link Flux} stream from chat completion chunks.
+ */
+ public Flux chatCompletionStream(ChatCompletionRequest chatRequest, String model) {
+
+ Assert.notNull(chatRequest, "The request body can not be null.");
+ Assert.isTrue(chatRequest.stream(), "Request must set the steam property to true.");
+
+ AtomicBoolean isInsideTool = new AtomicBoolean(false);
+
+ AtomicReference chatCompletionReference = new AtomicReference<>();
+
+ return this.webClient.post()
+ .uri(VERTEXAI_ANTHROPIC_ENDPOINT.formatted(projectId, location, model) + ":streamRawPredict")
+ .headers(headers -> headers.setBearerAuth(this.getBearerToken(credentials)))
+ .body(Mono.just(chatRequest), ChatCompletionRequest.class)
+ .retrieve()
+ .bodyToFlux(String.class)
+ .takeUntil(SSE_DONE_PREDICATE)
+ .filter(SSE_DONE_PREDICATE.negate())
+ .map(content -> ModelOptionsUtils.jsonToObject(content, StreamEvent.class))
+ .filter(event -> event.type() != EventType.PING)
+ // Detect if the chunk is part of a streaming function call.
+ .map(event -> {
+ if (this.streamHelper.isToolUseStart(event)) {
+ isInsideTool.set(true);
+ }
+ return event;
+ })
+ // Group all chunks belonging to the same function call.
+ .windowUntil(event -> {
+ if (isInsideTool.get() && this.streamHelper.isToolUseFinish(event)) {
+ isInsideTool.set(false);
+ return true;
+ }
+ return !isInsideTool.get();
+ })
+ // Merging the window chunks into a single chunk.
+ .concatMapIterable(window -> {
+ Mono monoChunk = window.reduce(new ToolUseAggregationEvent(),
+ this.streamHelper::mergeToolUseEvents);
+ return List.of(monoChunk);
+ })
+ .flatMap(mono -> mono)
+ .map(event -> streamHelper.eventToChatCompletionResponse(event, chatCompletionReference))
+ .filter(chatCompletionResponse -> chatCompletionResponse.type() != null);
+ }
+
+ /**
+ * Returns the bearer token from the given credentials.
+ * @param credentials The Google credentials.
+ * @return The bearer token.
+ */
+ private String getBearerToken(GoogleCredentials credentials) {
+ Assert.notNull(credentials, "The credentials can not be null.");
+
+ try {
+ credentials.refreshIfExpired();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return credentials.getAccessToken().getTokenValue();
+ }
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicMessage.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicMessage.java
new file mode 100644
index 00000000000..615ed5dff72
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicMessage.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * Input messages.
+ *
+ * Our models are trained to operate on alternating user and assistant conversational
+ * turns. When creating a new Message, you specify the prior conversational turns with the
+ * messages parameter, and the model then generates the next Message in the conversation.
+ * Each input message must be an object with a role and content. You can specify a single
+ * user-role message, or you can include multiple user and assistant messages. The first
+ * message must always use the user role. If the final message uses the assistant role,
+ * the response content will continue immediately from the content in that message. This
+ * can be used to constrain part of the model's response.
+ *
+ * @param content The contents of the message. Can be of one of String or
+ * MultiModalContent.
+ * @param role The role of the messages author. Could be one of the {@link Role} types.
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record AnthropicMessage(
+// @formatter:off
+ @JsonProperty("content") List content,
+ @JsonProperty("role") Role role) {
+ // @formatter:on
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicUsage.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicUsage.java
new file mode 100644
index 00000000000..362a9961390
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/AnthropicUsage.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ApiUsage} implementation for {@literal AnthropicApi}.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public class AnthropicUsage implements Usage {
+
+ public static AnthropicUsage from(ApiUsage usage) {
+ return new AnthropicUsage(usage);
+ }
+
+ private final ApiUsage usage;
+
+ protected AnthropicUsage(ApiUsage usage) {
+ Assert.notNull(usage, "AnthropicApi Usage must not be null");
+ this.usage = usage;
+ }
+
+ protected ApiUsage getUsage() {
+ return this.usage;
+ }
+
+ @Override
+ public Long getPromptTokens() {
+ return getUsage().inputTokens().longValue();
+ }
+
+ @Override
+ public Long getGenerationTokens() {
+ return getUsage().outputTokens().longValue();
+ }
+
+ @Override
+ public Long getTotalTokens() {
+ return this.getPromptTokens() + this.getGenerationTokens();
+ }
+
+ @Override
+ public String toString() {
+ return getUsage().toString();
+ }
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ApiUsage.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ApiUsage.java
new file mode 100644
index 00000000000..cbd071dabb0
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ApiUsage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Usage statistics.
+ *
+ * @param inputTokens The number of input tokens which were used.
+ * @param outputTokens The number of output tokens which were used. completion).
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ApiUsage(
+// @formatter:off
+ @JsonProperty("input_tokens") Integer inputTokens,
+ @JsonProperty("output_tokens") Integer outputTokens) {
+ // @formatter:off
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionRequest.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionRequest.java
new file mode 100644
index 00000000000..80b8564daf3
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionRequest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * @param anthropicVersion The VertexAI mandatory field for calling Anthropic Claude
+ * models (default value is 'vertex-2023-10-16').
+ * @param messages Input messages.
+ * @param system System prompt. A system prompt is a way of providing context and
+ * instructions to Claude, such as specifying a particular goal or role. See our
+ * guide to system
+ * prompts.
+ * @param maxTokens The maximum number of tokens to generate before stopping. Note that
+ * our models may stop before reaching this maximum. This parameter only specifies the
+ * absolute maximum number of tokens to generate. Different models have different maximum
+ * values for this parameter.
+ * @param metadata An object describing metadata about the request.
+ * @param stopSequences Custom text sequences that will cause the model to stop
+ * generating. Our models will normally stop when they have naturally completed their
+ * turn, which will result in a response stop_reason of "end_turn". If you want the model
+ * to stop generating when it encounters custom strings of text, you can use the
+ * stop_sequences parameter. If the model encounters one of the custom sequences, the
+ * response stop_reason value will be "stop_sequence" and the response stop_sequence value
+ * will contain the matched stop sequence.
+ * @param stream Whether to incrementally stream the response using server-sent events.
+ * @param temperature Amount of randomness injected into the response.Defaults to 1.0.
+ * Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice,
+ * and closer to 1.0 for creative and generative tasks. Note that even with temperature of
+ * 0.0, the results will not be fully deterministic.
+ * @param topP Use nucleus sampling. In nucleus sampling, we compute the cumulative
+ * distribution over all the options for each subsequent token in decreasing probability
+ * order and cut it off once it reaches a particular probability specified by top_p. You
+ * should either alter temperature or top_p, but not both. Recommended for advanced use
+ * cases only. You usually only need to use temperature.
+ * @param topK Only sample from the top K options for each subsequent token. Used to
+ * remove "long tail" low probability responses. Learn more technical details here.
+ * Recommended for advanced use cases only. You usually only need to use temperature.
+ * @param tools Definitions of tools that the model may use. If provided the model may
+ * return tool_use content blocks that represent the model's use of those tools. You can
+ * then run those tools using the tool input generated by the model and then optionally
+ * return results back to the model using tool_result content blocks.
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ChatCompletionRequest(
+// @formatter:off
+ @JsonProperty("anthropic_version") String anthropicVersion,
+ @JsonProperty("messages") List messages,
+ @JsonProperty("system") String system,
+ @JsonProperty("max_tokens") Integer maxTokens,
+ @JsonProperty("metadata") Metadata metadata,
+ @JsonProperty("stop_sequences") List stopSequences,
+ @JsonProperty("stream") Boolean stream,
+ @JsonProperty("temperature") Float temperature,
+ @JsonProperty("top_p") Float topP,
+ @JsonProperty("top_k") Integer topK,
+ @JsonProperty("tools") List tools) {
+ // @formatter:on
+
+ public ChatCompletionRequest(String anthropicVersion, List messages, String system,
+ Integer maxTokens, Float temperature, Boolean stream) {
+ this(anthropicVersion, messages, system, maxTokens, null, null, stream, temperature, null, null, null);
+ }
+
+ public ChatCompletionRequest(String anthropicVersion, List messages, String system,
+ Integer maxTokens, List stopSequences, Float temperature, Boolean stream) {
+ this(anthropicVersion, messages, system, maxTokens, null, stopSequences, stream, temperature, null, null, null);
+ }
+
+ /**
+ * @param userId An external identifier for the user who is associated with the
+ * request. This should be a uuid, hash value, or other opaque identifier. Anthropic
+ * may use this id to help detect abuse. Do not include any identifying information
+ * such as name, email address, or phone number.
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record Metadata(@JsonProperty("user_id") String userId) {
+ }
+
+ public static ChatCompletionRequestBuilder builder() {
+ return new ChatCompletionRequestBuilder();
+ }
+
+ public static ChatCompletionRequestBuilder from(ChatCompletionRequest request) {
+ return new ChatCompletionRequestBuilder(request);
+ }
+
+ public static class ChatCompletionRequestBuilder {
+
+ private String anthropicVersion;
+
+ private List messages;
+
+ private String system;
+
+ private Integer maxTokens;
+
+ private ChatCompletionRequest.Metadata metadata;
+
+ private List stopSequences;
+
+ private Boolean stream = false;
+
+ private Float temperature;
+
+ private Float topP;
+
+ private Integer topK;
+
+ private List tools;
+
+ private ChatCompletionRequestBuilder() {
+ }
+
+ private ChatCompletionRequestBuilder(ChatCompletionRequest request) {
+ this.anthropicVersion = request.anthropicVersion;
+ this.messages = request.messages;
+ this.system = request.system;
+ this.maxTokens = request.maxTokens;
+ this.metadata = request.metadata;
+ this.stopSequences = request.stopSequences;
+ this.stream = request.stream;
+ this.temperature = request.temperature;
+ this.topP = request.topP;
+ this.topK = request.topK;
+ this.tools = request.tools;
+ }
+
+ public ChatCompletionRequestBuilder withAnthropicVersion(String anthropicVersion) {
+ this.anthropicVersion = anthropicVersion;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withMessages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withSystem(String system) {
+ this.system = system;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withMaxTokens(Integer maxTokens) {
+ this.maxTokens = maxTokens;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withMetadata(ChatCompletionRequest.Metadata metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withStopSequences(List stopSequences) {
+ this.stopSequences = stopSequences;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withStream(Boolean stream) {
+ this.stream = stream;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withTemperature(Float temperature) {
+ this.temperature = temperature;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withTopP(Float topP) {
+ this.topP = topP;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withTopK(Integer topK) {
+ this.topK = topK;
+ return this;
+ }
+
+ public ChatCompletionRequestBuilder withTools(List tools) {
+ this.tools = tools;
+ return this;
+ }
+
+ public ChatCompletionRequest build() {
+ return new ChatCompletionRequest(anthropicVersion, messages, system, maxTokens, metadata, stopSequences,
+ stream, temperature, topP, topK, tools);
+ }
+
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionResponse.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionResponse.java
new file mode 100644
index 00000000000..163c2535ebc
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ChatCompletionResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * @param id Unique object identifier. The format and length of IDs may change over time.
+ * @param type Object type. For Messages, this is always "message".
+ * @param role Conversational role of the generated message. This will always be
+ * "assistant".
+ * @param content Content generated by the model. This is an array of content blocks.
+ * @param model The model that handled the request.
+ * @param stopReason The reason the model stopped generating tokens. This will be one of
+ * "end_turn", "max_tokens", "stop_sequence", "tool_use", or "timeout".
+ * @param stopSequence Which custom stop sequence was generated, if any.
+ * @param usage Input and output token usage.
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ChatCompletionResponse(
+// @formatter:off
+ @JsonProperty("id") String id,
+ @JsonProperty("type") String type,
+ @JsonProperty("role") Role role,
+ @JsonProperty("content") List content,
+ @JsonProperty("model") String model,
+ @JsonProperty("stop_reason") String stopReason,
+ @JsonProperty("stop_sequence") String stopSequence,
+ @JsonProperty("usage") ApiUsage usage) {
+ // @formatter:on
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ContentBlock.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ContentBlock.java
new file mode 100644
index 00000000000..e2ac91e6902
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/ContentBlock.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/**
+ * @param type the content type can be "text", "image", "tool_use", "tool_result" or
+ * "text_delta".
+ * @param source The source of the media content. Applicable for "image" types only.
+ * @param text The text of the message. Applicable for "text" types only.
+ * @param index The index of the content block. Applicable only for streaming responses.
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ContentBlock(
+// @formatter:off
+ @JsonProperty("type") Type type,
+ @JsonProperty("source") Source source,
+ @JsonProperty("text") String text,
+
+ // applicable only for streaming responses.
+ @JsonProperty("index") Integer index,
+
+ // tool_use response only
+ @JsonProperty("id") String id,
+ @JsonProperty("name") String name,
+ @JsonProperty("input") Map input,
+
+ // tool_result response only
+ @JsonProperty("tool_use_id") String toolUseId,
+ @JsonProperty("content") String content) {
+ // @formatter:on
+
+ public ContentBlock(String mediaType, String data) {
+ this(new Source(mediaType, data));
+ }
+
+ public ContentBlock(Source source) {
+ this(Type.IMAGE, source, null, null, null, null, null, null, null);
+ }
+
+ public ContentBlock(String text) {
+ this(Type.TEXT, null, text, null, null, null, null, null, null);
+ }
+
+ // Tool result
+ public ContentBlock(Type type, String toolUseId, String content) {
+ this(type, null, null, null, null, null, null, toolUseId, content);
+ }
+
+ public ContentBlock(Type type, Source source, String text, Integer index) {
+ this(type, source, text, index, null, null, null, null, null);
+ }
+
+ // Tool use input JSON delta streaming
+ public ContentBlock(Type type, String id, String name, Map input) {
+ this(type, null, null, null, id, name, input, null, null);
+ }
+
+ /**
+ * The ContentBlock type.
+ */
+ public enum Type {
+
+ /**
+ * Tool request
+ */
+ @JsonProperty("tool_use")
+ TOOL_USE("tool_use"),
+
+ /**
+ * Send tool result back to LLM.
+ */
+ @JsonProperty("tool_result")
+ TOOL_RESULT("tool_result"),
+
+ /**
+ * Text message.
+ */
+ @JsonProperty("text")
+ TEXT("text"),
+
+ /**
+ * Text delta message. Returned from the streaming response.
+ */
+ @JsonProperty("text_delta")
+ TEXT_DELTA("text_delta"),
+
+ /**
+ * Tool use input partial JSON delta streaming.
+ */
+ @JsonProperty("input_json_delta")
+ INPUT_JSON_DELTA("input_json_delta"),
+
+ /**
+ * Image message.
+ */
+ @JsonProperty("image")
+ IMAGE("image");
+
+ public final String value;
+
+ Type(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ }
+
+ /**
+ * The source of the media content. (Applicable for "image" types only)
+ *
+ * @param type The type of the media content. Only "base64" is supported at the
+ * moment.
+ * @param mediaType The media type of the content. For example, "image/png" or
+ * "image/jpeg".
+ * @param data The base64-encoded data of the content.
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record Source(
+ // @formatter:off
+ @JsonProperty("type") String type,
+ @JsonProperty("media_type") String mediaType,
+ @JsonProperty("data") String data) {
+ // @formatter:on
+
+ public Source(String mediaType, String data) {
+ this("base64", mediaType, data);
+ }
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Role.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Role.java
new file mode 100644
index 00000000000..9c2746c1ed8
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Role.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The role of the author of this message.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public enum Role {
+
+ // @formatter:off
+ @JsonProperty("user") USER,
+ @JsonProperty("assistant") ASSISTANT
+ // @formatter:on
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Tool.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Tool.java
new file mode 100644
index 00000000000..14ca9247b5c
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/Tool.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/**
+ * The tool used in the conversation.
+ *
+ * @param name The name of the tool.
+ * @param description The description of the tool.
+ * @param inputSchema The input schema of the tool.
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record Tool(
+// @formatter:off
+ @JsonProperty("name") String name,
+ @JsonProperty("description") String description,
+ @JsonProperty("input_schema") Map inputSchema) {
+ // @formatter:on
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockDeltaEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockDeltaEvent.java
new file mode 100644
index 00000000000..7c4ee851897
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockDeltaEvent.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * Event for content block delta.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ContentBlockDeltaEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("index") Integer index,
+ @JsonProperty("delta") ContentBlockDeltaBody delta) implements StreamEvent {
+ // @formatter:on
+
+ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
+ visible = true)
+ @JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockDeltaText.class, name = "text_delta"),
+ @JsonSubTypes.Type(value = ContentBlockDeltaJson.class, name = "input_json_delta") })
+ public interface ContentBlockDeltaBody {
+
+ String type();
+
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ContentBlockDeltaText(@JsonProperty("type") String type,
+ @JsonProperty("text") String text) implements ContentBlockDeltaBody {
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ContentBlockDeltaJson(@JsonProperty("type") String type,
+ @JsonProperty("partial_json") String partialJson) implements ContentBlockDeltaBody {
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStartEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStartEvent.java
new file mode 100644
index 00000000000..61d5ff0023d
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStartEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import java.util.Map;
+
+/**
+ * Event that represents a change in the content of a block.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ContentBlockStartEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("index") Integer index,
+ @JsonProperty("content_block") ContentBlockBody contentBlock) implements StreamEvent {
+ // @formatter:on
+
+ @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") })
+ public interface ContentBlockBody {
+
+ String type();
+
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ContentBlockToolUse(@JsonProperty("type") String type, @JsonProperty("id") String id,
+ @JsonProperty("name") String name,
+ @JsonProperty("input") Map input) implements ContentBlockBody {
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record ContentBlockText(@JsonProperty("type") String type,
+ @JsonProperty("text") String text) implements ContentBlockBody {
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStopEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStopEvent.java
new file mode 100644
index 00000000000..3a6fdf72708
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ContentBlockStopEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Event that indicates that a content block has stopped.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ContentBlockStopEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("index") Integer index) implements StreamEvent {
+ // @formatter:on
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ErrorEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ErrorEvent.java
new file mode 100644
index 00000000000..baa9eaa0481
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ErrorEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * An error event.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ErrorEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("error") Error error) implements StreamEvent {
+ // @formatter:on
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record Error(@JsonProperty("type") String type, @JsonProperty("message") String message) {
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/EventType.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/EventType.java
new file mode 100644
index 00000000000..2a8c762de9e
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/EventType.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The evnt type of the streamed chunk.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public enum EventType {
+
+ /**
+ * Message start event. Contains a Message object with empty content.
+ */
+ @JsonProperty("message_start")
+ MESSAGE_START,
+
+ /**
+ * Message delta event, indicating top-level changes to the final Message object.
+ */
+ @JsonProperty("message_delta")
+ MESSAGE_DELTA,
+
+ /**
+ * A final message stop event.
+ */
+ @JsonProperty("message_stop")
+ MESSAGE_STOP,
+
+ /**
+ *
+ */
+ @JsonProperty("content_block_start")
+ CONTENT_BLOCK_START,
+
+ /**
+ *
+ */
+ @JsonProperty("content_block_delta")
+ CONTENT_BLOCK_DELTA,
+
+ /**
+ *
+ */
+ @JsonProperty("content_block_stop")
+ CONTENT_BLOCK_STOP,
+
+ /**
+ *
+ */
+ @JsonProperty("error")
+ ERROR,
+
+ /**
+ *
+ */
+ @JsonProperty("ping")
+ PING,
+
+ /**
+ * Artifically created event to aggregate tool use events.
+ */
+ TOOL_USE_AGGREGATE;
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageDeltaEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageDeltaEvent.java
new file mode 100644
index 00000000000..bc61dfaaaf5
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageDeltaEvent.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * MessageDeltaEvent
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageDeltaEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("delta") MessageDelta delta,
+ @JsonProperty("usage") MessageDeltaUsage usage) implements StreamEvent {
+ // @formatter:on
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record MessageDelta(@JsonProperty("stop_reason") String stopReason,
+ @JsonProperty("stop_sequence") String stopSequence) {
+ }
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public record MessageDeltaUsage(@JsonProperty("output_tokens") Integer outputTokens) {
+ }
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStartEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStartEvent.java
new file mode 100644
index 00000000000..5e90e3692c7
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStartEvent.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.springframework.ai.vertexai.anthropic.model.ChatCompletionResponse;
+
+/**
+ * A message start event.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageStartEvent(
+// @formatter:off
+ @JsonProperty("type") EventType type,
+ @JsonProperty("message") ChatCompletionResponse message) implements StreamEvent {
+ // @formatter:on
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStopEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStopEvent.java
new file mode 100644
index 00000000000..8178052e6b7
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/MessageStopEvent.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A message stop event.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record MessageStopEvent(@JsonProperty("type") EventType type) implements StreamEvent {
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/PingEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/PingEvent.java
new file mode 100644
index 00000000000..edf61ddcafc
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/PingEvent.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A ping event.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record PingEvent(@JsonProperty("type") EventType type) implements StreamEvent {
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/StreamEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/StreamEvent.java
new file mode 100644
index 00000000000..d276665a5bb
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/StreamEvent.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * A stream event.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type",
+ visible = true)
+@JsonSubTypes({ @JsonSubTypes.Type(value = ContentBlockStartEvent.class, name = "content_block_start"),
+ @JsonSubTypes.Type(value = ContentBlockDeltaEvent.class, name = "content_block_delta"),
+ @JsonSubTypes.Type(value = ContentBlockStopEvent.class, name = "content_block_stop"),
+ @JsonSubTypes.Type(value = PingEvent.class, name = "ping"),
+ @JsonSubTypes.Type(value = ErrorEvent.class, name = "error"),
+ @JsonSubTypes.Type(value = MessageStartEvent.class, name = "message_start"),
+ @JsonSubTypes.Type(value = MessageDeltaEvent.class, name = "message_delta"),
+ @JsonSubTypes.Type(value = MessageStopEvent.class, name = "message_stop") })
+public interface StreamEvent {
+
+ @JsonProperty("type")
+ EventType type();
+
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ToolUseAggregationEvent.java b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ToolUseAggregationEvent.java
new file mode 100644
index 00000000000..54617642668
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/model/stream/ToolUseAggregationEvent.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vertexai.anthropic.model.stream;
+
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Special event used to aggregate multiple tool use events into a single event with list
+ * of aggregated ContentBlockToolUse.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+public class ToolUseAggregationEvent implements StreamEvent {
+
+ private Integer index;
+
+ private String id;
+
+ private String name;
+
+ private String partialJson = "";
+
+ private List toolContentBlocks = new ArrayList<>();
+
+ @Override
+ public EventType type() {
+ return EventType.TOOL_USE_AGGREGATE;
+ }
+
+ public List getToolContentBlocks() {
+ return this.toolContentBlocks;
+ }
+
+ public boolean isEmpty() {
+ return (this.index == null || this.id == null || this.name == null || !StringUtils.hasText(this.partialJson));
+ }
+
+ public ToolUseAggregationEvent withIndex(Integer index) {
+ this.index = index;
+ return this;
+ }
+
+ public ToolUseAggregationEvent withId(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public ToolUseAggregationEvent withName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ToolUseAggregationEvent appendPartialJson(String partialJson) {
+ this.partialJson = this.partialJson + partialJson;
+ return this;
+ }
+
+ public void squashIntoContentBlock() {
+ Map map = (StringUtils.hasText(this.partialJson))
+ ? ModelOptionsUtils.jsonToMap(this.partialJson) : Map.of();
+ this.toolContentBlocks.add(new ContentBlockStartEvent.ContentBlockToolUse("tool_use", this.id, this.name, map));
+ this.index = null;
+ this.id = null;
+ this.name = null;
+ this.partialJson = "";
+ }
+
+ @Override
+ public String toString() {
+ return "EventToolUseBuilder [index=" + index + ", id=" + id + ", name=" + name + ", partialJson=" + partialJson
+ + ", toolUseMap=" + toolContentBlocks + "]";
+ }
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/main/resources/META-INF/spring/aot.factories b/models/spring-ai-vertex-ai-anthropic/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 00000000000..51820519f61
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1,2 @@
+org.springframework.aot.hint.RuntimeHintsRegistrar=\
+ org.springframework.ai.vertexai.anthropic.aot.VertexAiAnthropicRuntimeHints
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/VertexAIAnthropicChatModelTest.java b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/VertexAIAnthropicChatModelTest.java
new file mode 100644
index 00000000000..6a7ee31d2e5
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/VertexAIAnthropicChatModelTest.java
@@ -0,0 +1,217 @@
+package org.springframework.ai.vertexai.anthropic;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.ToolResponseMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.vertexai.anthropic.api.StreamHelper;
+import org.springframework.ai.vertexai.anthropic.api.VertexAiAnthropicApi;
+import org.springframework.ai.vertexai.anthropic.model.*;
+import org.springframework.http.ResponseEntity;
+import reactor.core.publisher.Flux;
+
+import java.util.HashMap;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class VertexAiAnthropicChatModelTest {
+
+ private VertexAiAnthropicApi anthropicApi;
+
+ private VertexAiAnthropicChatModel chatModel;
+
+ @BeforeEach
+ void setUp() {
+ anthropicApi = mock(VertexAiAnthropicApi.class);
+ chatModel = new VertexAiAnthropicChatModel(anthropicApi);
+ }
+
+ @Test
+ void call_withValidPrompt_returnsChatResponse() {
+ Prompt prompt = new Prompt(List.of(new UserMessage("Hello")));
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("Hello, how can I help you?")))
+ .build();
+
+ when(anthropicApi.chatCompletion(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(ResponseEntity.ok(response));
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+
+ assertNotNull(chatResponse);
+ assertEquals(1, chatResponse.getResults().size());
+ assertEquals("Hello, how can I help you?", chatResponse.getResults().get(0).getOutput().getContent());
+ }
+
+ @Test
+ void call_withValidPromptWithOptions_returnsChatResponse() {
+ VertexAiAnthropicChatOptions options = VertexAiAnthropicChatOptions.builder()
+ .withAnthropicVersion("vertex-2023-10-16")
+ .withModel("claude-3-opus@20240229")
+ .withMaxTokens(100)
+ .build();
+ Prompt prompt = new Prompt(new UserMessage("Hello"), options);
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-opus@20240229")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("Hello, how can I help you?")))
+ .build();
+
+ when(anthropicApi.chatCompletion(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(ResponseEntity.ok(response));
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+
+ assertNotNull(chatResponse);
+ assertEquals(1, chatResponse.getResults().size());
+ assertEquals("Hello, how can I help you?", chatResponse.getResults().get(0).getOutput().getContent());
+ }
+
+ @Test
+ void call_withValidPrompt_returnsChatResponseWithTools() {
+ Prompt prompt = new Prompt(List.of(new UserMessage("What is the wheather today in Milan?")));
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock(ContentBlock.Type.TOOL_USE, "1", "weather", new HashMap<>())))
+ .build();
+
+ when(anthropicApi.chatCompletion(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(ResponseEntity.ok(response));
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+
+ assertNotNull(chatResponse);
+ assertEquals(1, chatResponse.getResults().size());
+ assertEquals("weather", chatResponse.getResults().get(0).getOutput().getToolCalls().get(0).name());
+ }
+
+ @Test
+ void call_withValidCompleteToolPrompt_returnsChatResponse() {
+ AssistantMessage.ToolCall toolCall = new AssistantMessage.ToolCall("1", "type", "weather",
+ "{\"city\": \"Milan\", \"when\": \"today\"}");
+
+ List messages = List.of(new UserMessage("Hello"), new AssistantMessage("Hello, how can I help you?"),
+ new UserMessage("What is the wheather today in Milan?"),
+ new ToolResponseMessage(
+ List.of(new ToolResponseMessage.ToolResponse("1", "weather", "20 degrees and sunny"))),
+ new AssistantMessage("The weather in Milan is 20°C and sunny.", new HashMap<>(), List.of(toolCall)),
+ new UserMessage("Thank you"));
+
+ Prompt prompt = new Prompt(messages);
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("You're welcome!")))
+ .build();
+
+ when(anthropicApi.chatCompletion(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(ResponseEntity.ok(response));
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+
+ assertNotNull(chatResponse);
+ assertEquals(1, chatResponse.getResults().size());
+ assertEquals("You're welcome!", chatResponse.getResults().get(0).getOutput().getContent());
+ }
+
+ @Test
+ void call_withValidPartialToolPrompt_returnsChatResponse() {
+ List messages = List.of(new UserMessage("Hello"), new AssistantMessage("Hello, how can I help you?"),
+ new UserMessage("What is the wheather today in Milan?"), new ToolResponseMessage(
+ List.of(new ToolResponseMessage.ToolResponse("1", "weather", "20 degrees and sunny"))));
+
+ Prompt prompt = new Prompt(messages);
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("The weather in Milan is 20°C and sunny.")))
+ .build();
+
+ when(anthropicApi.chatCompletion(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(ResponseEntity.ok(response));
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+
+ assertNotNull(chatResponse);
+ assertEquals(1, chatResponse.getResults().size());
+ assertEquals("The weather in Milan is 20°C and sunny.",
+ chatResponse.getResults().get(0).getOutput().getContent());
+ }
+
+ @Test
+ void stream_withValidPrompt_returnsFluxOfChatResponses() {
+ Prompt prompt = new Prompt(List.of(new UserMessage("Stream this")));
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("Streaming response")))
+ .build();
+
+ when(anthropicApi.chatCompletionStream(any(ChatCompletionRequest.class), anyString()))
+ .thenReturn(Flux.just(response));
+
+ Flux chatResponseFlux = chatModel.stream(prompt);
+
+ List chatResponses = chatResponseFlux.collectList().block();
+ assertNotNull(chatResponses);
+ assertEquals(1, chatResponses.size());
+ assertEquals("Streaming response", chatResponses.get(0).getResults().get(0).getOutput().getContent());
+ }
+
+ @Test
+ void call_withNullPrompt_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> chatModel.call((Prompt) null));
+ }
+
+ @Test
+ void stream_withNullPrompt_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> chatModel.stream((Prompt) null));
+ }
+
+ @Test
+ void call_withEmptyPrompt_throwsException() {
+ Prompt prompt = new Prompt(List.of());
+ assertThrows(IllegalArgumentException.class, () -> chatModel.call(prompt));
+ }
+
+ @Test
+ void stream_withEmptyPrompt_throwsException() {
+ Prompt prompt = new Prompt(List.of());
+ assertThrows(IllegalArgumentException.class, () -> chatModel.stream(prompt));
+ }
+
+}
diff --git a/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/aot/VertexAIAnthropicRuntimeHintsTest.java b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/aot/VertexAIAnthropicRuntimeHintsTest.java
new file mode 100644
index 00000000000..8f0b1b7e0a3
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/aot/VertexAIAnthropicRuntimeHintsTest.java
@@ -0,0 +1,28 @@
+package org.springframework.ai.vertexai.anthropic.aot;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.TypeReference;
+
+import java.util.Set;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
+import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
+
+class VertexAiAnthropicRuntimeHintsTest {
+
+ @Test
+ void registerHints() {
+ RuntimeHints runtimeHints = new RuntimeHints();
+ VertexAiAnthropicRuntimeHints vertexAIAnthropicRuntimeHints = new VertexAiAnthropicRuntimeHints();
+ vertexAIAnthropicRuntimeHints.registerHints(runtimeHints, null);
+
+ Set jsonAnnotatedClasses = findJsonAnnotatedClassesInPackage(VertexAiAnthropicChatModel.class);
+ for (TypeReference jsonAnnotatedClass : jsonAnnotatedClasses) {
+ assertThat(runtimeHints).matches(reflection().onType(jsonAnnotatedClass));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/api/VertexAIAnthropicApiTest.java b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/api/VertexAIAnthropicApiTest.java
new file mode 100644
index 00000000000..8a93d9f7192
--- /dev/null
+++ b/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/api/VertexAIAnthropicApiTest.java
@@ -0,0 +1,259 @@
+package org.springframework.ai.vertexai.anthropic.api;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.vertexai.anthropic.model.*;
+import org.springframework.ai.vertexai.anthropic.model.stream.EventType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+class VertexAiAnthropicApiTest {
+
+ private VertexAiAnthropicApi vertexAIAnthropicApi;
+
+ private GoogleCredentials mockCredentials;
+
+ private RestClient mockRestClient;
+
+ private WebClient mockWebClient;
+
+ @BeforeEach
+ void setUp() {
+ mockCredentials = mock(GoogleCredentials.class);
+ mockRestClient = mock(RestClient.class);
+ mockWebClient = mock(WebClient.class);
+ vertexAIAnthropicApi = new VertexAiAnthropicApi.Builder().projectId("project-id")
+ .location("location")
+ .credentials(mockCredentials)
+ .restClient(mockRestClient)
+ .webClient(mockWebClient)
+ .build();
+ }
+
+ @Test
+ void chatCompletion_validRequest_returnsResponseEntity() throws IOException {
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .withAnthropicVersion("vertex-2023-10-16")
+ .withMaxTokens(100)
+ .withMessages(List.of(new AnthropicMessage(List.of(new ContentBlock("Hello")), Role.USER)))
+ .withTemperature(0.5f)
+ .withTopP(1.0f)
+ .withStream(false)
+ .build();
+
+ ChatCompletionResponse response = new StreamHelper.ChatCompletionResponseBuilder().withId("1")
+ .withType("message")
+ .withRole(Role.ASSISTANT)
+ .withModel("claude-3-5-sonnet@20240620")
+ .withStopReason("end_turn")
+ .withStopSequence(null)
+ .withUsage(new ApiUsage(100, 50))
+ .withContent(List.of(new ContentBlock("Hello, how can I help you?")))
+ .build();
+
+ ResponseEntity expectedResponse = ResponseEntity.ok(response);
+
+ // Mock the RestClient and its nested interfaces
+ RestClient.RequestBodyUriSpec mockRequestBodyUriSpec = mock(RestClient.RequestBodyUriSpec.class);
+ RestClient.RequestBodySpec mockRequestBodySpec = mock(RestClient.RequestBodySpec.class);
+ RestClient.ResponseSpec mockResponseSpec = mock(RestClient.ResponseSpec.class);
+
+ // Stub the methods to return the mock instances and expected response
+ when(mockRestClient.post()).thenReturn(mockRequestBodyUriSpec);
+ when(mockRequestBodyUriSpec.uri(anyString())).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.headers(any(Consumer.class))).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.body(anyString())).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.retrieve()).thenReturn(mockResponseSpec);
+ when(mockResponseSpec.toEntity(ChatCompletionResponse.class)).thenReturn(expectedResponse);
+
+ ResponseEntity responseEntity = vertexAIAnthropicApi.chatCompletion(request,
+ "claude-3-5-sonnet@20240620");
+
+ assertEquals(expectedResponse, responseEntity);
+ }
+
+ @Test
+ void chatCompletionStream_validRequest_returnsFlux() {
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .withAnthropicVersion("vertex-2023-10-16")
+ .withMaxTokens(100)
+ .withMessages(List.of(new AnthropicMessage(List.of(new ContentBlock("Hello")), Role.USER)))
+ .withTemperature(0.5f)
+ .withTopP(1.0f)
+ .withStream(true)
+ .build();
+
+ List mockResponseStrings = List.of(
+ "{\"type\": \"message_start\", \"message\": {\"id\": \"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY\", \"type\": \"message\", \"role\": \"assistant\", \"content\": [], \"model\": \"claude-3-5-sonnet-20240620\", \"stop_reason\": null, \"stop_sequence\": null, \"usage\": {\"input_tokens\": 25, \"output_tokens\": 1}}}",
+ "{\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}",
+ "{\"type\": \"ping\"}",
+ "{\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello\"}}",
+ "{\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"!\"}}",
+ "{\"type\": \"content_block_stop\", \"index\": 0}",
+ "{\"type\": \"message_delta\", \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\":null}, \"usage\": {\"output_tokens\": 15}}",
+ "{\"type\": \"message_stop\"}");
+
+ Flux expectedFlux = Flux.fromIterable(mockResponseStrings);
+
+ // Mock the WebClient and its nested interfaces
+ WebClient.RequestBodyUriSpec mockRequestBodyUriSpec = mock(WebClient.RequestBodyUriSpec.class);
+ WebClient.RequestBodySpec mockRequestBodySpec = mock(WebClient.RequestBodySpec.class);
+ WebClient.RequestHeadersSpec mockRequestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
+ WebClient.ResponseSpec mockResponseSpec = mock(WebClient.ResponseSpec.class);
+
+ // Stub the methods to return the mock instances and expected response
+ when(mockWebClient.post()).thenReturn(mockRequestBodyUriSpec);
+ when(mockRequestBodyUriSpec.uri(anyString())).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.headers(any(Consumer.class))).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.body(any(Mono.class), eq(ChatCompletionRequest.class)))
+ .thenReturn(mockRequestHeadersSpec);
+ when(mockRequestHeadersSpec.retrieve()).thenReturn(mockResponseSpec);
+ when(mockResponseSpec.bodyToFlux(String.class)).thenReturn(expectedFlux);
+
+ Flux responseFlux = vertexAIAnthropicApi.chatCompletionStream(request,
+ "claude-3-5-sonnet@20240620");
+
+ // concatenate alle the text contents from the responseFlux into a single string
+ StringBuilder sb = new StringBuilder();
+ responseFlux.subscribe(s -> {
+ if (s.content().size() > 0) {
+ sb.append(s.content().get(0).text());
+ }
+ });
+
+ assertEquals("Hello!", sb.toString());
+ }
+
+ @Test
+ void chatCompletionStream_validRequestWithTools_returnsFlux() {
+ String toolInput = "{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The city and state, e.g. San Francisco, CA\"}},\"required\":[\"location\"]}";
+ Tool tool = new Tool("get_weather", "Get the current weather in a given location",
+ ModelOptionsUtils.jsonToObject(toolInput, HashMap.class));
+
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .withAnthropicVersion("vertex-2023-10-16")
+ .withMaxTokens(100)
+ .withMessages(List.of(new AnthropicMessage(List.of(new ContentBlock("Hello")), Role.USER)))
+ .withTemperature(0.5f)
+ .withTopP(1.0f)
+ .withStream(true)
+ .withTools(List.of(tool))
+ .build();
+
+ List mockResponseStrings = List.of(
+ "{\"type\":\"message_start\",\"message\":{\"id\":\"msg_014p7gG3wDgGV9EUtLvnow3U\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-3-5-sonnet@20240620\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":472,\"output_tokens\":2},\"content\":[],\"stop_reason\":null}}",
+ "{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}",
+ "{\"type\": \"ping\"}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Okay\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\",\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" let\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"'s\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" check\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" the\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" weather\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" for\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" San\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" Francisco\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\",\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" CA\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\":\"}}",
+ "{\"type\":\"content_block_stop\",\"index\":0}",
+ "{\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01T1x1fJ34qAmk2tNTrN7Up6\",\"name\":\"get_weather\",\"input\":{}}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"location\\\":\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"San\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" Francisc\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"o,\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" CA\\\"\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\", \"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"unit\\\": \\\"fah\"}}",
+ "{\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"renheit\\\"}\"}}",
+ "{\"type\":\"content_block_stop\",\"index\":1}",
+ "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":89}}",
+ "{\"type\":\"message_stop\"}");
+
+ Flux expectedFlux = Flux.fromIterable(mockResponseStrings);
+
+ // Mock the WebClient and its nested interfaces
+ WebClient.RequestBodyUriSpec mockRequestBodyUriSpec = mock(WebClient.RequestBodyUriSpec.class);
+ WebClient.RequestBodySpec mockRequestBodySpec = mock(WebClient.RequestBodySpec.class);
+ WebClient.RequestHeadersSpec mockRequestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
+ WebClient.ResponseSpec mockResponseSpec = mock(WebClient.ResponseSpec.class);
+
+ // Stub the methods to return the mock instances and expected response
+ when(mockWebClient.post()).thenReturn(mockRequestBodyUriSpec);
+ when(mockRequestBodyUriSpec.uri(anyString())).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.headers(any(Consumer.class))).thenReturn(mockRequestBodySpec);
+ when(mockRequestBodySpec.body(any(Mono.class), eq(ChatCompletionRequest.class)))
+ .thenReturn(mockRequestHeadersSpec);
+ when(mockRequestHeadersSpec.retrieve()).thenReturn(mockResponseSpec);
+ when(mockResponseSpec.bodyToFlux(String.class)).thenReturn(expectedFlux);
+
+ Flux responseFlux = vertexAIAnthropicApi.chatCompletionStream(request,
+ "claude-3-5-sonnet@20240620");
+
+ // concatenate alle the text contents from the responseFlux into a single string
+ StringBuilder sb = new StringBuilder();
+ responseFlux.subscribe(s -> {
+ if (s.content().size() > 0) {
+ if (s.content().get(0).type().equals(ContentBlock.Type.TEXT)
+ || s.content().get(0).type().equals(ContentBlock.Type.TEXT_DELTA)) {
+ sb.append(s.content().get(0).text());
+ }
+ else if (s.type().equals(EventType.CONTENT_BLOCK_STOP.name())
+ && s.content().get(0).type().equals(ContentBlock.Type.TOOL_USE)) {
+ sb.append(ModelOptionsUtils.toJsonString(s.content().get(0).input()));
+ }
+ }
+ });
+
+ assertEquals(
+ "Okay, let's check the weather for San Francisco, CA:{\"unit\":\"fahrenheit\",\"location\":\"San Francisco, CA\"}",
+ sb.toString());
+ }
+
+ @Test
+ void chatCompletion_nullRequest_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ vertexAIAnthropicApi.chatCompletion(null, "claude-3-5-sonnet@20240620");
+ });
+ }
+
+ @Test
+ void chatCompletionStream_nullRequest_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ vertexAIAnthropicApi.chatCompletionStream(null, "claude-3-5-sonnet@20240620");
+ });
+ }
+
+ @Test
+ void chatCompletionStream_nonStreamingRequest_throwsException() {
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .withAnthropicVersion("vertex-2023-10-16")
+ .withMaxTokens(100)
+ .withMessages(List.of(new AnthropicMessage(List.of(new ContentBlock("Hello")), Role.USER)))
+ .withTemperature(0.5f)
+ .withTopP(1.0f)
+ .withStream(false)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ vertexAIAnthropicApi.chatCompletionStream(request, "claude-3-5-sonnet@20240620");
+ });
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index fad014fe95d..9c65970d05e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,6 +72,7 @@
models/spring-ai-qianfan
models/spring-ai-stability-ai
models/spring-ai-transformers
+ models/spring-ai-vertex-ai-anthropic
models/spring-ai-vertex-ai-gemini
models/spring-ai-vertex-ai-embedding
models/spring-ai-vertex-ai-palm2
@@ -91,6 +92,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-qianfan
spring-ai-spring-boot-starters/spring-ai-starter-stability-ai
spring-ai-spring-boot-starters/spring-ai-starter-transformers
+ spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-anthropic
spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding
spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini
spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-palm2
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 7f555636e17..5a3ddb6136a 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -100,6 +100,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic
+ ${project.version}
+
+
org.springframework.ai
spring-ai-vertex-ai-palm2
@@ -403,6 +409,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic-spring-boot-starter
+ ${project.version}
+
+
org.springframework.ai
spring-ai-vertex-ai-palm2-spring-boot-starter
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
index 0676c38d74c..3775599fcda 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
@@ -16,6 +16,7 @@
*** xref:api/chat/azure-openai-chat.adoc[Azure OpenAI]
**** xref:api/chat/functions/azure-open-ai-chat-functions.adoc[Function Calling]
*** xref:api/chat/google-vertexai.adoc[Google VertexAI]
+**** xref:api/chat/vertexai-anthropic-chat.adoc[VertexAI Anthropic]
**** xref:api/chat/vertexai-palm2-chat.adoc[VertexAI PaLM2 ]
**** xref:api/chat/vertexai-gemini-chat.adoc[VertexAI Gemini]
***** xref:api/chat/functions/vertexai-gemini-chat-functions.adoc[Function Calling]
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-vertexai.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-vertexai.adoc
index 55d20942970..3432d72343d 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-vertexai.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-vertexai.adoc
@@ -5,5 +5,6 @@ link:https://cloud.google.com/vertex-ai/docs/reference[VertexAI API] provides hi
Spring AI provides integration with VertexAI API through the following clients:
+* xref:api/chat/vertexai-anthropic-chat.adoc[]
* xref:api/chat/vertexai-palm2-chat.adoc[]
* xref:api/chat/vertexai-gemini-chat.adoc[]
\ No newline at end of file
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/vertexai-anthropic-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/vertexai-anthropic-chat.adoc
new file mode 100644
index 00000000000..4a60fd2b33e
--- /dev/null
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/vertexai-anthropic-chat.adoc
@@ -0,0 +1,208 @@
+= VertexAI Anthropic Chat
+
+The https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude[Vertex AI Anthropic API] allows developers to build generative AI applications using the Anthropic Claude models.
+The Vertex AI Anthropic API when using Claude 3.5 Sonnet supports multimodal prompts as input and output text or code.
+A multimodal model is a model that is capable of processing information from multiple modalities, including images, videos, and text. For example, you can send the model a photo of a plate of cookies and ask it to give you a recipe for those cookies.
+
+Anthropic Claude models is available through the VertexAI Model Garden feature that makes available the following link:https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#available-claude-models[models]:
+
+- Claude 3.5 Sonnet
+- Claude 3 Opus
+- Claude 3 Haiku
+- Claude 3 Sonnet
+
+== Prerequisites
+
+- `PROJECT_ID` your Google Cloud projectID
+- `LOCATION` the region where the model is deployed (e.g. `europe-west1` - Please check the available regions in the Google Cloud documentation)
+- `CREDENTIALS` the Google Cloud credentials granted to access VertexAI resources (e.g. a service account json file)
+
+== Auto-configuration
+
+Spring AI provides Spring Boot auto-configuration for the VertexAI Anthropic Chat Client.
+To enable it add the following dependency to your project's Maven `pom.xml` file:
+
+[source, xml]
+----
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic-spring-boot-starter
+
+----
+
+or to your Gradle `build.gradle` build file.
+
+[source,groovy]
+----
+dependencies {
+ implementation 'org.springframework.ai:spring-ai-vertex-ai-anthropic-spring-boot-starter'
+}
+----
+
+TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
+
+=== Chat Properties
+
+The prefix `spring.ai.vertex.ai.anthropic` is used as the property prefix that lets you connect to VertexAI.
+
+[cols="3,5,1"]
+|====
+| Property | Description | Default
+
+| spring.ai.vertex.ai.anthropic.projectId | Google Cloud Platform project ID | -
+| spring.ai.vertex.ai.anthropic.location | Region | -
+| spring.ai.vertex.ai.anthropic.credentialsUri | URI to Vertex AI Anthropic credentials. When provided it is used to create an a `GoogleCredentials` instance to authenticate the `VertexAI`. | -
+|====
+
+The prefix `spring.ai.vertex.ai.anthropic.chat` is the property prefix that lets you configure the chat model implementation for VertexAI Anthropic Chat.
+
+[cols="3,5,1"]
+|====
+| Property | Description | Default
+
+| spring.ai.vertex.ai.anthropic.chat.options.model | Supported models are `claude-3-5-sonnet@20240620`, `claude-3-opus@20240229`, `claude-3-sonnet@20240229` and `claude-3-haiku@20240307`. | claude-3-5-sonnet@20240620
+| spring.ai.vertex.ai.anthropic.chat.options.temperature | Controls the randomness of the output. Values can range over [0.0,1.0], inclusive. A value closer to 1.0 will produce responses that are more varied, while a value closer to 0.0 will typically result in less surprising responses from the generative. This value specifies default to be used by the backend while making the call to the generative. | 0.8
+| spring.ai.vertex.ai.anthropic.chat.options.topK | The maximum number of tokens to consider when sampling. The generative uses combined Top-k and nucleus sampling. Top-k sampling considers the set of topK most probable tokens. | -
+| spring.ai.vertex.ai.anthropic.chat.options.topP | The maximum cumulative probability of tokens to consider when sampling. The generative uses combined Top-k and nucleus sampling. Nucleus sampling considers the smallest set of tokens whose probability sum is at least topP. | -
+| spring.ai.vertex.ai.anthropic.chat.options.maxTokens | The maximum number of tokens to generate. | 500
+| spring.ai.vertex.ai.anthropic.chat.options.anthropicVersion | The version of VertexAI Anthropic API | vertex-2023-10-16
+
+|====
+
+TIP: All properties prefixed with `spring.ai.vertex.ai.anthropic.chat.options` can be overridden at runtime by adding a request specific <> to the `Prompt` call.
+
+== Runtime options [[chat-options]]
+
+The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/VertexAiAnthropicChatOptions.java[VertexAiAnthropicChatOptions.java] provides model configurations, such as the temperature, the topK, etc.
+
+On start-up, the default options can be configured with the `VertexAiAnthropicChatModel(api)` constructor or the `spring.ai.vertex.ai.chat.options.*` properties.
+
+At runtime you can override the default options by adding new, request specific, options to the `Prompt` call.
+For example to override the default temperature for a specific request:
+
+[source,java]
+----
+ChatResponse response = chatModel.call(
+ new Prompt(
+ "Generate the names of 5 famous pirates.",
+ VertexAiAnthropicChatOptions.builder()
+ .withTemperature(0.4)
+ .build()
+ ));
+----
+
+TIP: In addition to the model specific `VertexAiChatAnthropicOptions` you can use a portable https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java[ChatOptions] instance, created with the
+https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java[ChatOptionsBuilder#builder()].
+
+== Function Calling
+
+You can register custom Java functions with the VertexAiAnthropicChatModel and have the Claude 3.5 Sonnet model intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
+This is a powerful technique to connect the LLM capabilities with external tools and APIs.
+
+== Multimodal
+
+Multimodality refers to a model's ability to simultaneously understand and process information from various sources, including text, images, audio, and other data formats. This paradigm represents a significant advancement in AI models.
+
+VertexAI Anthropic models support this capability by comprehending and integrating text, code, audio, images, and video. For more details, refer to the Anthropic website's https://docs.anthropic.com/en/api/messages-examples#vision[Vision] paragraph.
+
+Spring AI's `Message` interface supports multimodal AI models by introducing the Media type.
+This type contains data and information about media attachments in messages, using Spring's `org.springframework.util.MimeType` and a `java.lang.Object` for the raw media data.
+
+Below is a simple code example, demonstrating the combination of user text with an image.
+
+[source,java]
+----
+byte[] data = new ClassPathResource("/vertex-test.png").getContentAsByteArray();
+
+var userMessage = new UserMessage("Explain what do you see o this picture?",
+ List.of(new Media(MimeTypeUtils.IMAGE_PNG, data)));
+
+ChatResponse response = chatModel.call(new Prompt(List.of(userMessage)));
+----
+
+== Sample Controller
+
+https://start.spring.io/[Create] a new Spring Boot project and add the `spring-ai-vertex-ai-anthropic-spring-boot-starter` to your pom (or gradle) dependencies.
+
+Add a `application.properties` file, under the `src/main/resources` directory, to enable and configure the VertexAi chat model:
+
+[source,application.properties]
+----
+spring.ai.vertex.ai.anthropic.project-id=PROJECT_ID
+spring.ai.vertex.ai.anthropic.location=LOCATION
+spring.ai.vertex.ai.anthropic.credentials-uri=CREDENTIALS
+----
+
+This will create a `VertexAiAnthropicChatModel` implementation that you can inject into your class.
+Here is an example of a simple `@Controller` class that uses the chat model for text generations.
+
+[source,java]
+----
+@RestController
+public class ChatController {
+
+ private final VertexAiAnthropicChatModel chatModel;
+
+ @Autowired
+ public ChatController(VertexAiAnthropicChatModel chatModel) {
+ this.chatModel = chatModel;
+ }
+
+ @GetMapping("/ai/generate")
+ public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
+ return Map.of("generation", chatModel.call(message));
+ }
+
+ @GetMapping("/ai/generateStream")
+ public Flux generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
+ Prompt prompt = new Prompt(new UserMessage(message));
+ return chatModel.stream(prompt);
+ }
+}
+----
+
+== Manual Configuration
+
+The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-vertex-ai-anthropic/src/main/java/org/springframework/ai/vertexai/anthropic/VertexAiAnthropicChatModel.java[VertexAiAnthropicChatModel] implements the `ChatModel` and uses the `VertexAI` to connect to the Vertex AI Anthropic service.
+
+Add the `spring-ai-vertex-ai-anthropic` dependency to your project's Maven `pom.xml` file:
+
+[source, xml]
+----
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic
+
+----
+
+or to your Gradle `build.gradle` build file.
+
+[source,groovy]
+----
+dependencies {
+ implementation 'org.springframework.ai:spring-ai-vertex-ai-anthropic'
+}
+----
+
+TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
+
+Next, create a `VertexAiAnthropicChatModel` and use it for text generations:
+
+[source,java]
+----
+VertexAiAnthropicApi vertexAiAnthropicApi = new VertexAiAnthropicApi(projectId, location, credentials);
+
+VertexAiAnthropicChatModel chatModel = new VertexAiAnthropicChatModel(vertexAiAnthropicApi,
+ VertexAiAnthropicChatOptions.builder()
+ .withMaxTokens(100)
+ .withAnthropicVersion(VertexAiAnthropicChatModel.DEFAULT_ANTHROPIC_VERSION)
+ .withModel(VertexAiAnthropicChatModel.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .withTemperature(0.4)
+ .build());
+
+ChatResponse response = chatModel.call(
+ new Prompt("Generate the names of 5 famous pirates."));
+----
+
+The `VertexAiAnthropicChatOptions` provides the configuration information for the chat requests.
+The `VertexAiAnthropicChatOptions.Builder` is fluent options builder.
\ No newline at end of file
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc
index b754b21de23..203760865ad 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc
@@ -198,6 +198,7 @@ image::spring-ai-chat-completions-clients.jpg[align="center", width="800px"]
* xref:api/chat/azure-openai-chat.adoc[Microsoft Azure Open AI Chat Completion] (streaming & function-calling support)
* xref:api/chat/ollama-chat.adoc[Ollama Chat Completion]
* xref:api/chat/huggingface.adoc[Hugging Face Chat Completion] (no streaming support)
+* xref:api/chat/vertexai-anthropic-chat.adoc[Google Vertex AI Anthropic Chat Completion] (streaming, multi-modality & function-calling support)
* xref:api/chat/vertexai-palm2-chat.adoc[Google Vertex AI PaLM2 Chat Completion] (no streaming support)
* xref:api/chat/vertexai-gemini-chat.adoc[Google Vertex AI Gemini Chat Completion] (streaming, multi-modality & function-calling support)
* xref:api/bedrock.adoc[Amazon Bedrock]
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc
index fc389b2fdde..014a4764199 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/multimodality.adoc
@@ -71,6 +71,7 @@ Latest version of Spring AI provides multimodal support for the following Chat C
* xref:api/chat/openai-chat.adoc#_multimodal[Open AI - (GPT-4-Vision and GPT-4o models)]
* xref:api/chat/ollama-chat.adoc#_multimodal[Ollama - (LlaVa and Baklava models)]
+* xref:api/chat/vertexai-anthropic-chat.adoc#_multimodal[Vertex AI Anthropic - (claude-3-5-sonnet)]
* xref:api/chat/vertexai-gemini-chat.adoc#_multimodal[Vertex AI Gemini - (gemini-1.5-pro-001, gemini-1.5-flash-001 models)]
* xref:api/chat/anthropic-chat.adoc#_multimodal[Anthropic Claude 3]
* xref:api/chat/bedrock/bedrock-anthropic3.adoc#_multimodal[AWS Bedrock Anthropic Claude 3]
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc
index 2f29b8392bc..262be253d08 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc
@@ -251,6 +251,7 @@ The following AI Models have been tested to support List, Map and Bean structure
| xref:api/chat/azure-openai-chat.adoc[Azure OpenAI] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelIT.java[AzureOpenAiChatModelIT.java]
| xref:api/chat/mistralai-chat.adoc[Mistral AI] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelIT.java[MistralAiChatModelIT.java]
| xref:api/chat/ollama-chat.adoc[Ollama] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelIT.java[OllamaChatModelIT.java]
+| xref:api/chat/vertexai-anthropic-chat.adoc[Vertex AI Anthropic] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-vertex-ai-anthropic/src/test/java/org/springframework/ai/vertexai/anthropic/VertexAiAnthropicChatModelTest.java[VertexAiGeminiChatModelTest.java]
| xref:api/chat/vertexai-gemini-chat.adoc[Vertex AI Gemini] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java[VertexAiGeminiChatModelIT.java]
| xref:api/chat/bedrock/bedrock-anthropic.adoc[Bedrock Anthropic 2] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatModelIT.java[BedrockAnthropicChatModelIT.java]
| xref:api/chat/bedrock/bedrock-anthropic3.adoc[Bedrock Anthropic 3] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatModelIT.java[BedrockAnthropic3ChatModelIT.java]
diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml
index 7a21efbfd7e..ee020141d06 100644
--- a/spring-ai-spring-boot-autoconfigure/pom.xml
+++ b/spring-ai-spring-boot-autoconfigure/pom.xml
@@ -216,6 +216,14 @@
true
+
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic
+ ${project.parent.version}
+ true
+
+
org.springframework.ai
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfiguration.java
new file mode 100644
index 00000000000..361a0702deb
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfiguration.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallbackContext;
+import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.ai.vertexai.anthropic.api.VertexAiAnthropicApi;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Auto-configuration for Vertex AI Anthropic Chat.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@ConditionalOnClass({ VertexAiAnthropicApi.class, VertexAiAnthropicChatModel.class })
+@EnableConfigurationProperties({ VertexAiAnthropicChatProperties.class, VertexAiAnthropicConnectionProperties.class })
+public class VertexAiAnthropicAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public VertexAiAnthropicApi vertexAiAnthropicApi(VertexAiAnthropicConnectionProperties connectionProperties)
+ throws IOException {
+
+ Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!");
+ Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!");
+
+ VertexAiAnthropicApi.Builder vertexAiAnthropicApiBuilder = new VertexAiAnthropicApi.Builder()
+ .projectId(connectionProperties.getProjectId())
+ .location(connectionProperties.getLocation());
+
+ if (connectionProperties.getCredentialsUri() != null) {
+ GoogleCredentials credentials = GoogleCredentials
+ .fromStream(connectionProperties.getCredentialsUri().getInputStream());
+
+ vertexAiAnthropicApiBuilder.credentials(credentials);
+ }
+
+ return vertexAiAnthropicApiBuilder.build();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = VertexAiAnthropicChatProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public VertexAiAnthropicChatModel vertexAiAnthropicChat(VertexAiAnthropicApi vertexAiAnthropicApi,
+ VertexAiAnthropicChatProperties chatProperties, List toolFunctionCallbacks,
+ ApplicationContext context) {
+
+ FunctionCallbackContext functionCallbackContext = springAiFunctionManager(context);
+
+ return new VertexAiAnthropicChatModel(vertexAiAnthropicApi, chatProperties.getOptions(),
+ RetryUtils.DEFAULT_RETRY_TEMPLATE, functionCallbackContext, toolFunctionCallbacks);
+ }
+
+ /**
+ * Because of the OPEN_API_SCHEMA type, the FunctionCallbackContext instance must
+ * different from the other JSON schema types.
+ */
+ private FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
+ FunctionCallbackContext manager = new FunctionCallbackContext();
+ manager.setSchemaType(SchemaType.OPEN_API_SCHEMA);
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicChatProperties.java
new file mode 100644
index 00000000000..0f57d59be99
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicChatProperties.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic;
+
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Vertex AI Anthropic Chat.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@ConfigurationProperties(VertexAiAnthropicChatProperties.CONFIG_PREFIX)
+public class VertexAiAnthropicChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.anthropic.chat";
+
+ /**
+ * Vertex AI Anthropic API generative options.
+ */
+ private VertexAiAnthropicChatOptions options = VertexAiAnthropicChatOptions.builder()
+ .withTemperature(0.8f)
+ .withMaxTokens(500)
+ .withAnthropicVersion(VertexAiAnthropicChatModel.DEFAULT_ANTHROPIC_VERSION)
+ .withModel(VertexAiAnthropicChatModel.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .build();
+
+ public VertexAiAnthropicChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(VertexAiAnthropicChatOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicConnectionProperties.java
new file mode 100644
index 00000000000..0f038c99773
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicConnectionProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.io.Resource;
+
+/**
+ * Configuration properties for Vertex AI Anthropic Chat.
+ *
+ * @author Alessio Bertazzo
+ * @since 1.0.0
+ */
+@ConfigurationProperties(VertexAiAnthropicConnectionProperties.CONFIG_PREFIX)
+public class VertexAiAnthropicConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.anthropic";
+
+ /**
+ * Vertex AI Anthropic project ID.
+ */
+ private String projectId;
+
+ /**
+ * Vertex AI Anthropic location.
+ */
+ private String location;
+
+ /**
+ * URI to Vertex AI Anthropic credentials (optional)
+ */
+ private Resource credentialsUri;
+
+ public String getProjectId() {
+ return this.projectId;
+ }
+
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public Resource getCredentialsUri() {
+ return this.credentialsUri;
+ }
+
+ public void setCredentialsUri(Resource credentialsUri) {
+ this.credentialsUri = credentialsUri;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 1c74d1a0a7f..29a4f3f4fec 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -3,6 +3,7 @@ org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration
org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration
org.springframework.ai.autoconfigure.transformers.TransformersEmbeddingModelAutoConfiguration
org.springframework.ai.autoconfigure.huggingface.HuggingfaceChatAutoConfiguration
+org.springframework.ai.autoconfigure.vertexai.anthropic.VertexAiAnthropicAutoConfiguration
org.springframework.ai.autoconfigure.vertexai.palm2.VertexAiPalm2AutoConfiguration
org.springframework.ai.autoconfigure.vertexai.gemini.VertexAiGeminiAutoConfiguration
org.springframework.ai.autoconfigure.bedrock.jurrasic2.BedrockAi21Jurassic2ChatAutoConfiguration
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfigurationIT.java
new file mode 100644
index 00000000000..b1b12af0bfc
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/VertexAiAnthropicAutoConfigurationIT.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import reactor.core.publisher.Flux;
+
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_LOCATION", matches = ".*")
+public class VertexAiAnthropicAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(VertexAiAnthropicAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues(
+ "spring.ai.vertex.ai.anthropic.project-id=" + System.getenv("VERTEX_AI_ANTHROPIC_PROJECT_ID"),
+ "spring.ai.vertex.ai.anthropic.location=" + System.getenv("VERTEX_AI_ANTHROPIC_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiAnthropicAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ contextRunner.run(context -> {
+ VertexAiAnthropicChatModel chatModel = context.getBean(VertexAiAnthropicChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ contextRunner.run(context -> {
+ VertexAiAnthropicChatModel chatModel = context.getBean(VertexAiAnthropicChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList().block().stream().map(chatResponse -> {
+ return chatResponse.getResults().get(0).getOutput().getContent();
+ }).collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 00000000000..e1f9ed47f60
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionBeanIT.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic.tool;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.autoconfigure.vertexai.anthropic.VertexAiAnthropicAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import java.util.List;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_LOCATION", matches = ".*")
+class FunctionCallWithFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.vertex.ai.anthropic.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"),
+ "spring.ai.vertex.ai.anthropic.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION"))
+
+ .withConfiguration(AutoConfigurations.of(VertexAiAnthropicAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+
+ contextRunner
+ .withPropertyValues("spring.ai.vertex.ai.anthropic.chat.options.model="
+ + VertexAiAnthropicChatModel.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .run(context -> {
+
+ VertexAiAnthropicChatModel chatModel = context.getBean(VertexAiAnthropicChatModel.class);
+
+ var userMessage = new UserMessage("""
+ What's the weather like in San Francisco, Paris and in Tokyo?
+ Return the temperature in Celsius.
+ Perform multiple funciton execution if necessary.
+ """);
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ VertexAiAnthropicChatOptions.builder().withFunction("weatherFunction").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
+
+ response = chatModel.call(new Prompt(List.of(userMessage),
+ VertexAiAnthropicChatOptions.builder().withFunction("weatherFunction3").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
+
+ response = chatModel
+ .call(new Prompt(List.of(userMessage), VertexAiAnthropicChatOptions.builder().build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).doesNotContain("30", "10", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get the weather in location")
+ public Function weatherFunction() {
+ return new MockWeatherService();
+ }
+
+ // Relies on the Request's JsonClassDescription annotation to provide the
+ // function description.
+ @Bean
+ public Function weatherFunction3() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionWrapperIT.java
new file mode 100644
index 00000000000..8099e502042
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithFunctionWrapperIT.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic.tool;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.autoconfigure.vertexai.anthropic.VertexAiAnthropicAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallbackWrapper;
+import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_LOCATION", matches = ".*")
+public class FunctionCallWithFunctionWrapperIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionWrapperIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues(
+ "spring.ai.vertex.ai.anthropic.project-id=" + System.getenv("VERTEX_AI_ANTHROPIC_PROJECT_ID"),
+ "spring.ai.vertex.ai.anthropic.location=" + System.getenv("VERTEX_AI_ANTHROPIC_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiAnthropicAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ contextRunner
+ .withPropertyValues("spring.ai.vertex.ai.anthropic.chat.options.model="
+ + VertexAiAnthropicChatModel.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .run(context -> {
+
+ VertexAiAnthropicChatModel chatModel = context.getBean(VertexAiAnthropicChatModel.class);
+
+ var userMessage = new UserMessage("""
+ What's the weather like in San Francisco, Paris and in Tokyo?
+ Return the temperature in Celsius.
+ """);
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ VertexAiAnthropicChatOptions.builder().withFunction("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public FunctionCallback weatherFunctionInfo() {
+
+ return FunctionCallbackWrapper.builder(new MockWeatherService())
+ .withName("WeatherInfo")
+ .withSchemaType(SchemaType.OPEN_API_SCHEMA)
+ .withDescription("Get the current weather in a given location")
+ .build();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 00000000000..51ad3bad9c2
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/FunctionCallWithPromptFunctionIT.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic.tool;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.autoconfigure.vertexai.anthropic.VertexAiAnthropicAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallbackWrapper;
+import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatModel;
+import org.springframework.ai.vertexai.anthropic.VertexAiAnthropicChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_ANTHROPIC_LOCATION", matches = ".*")
+public class FunctionCallWithPromptFunctionIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues(
+ "spring.ai.vertex.ai.anthropic.project-id=" + System.getenv("VERTEX_AI_ANTHROPIC_PROJECT_ID"),
+ "spring.ai.vertex.ai.anthropic.location=" + System.getenv("VERTEX_AI_ANTHROPIC_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiAnthropicAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ contextRunner
+ .withPropertyValues("spring.ai.vertex.ai.anthropic.chat.options.model="
+ + VertexAiAnthropicChatModel.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .run(context -> {
+
+ VertexAiAnthropicChatModel chatModel = context.getBean(VertexAiAnthropicChatModel.class);
+
+ // var systemMessage = new SystemMessage("""
+ // Use Multi-turn function calling.
+ // Answer for all listed locations.
+ // If the information was not fetched call the function again. Repeat at
+ // most 3 times.
+ // """);
+ var userMessage = new UserMessage("""
+ What's the weather like in San Francisco, Paris and in Tokyo?
+ Return the temperature in Celsius.
+ """);
+
+ var promptOptions = VertexAiAnthropicChatOptions.builder()
+ .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
+ .withName("CurrentWeatherService")
+ .withSchemaType(SchemaType.OPEN_API_SCHEMA) // IMPORTANT!!
+ .withDescription("Get the weather in location")
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
+
+ // Verify that no function call is made.
+ response = chatModel
+ .call(new Prompt(List.of(userMessage), VertexAiAnthropicChatOptions.builder().build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getContent()).doesNotContain("30", "10", "15");
+
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/MockWeatherService.java
new file mode 100644
index 00000000000..45221196b29
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/anthropic/tool/MockWeatherService.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vertexai.anthropic.tool;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+import java.util.function.Function;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Alessio Bertazzo
+ */
+@JsonClassDescription("Get the weather in location")
+public class MockWeatherService implements Function {
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ private Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+ }
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-anthropic/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-anthropic/pom.xml
new file mode 100644
index 00000000000..86a177fb344
--- /dev/null
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-anthropic/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-vertex-ai-anthropic-spring-boot-starter
+ jar
+ Spring AI Starter - VertexAI Anthropic
+ Spring AI Vertex Anthropic AI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.ai
+ spring-ai-spring-boot-autoconfigure
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-vertex-ai-anthropic
+ ${project.parent.version}
+
+
+
+