diff --git a/models/spring-ai-watsonx-ai/pom.xml b/models/spring-ai-watsonx-ai/pom.xml
new file mode 100644
index 000000000..8cd07c4e2
--- /dev/null
+++ b/models/spring-ai-watsonx-ai/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+
+ spring-ai-watsonx-ai
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-retry
+ ${project.parent.version}
+
+
+
+ org.springframework
+ spring-webflux
+
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+ com.ibm.cloud
+ sdk-core
+ ${ibm.sdk.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+ 3.6.2
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.version}
+ test
+
+
+
+
+
diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatClient.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatClient.java
new file mode 100644
index 000000000..6ec0c5072
--- /dev/null
+++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatClient.java
@@ -0,0 +1,139 @@
+/*
+ * 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.watsonx;
+
+import java.util.List;
+import java.util.Map;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.ChatClient;
+import org.springframework.ai.chat.ChatResponse;
+import org.springframework.ai.chat.Generation;
+import org.springframework.ai.chat.StreamingChatClient;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.watsonx.api.WatsonxAiApi;
+import org.springframework.ai.watsonx.api.WatsonxAiRequest;
+import org.springframework.ai.watsonx.api.WatsonxAiResponse;
+import org.springframework.ai.watsonx.utils.MessageToPromptConverter;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ChatClient} implementation for {@literal watsonx.ai}.
+ *
+ * watsonx.ai allows developers to use large language models within a SaaS service. It
+ * supports multiple open-source models as well as IBM created models
+ * [watsonx.ai](https://www.ibm.com/products/watsonx-ai). Please refer to the watsonx.ai
+ * models for the most up-to-date information about the available models.
+ *
+ * @author Pablo Sanchidrian Herrera
+ * @author John Jario Moreno Rojas
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+public class WatsonxAiChatClient implements ChatClient, StreamingChatClient {
+
+ private final WatsonxAiApi watsonxAiApi;
+
+ private final WatsonxAiChatOptions defaultOptions;
+
+ public WatsonxAiChatClient(WatsonxAiApi watsonxAiApi) {
+ this(watsonxAiApi,
+ WatsonxAiChatOptions.builder()
+ .withTemperature(0.7f)
+ .withTopP(1.0f)
+ .withTopK(50)
+ .withDecodingMethod("greedy")
+ .withMaxNewTokens(20)
+ .withMinNewTokens(0)
+ .withRepetitionPenalty(1.0f)
+ .build());
+ }
+
+ public WatsonxAiChatClient(WatsonxAiApi watsonxAiApi, WatsonxAiChatOptions defaultOptions) {
+ Assert.notNull(watsonxAiApi, "watsonxAiApi cannot be null");
+ Assert.notNull(defaultOptions, "defaultOptions cannot be null");
+ this.watsonxAiApi = watsonxAiApi;
+ this.defaultOptions = defaultOptions;
+ }
+
+ @Override
+ public ChatResponse call(Prompt prompt) {
+
+ WatsonxAiRequest request = request(prompt);
+
+ WatsonxAiResponse response = this.watsonxAiApi.generate(request).getBody();
+ var generator = new Generation(response.results().get(0).generatedText());
+
+ generator = generator.withGenerationMetadata(
+ ChatGenerationMetadata.from(response.results().get(0).stopReason(), response.system()));
+
+ return new ChatResponse(List.of(generator));
+ }
+
+ @Override
+ public Flux stream(Prompt prompt) {
+
+ WatsonxAiRequest request = request(prompt);
+
+ Flux response = this.watsonxAiApi.generateStreaming(request);
+
+ return response.map(chunk -> {
+ Generation generation = new Generation(chunk.results().get(0).generatedText());
+ if (chunk.system() != null) {
+ generation = generation.withGenerationMetadata(
+ ChatGenerationMetadata.from(chunk.results().get(0).stopReason(), chunk.system()));
+ }
+ return new ChatResponse(List.of(generation));
+ });
+ }
+
+ public WatsonxAiRequest request(Prompt prompt) {
+
+ WatsonxAiChatOptions options = WatsonxAiChatOptions.builder().build();
+
+ if (this.defaultOptions != null) {
+ options = ModelOptionsUtils.merge(options, this.defaultOptions, WatsonxAiChatOptions.class);
+ }
+
+ if (prompt.getOptions() != null) {
+ if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
+ var updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ChatOptions.class,
+ WatsonxAiChatOptions.class);
+
+ options = ModelOptionsUtils.merge(updatedRuntimeOptions, options, WatsonxAiChatOptions.class);
+ }
+ else {
+ throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
+ + prompt.getOptions().getClass().getSimpleName());
+ }
+ }
+
+ Map parameters = options.toMap();
+
+ final String convertedPrompt = MessageToPromptConverter.create()
+ .withAssistantPrompt("")
+ .withHumanPrompt("")
+ .toPrompt(prompt.getInstructions());
+
+ return WatsonxAiRequest.builder(convertedPrompt).withParameters(parameters).build();
+ }
+
+}
\ No newline at end of file
diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java
new file mode 100644
index 000000000..149435049
--- /dev/null
+++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java
@@ -0,0 +1,286 @@
+/*
+ * 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.watsonx;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.ai.chat.prompt.ChatOptions;
+
+/**
+ * Helper class for creating watsonx.ai options.
+ *
+ * @author Pablo Sanchidrian Herrera
+ * @author John Jairo Moreno Rojas
+ * @since 1.0.0
+ * @see watsonx.ai
+ * valid Parameters and values
+ */
+// @formatter:off
+public class WatsonxAiChatOptions implements ChatOptions {
+
+ /**
+ * The temperature of the model. Increasing the temperature will
+ * make the model answer more creatively. (Default: 0.7)
+ */
+ @JsonProperty("temperature") private Float temperature;
+
+ /**
+ * Works together with top-k. A higher value (e.g., 0.95) will lead to
+ * more diverse text, while a lower value (e.g., 0.2) will generate more focused and
+ * conservative text. (Default: 1.0)
+ */
+ @JsonProperty("top_p") private Float topP;
+
+ /**
+ * Reduces the probability of generating nonsense. A higher value (e.g.
+ * 100) will give more diverse answers, while a lower value (e.g. 10) will be more
+ * conservative. (Default: 50)
+ */
+ @JsonProperty("top_k") private Integer topK;
+
+ /**
+ * Decoding is the process that a model uses to choose the tokens in the generated output.
+ * Choose one of the following decoding options:
+ *
+ * Greedy: Selects the token with the highest probability at each step of the decoding process.
+ * Greedy decoding produces output that closely matches the most common language in the model's pretraining
+ * data and in your prompt text, which is desirable in less creative or fact-based use cases. A weakness of
+ * greedy decoding is that it can cause repetitive loops in the generated output.
+ *
+ * Sampling decoding: Offers more variability in how tokens are selected.
+ * With sampling decoding, the model samples tokens, meaning the model chooses a subset of tokens,
+ * and then one token is chosen randomly from this subset to be added to the output text. Sampling adds
+ * variability and randomness to the decoding process, which can be desirable in creative use cases.
+ * However, with greater variability comes a greater risk of incorrect or nonsensical output.
+ * (Default: greedy)
+ */
+ @JsonProperty("decoding_method") private String decodingMethod;
+
+ /**
+ * Sets the limit of tokens that the LLM follow. (Default: 20)
+ */
+ @JsonProperty("max_new_tokens") private Integer maxNewTokens;
+
+ /**
+ * Sets how many tokens must the LLM generate. (Default: 0)
+ */
+ @JsonProperty("min_new_tokens") private Integer minNewTokens = 0;
+
+ /**
+ * Sets when the LLM should stop.
+ * (e.g., ["\n\n\n"]) then when the LLM generates three consecutive line breaks it will terminate.
+ * Stop sequences are ignored until after the number of tokens that are specified in the Min tokens parameter are generated.
+ */
+ @JsonProperty("stop_sequences") private List stopSequences = List.of();
+
+ /**
+ * Sets how strongly to penalize repetitions. A higher value
+ * (e.g., 1.8) will penalize repetitions more strongly, while a lower value (e.g.,
+ * 1.1) will be more lenient. (Default: 1.0)
+ */
+ @JsonProperty("repetition_penalty") private Float repetitionPenalty;
+
+ /**
+ * Produce repeatable results, set the same random seed value every time. (Default: randomly generated)
+ */
+ @JsonProperty("random_seed") private Integer randomSeed;
+
+ /**
+ * Model is the identifier of the LLM Model to be used
+ */
+ @JsonProperty("model") private String model;
+
+
+ public Float getTemperature() {
+ return temperature;
+ }
+
+ public void setTemperature(Float temperature) {
+ this.temperature = temperature;
+ }
+
+ public Float getTopP() {
+ return topP;
+ }
+
+ public void setTopP(Float topP) {
+ this.topP = topP;
+ }
+
+ public Integer getTopK() {
+ return topK;
+ }
+
+ public void setTopK(Integer topK) {
+ this.topK = topK;
+ }
+
+ public String getDecodingMethod() {
+ return decodingMethod;
+ }
+
+ public void setDecodingMethod(String decodingMethod) {
+ this.decodingMethod = decodingMethod;
+ }
+
+ public Integer getMaxNewTokens() {
+ return maxNewTokens;
+ }
+
+ public void setMaxNewTokens(Integer maxNewTokens) {
+ this.maxNewTokens = maxNewTokens;
+ }
+
+ public Integer getMinNewTokens() {
+ return minNewTokens;
+ }
+
+ public void setMinNewTokens(Integer minNewTokens) {
+ this.minNewTokens = minNewTokens;
+ }
+
+ public List getStopSequences() {
+ return stopSequences;
+ }
+
+ public void setStopSequences(List stopSequences) {
+ this.stopSequences = stopSequences;
+ }
+
+ public Float getRepetitionPenalty() {
+ return repetitionPenalty;
+ }
+
+ public void setRepetitionPenalty(Float repetitionPenalty) {
+ this.repetitionPenalty = repetitionPenalty;
+ }
+
+ public Integer getRandomSeed() {
+ return randomSeed;
+ }
+
+ public void setRandomSeed(Integer randomSeed) {
+ this.randomSeed = randomSeed;
+ }
+
+ public String getModel() {
+ return model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ WatsonxAiChatOptions options = new WatsonxAiChatOptions();
+
+ public Builder withTemperature(Float temperature) {
+ this.options.temperature = temperature;
+ return this;
+ }
+
+ public Builder withTopP(Float topP) {
+ this.options.topP = topP;
+ return this;
+ }
+
+ public Builder withTopK(Integer topK) {
+ this.options.topK = topK;
+ return this;
+ }
+
+ public Builder withDecodingMethod(String decodingMethod) {
+ this.options.decodingMethod = decodingMethod;
+ return this;
+ }
+
+ public Builder withMaxNewTokens(Integer maxNewTokens) {
+ this.options.maxNewTokens = maxNewTokens;
+ return this;
+ }
+
+ public Builder withMinNewTokens(Integer minNewTokens) {
+ this.options.minNewTokens = minNewTokens;
+ return this;
+ }
+
+ public Builder withStopSequences(List stopSequences) {
+ this.options.stopSequences = stopSequences;
+ return this;
+ }
+
+ public Builder withRepetitionPenalty(Float repetitionPenalty) {
+ this.options.repetitionPenalty = repetitionPenalty;
+ return this;
+ }
+
+ public Builder withRandomSeed(Integer randomSeed) {
+ this.options.randomSeed = randomSeed;
+ return this;
+ }
+
+ public Builder withModel(String model) {
+ this.options.model = model;
+ return this;
+ }
+
+ public WatsonxAiChatOptions build() {
+ return this.options;
+ }
+ }
+
+ /**
+ * Convert the {@link WatsonxAiChatOptions} object to a {@link Map} of key/value pairs.
+ * @return The {@link Map} of key/value pairs.
+ */
+ public Map toMap() {
+ try {
+ var json = new ObjectMapper().writeValueAsString(this);
+ return new ObjectMapper().readValue(json, new TypeReference