Streamline ChatOptions

* Surface more configuration APIs to ChatOptions
* Use abstraction in Observations directly instead of dedicated implementation
* Simplify metadata config in observations for defined models
* Improve merging of runtime and default options in OpenAI
* Fix missing option in Mistral AI

Relates to gh-1148

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2024-08-10 14:48:05 +02:00
parent af25430054
commit bf84d5945e
59 changed files with 1034 additions and 697 deletions

View File

@@ -37,6 +37,7 @@ import org.springframework.util.Assert;
* The options to be used when sending a chat request to the Anthropic API.
*
* @author Christian Tzolov
* @author Thomas Vitale
* @since 1.0.0
*/
@JsonInclude(Include.NON_NULL)
@@ -149,6 +150,7 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
}
@Override
public String getModel() {
return model;
}
@@ -157,6 +159,7 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
this.model = model;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -173,6 +176,7 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
this.metadata = metadata;
}
@Override
public List<String> getStopSequences() {
return this.stopSequences;
}
@@ -199,6 +203,7 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
this.topP = topP;
}
@Override
public Integer getTopK() {
return this.topK;
}
@@ -229,6 +234,18 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
this.functions = functions;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public AnthropicChatOptions copy() {
return fromOptions(this);

View File

@@ -38,6 +38,7 @@ import org.springframework.util.Assert;
* prompt data.
*
* @author Christian Tzolov
* @author Thomas Vitale
*/
@JsonInclude(Include.NON_NULL)
public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
@@ -108,7 +109,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* output new topics.
*/
@JsonProperty(value = "presence_penalty")
private Double presencePenalty;
private Float presencePenalty;
/**
* A value that influences the probability of generated tokens appearing based on
@@ -117,7 +118,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
* model repeating the same statements verbatim.
*/
@JsonProperty(value = "frequency_penalty")
private Double frequencyPenalty;
private Float frequencyPenalty;
/**
* The deployment name as defined in Azure Open AI Studio when creating a deployment
@@ -182,9 +183,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
}
public Builder withFrequencyPenalty(Float frequencyPenalty) {
if (frequencyPenalty != null) {
this.options.frequencyPenalty = frequencyPenalty.doubleValue();
}
this.options.frequencyPenalty = frequencyPenalty;
return this;
}
@@ -204,9 +203,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
}
public Builder withPresencePenalty(Float presencePenalty) {
if (presencePenalty != null) {
this.options.presencePenalty = presencePenalty.doubleValue();
}
this.options.presencePenalty = presencePenalty;
return this;
}
@@ -259,6 +256,7 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -291,6 +289,17 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
this.n = n;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -299,22 +308,35 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
this.stop = stop;
}
public Double getPresencePenalty() {
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
public void setPresencePenalty(Double presencePenalty) {
public void setPresencePenalty(Float presencePenalty) {
this.presencePenalty = presencePenalty;
}
public Double getFrequencyPenalty() {
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
public void setFrequencyPenalty(Double frequencyPenalty) {
public void setFrequencyPenalty(Float frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
}
@Override
@JsonIgnore
public String getModel() {
return getDeploymentName();
}
@JsonIgnore
public void setModel(String model) {
setDeploymentName(model);
}
public String getDeploymentName() {
return this.deploymentName;
}
@@ -341,17 +363,6 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
this.topP = topP;
}
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
@Override
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
@@ -378,6 +389,12 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
this.responseFormat = responseFormat;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public AzureOpenAiChatOptions copy() {
return fromOptions(this);
@@ -385,13 +402,11 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOptions) {
return builder().withDeploymentName(fromOptions.getDeploymentName())
.withFrequencyPenalty(
fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty().floatValue() : null)
.withFrequencyPenalty(fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty() : null)
.withLogitBias(fromOptions.getLogitBias())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(
fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty().floatValue() : null)
.withPresencePenalty(fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty() : null)
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.azure.openai;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.ai.embedding.EmbeddingOptions;
/**
@@ -125,10 +126,16 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public String getModel() {
return getDeploymentName();
}
@JsonIgnore
public void setModel(String model) {
setDeploymentName(model);
}
public String getUser() {
return this.user;
}

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.bedrock.anthropic;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -26,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Christian Tzolov
* @author Thomas Vitale
*/
@JsonInclude(Include.NON_NULL)
public class AnthropicChatOptions implements ChatOptions {
@@ -122,6 +124,17 @@ public class AnthropicChatOptions implements ChatOptions {
this.temperature = temperature;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getMaxTokensToSample();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setMaxTokensToSample(maxTokens);
}
public Integer getMaxTokensToSample() {
return this.maxTokensToSample;
}
@@ -148,6 +161,7 @@ public class AnthropicChatOptions implements ChatOptions {
this.topP = topP;
}
@Override
public List<String> getStopSequences() {
return this.stopSequences;
}
@@ -164,6 +178,24 @@ public class AnthropicChatOptions implements ChatOptions {
this.anthropicVersion = anthropicVersion;
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public AnthropicChatOptions copy() {
return fromOptions(this);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.bedrock.anthropic3;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -24,6 +25,7 @@ import java.util.List;
/**
* @author Ben Middleton
* @author Thomas Vitale
* @since 1.0.0
*/
@JsonInclude(Include.NON_NULL)
@@ -121,6 +123,7 @@ public class Anthropic3ChatOptions implements ChatOptions {
this.temperature = temperature;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -147,6 +150,7 @@ public class Anthropic3ChatOptions implements ChatOptions {
this.topP = topP;
}
@Override
public List<String> getStopSequences() {
return this.stopSequences;
}
@@ -163,6 +167,24 @@ public class Anthropic3ChatOptions implements ChatOptions {
this.anthropicVersion = anthropicVersion;
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public Anthropic3ChatOptions copy() {
return fromOptions(this);

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.bedrock.cohere;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -28,6 +29,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
/**
* @author Christian Tzolov
* @author Thomas Vitale
* @since 0.8.0
*/
@JsonInclude(Include.NON_NULL)
@@ -165,6 +167,7 @@ public class BedrockCohereChatOptions implements ChatOptions {
this.topK = topK;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -173,6 +176,7 @@ public class BedrockCohereChatOptions implements ChatOptions {
this.maxTokens = maxTokens;
}
@Override
public List<String> getStopSequences() {
return this.stopSequences;
}
@@ -213,6 +217,24 @@ public class BedrockCohereChatOptions implements ChatOptions {
this.truncate = truncate;
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public BedrockCohereChatOptions copy() {
return fromOptions(this);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.bedrock.cohere;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -88,11 +89,13 @@ public class BedrockCohereEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -16,14 +16,18 @@
package org.springframework.ai.bedrock.jurassic2;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.util.List;
/**
* Request body for the /complete endpoint of the Jurassic-2 API.
*
* @author Ahmed Yousri
* @author Thomas Vitale
* @since 1.0.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -75,25 +79,25 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* Stops decoding if any of the strings is generated.
*/
@JsonProperty("stopSequences")
private String[] stopSequences;
private List<String> stopSequences;
/**
* Penalty object for frequency.
*/
@JsonProperty("frequencyPenalty")
private Penalty frequencyPenalty;
private Penalty frequencyPenaltyOptions;
/**
* Penalty object for presence.
*/
@JsonProperty("presencePenalty")
private Penalty presencePenalty;
private Penalty presencePenaltyOptions;
/**
* Penalty object for count.
*/
@JsonProperty("countPenalty")
private Penalty countPenalty;
private Penalty countPenaltyOptions;
// Getters and setters
@@ -133,6 +137,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* Gets the maximum number of tokens to generate per result.
* @return The maximum number of tokens.
*/
@Override
public Integer getMaxTokens() {
return maxTokens;
}
@@ -165,6 +170,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* Gets the temperature for modifying the token sampling distribution.
* @return The temperature.
*/
@Override
public Float getTemperature() {
return temperature;
}
@@ -182,6 +188,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* mass.
* @return The topP parameter.
*/
@Override
public Float getTopP() {
return topP;
}
@@ -216,7 +223,8 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* Gets the stop sequences for stopping decoding if any of the strings is generated.
* @return The stop sequences.
*/
public String[] getStopSequences() {
@Override
public List<String> getStopSequences() {
return stopSequences;
}
@@ -224,56 +232,88 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
* Sets the stop sequences for stopping decoding if any of the strings is generated.
* @param stopSequences The stop sequences.
*/
public void setStopSequences(String[] stopSequences) {
public void setStopSequences(List<String> stopSequences) {
this.stopSequences = stopSequences;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return getFrequencyPenaltyOptions() != null ? getFrequencyPenaltyOptions().scale() : null;
}
@JsonIgnore
public void setFrequencyPenalty(Float frequencyPenalty) {
if (frequencyPenalty != null) {
setFrequencyPenaltyOptions(Penalty.builder().scale(frequencyPenalty).build());
}
}
/**
* Gets the frequency penalty object.
* @return The frequency penalty object.
*/
public Penalty getFrequencyPenalty() {
return frequencyPenalty;
public Penalty getFrequencyPenaltyOptions() {
return frequencyPenaltyOptions;
}
/**
* Sets the frequency penalty object.
* @param frequencyPenalty The frequency penalty object.
* @param frequencyPenaltyOptions The frequency penalty object.
*/
public void setFrequencyPenalty(Penalty frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
public void setFrequencyPenaltyOptions(Penalty frequencyPenaltyOptions) {
this.frequencyPenaltyOptions = frequencyPenaltyOptions;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return getPresencePenaltyOptions() != null ? getPresencePenaltyOptions().scale() : null;
}
@JsonIgnore
public void setPresencePenalty(Float presencePenalty) {
if (presencePenalty != null) {
setPresencePenaltyOptions(Penalty.builder().scale(presencePenalty).build());
}
}
/**
* Gets the presence penalty object.
* @return The presence penalty object.
*/
public Penalty getPresencePenalty() {
return presencePenalty;
public Penalty getPresencePenaltyOptions() {
return presencePenaltyOptions;
}
/**
* Sets the presence penalty object.
* @param presencePenalty The presence penalty object.
* @param presencePenaltyOptions The presence penalty object.
*/
public void setPresencePenalty(Penalty presencePenalty) {
this.presencePenalty = presencePenalty;
public void setPresencePenaltyOptions(Penalty presencePenaltyOptions) {
this.presencePenaltyOptions = presencePenaltyOptions;
}
/**
* Gets the count penalty object.
* @return The count penalty object.
*/
public Penalty getCountPenalty() {
return countPenalty;
public Penalty getCountPenaltyOptions() {
return countPenaltyOptions;
}
/**
* Sets the count penalty object.
* @param countPenalty The count penalty object.
* @param countPenaltyOptions The count penalty object.
*/
public void setCountPenalty(Penalty countPenalty) {
this.countPenalty = countPenalty;
public void setCountPenaltyOptions(Penalty countPenaltyOptions) {
this.countPenaltyOptions = countPenaltyOptions;
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
public static Builder builder() {
@@ -314,7 +354,7 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
return this;
}
public Builder withStopSequences(String[] stopSequences) {
public Builder withStopSequences(List<String> stopSequences) {
request.setStopSequences(stopSequences);
return this;
}
@@ -324,18 +364,18 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
return this;
}
public Builder withFrequencyPenalty(BedrockAi21Jurassic2ChatOptions.Penalty frequencyPenalty) {
request.setFrequencyPenalty(frequencyPenalty);
public Builder withFrequencyPenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty frequencyPenalty) {
request.setFrequencyPenaltyOptions(frequencyPenalty);
return this;
}
public Builder withPresencePenalty(BedrockAi21Jurassic2ChatOptions.Penalty presencePenalty) {
request.setPresencePenalty(presencePenalty);
public Builder withPresencePenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty presencePenalty) {
request.setPresencePenaltyOptions(presencePenalty);
return this;
}
public Builder withCountPenalty(BedrockAi21Jurassic2ChatOptions.Penalty countPenalty) {
request.setCountPenalty(countPenalty);
public Builder withCountPenaltyOptions(BedrockAi21Jurassic2ChatOptions.Penalty countPenalty) {
request.setCountPenaltyOptions(countPenalty);
return this;
}
@@ -427,9 +467,9 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withStopSequences(fromOptions.getStopSequences())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withCountPenalty(fromOptions.getCountPenalty())
.withFrequencyPenaltyOptions(fromOptions.getFrequencyPenaltyOptions())
.withPresencePenaltyOptions(fromOptions.getPresencePenaltyOptions())
.withCountPenaltyOptions(fromOptions.getCountPenaltyOptions())
.build();
}

View File

@@ -22,8 +22,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.util.List;
/**
* @author Christian Tzolov
* @author Thomas Vitale
*/
@JsonInclude(Include.NON_NULL)
public class BedrockLlamaChatOptions implements ChatOptions {
@@ -74,6 +77,7 @@ public class BedrockLlamaChatOptions implements ChatOptions {
}
@Override
public Float getTemperature() {
return this.temperature;
}
@@ -82,6 +86,7 @@ public class BedrockLlamaChatOptions implements ChatOptions {
this.temperature = temperature;
}
@Override
public Float getTopP() {
return this.topP;
}
@@ -90,6 +95,17 @@ public class BedrockLlamaChatOptions implements ChatOptions {
this.topP = topP;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getMaxGenLen();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setMaxGenLen(maxTokens);
}
public Integer getMaxGenLen() {
return this.maxGenLen;
}
@@ -100,13 +116,32 @@ public class BedrockLlamaChatOptions implements ChatOptions {
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
public String getModel() {
return null;
}
@Override
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return null;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.bedrock.titan;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -26,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Christian Tzolov
* @author Thomas Vitale
* @since 0.8.0
*/
@JsonInclude(Include.NON_NULL)
@@ -87,6 +89,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
}
@Override
public Float getTemperature() {
return temperature;
}
@@ -95,6 +98,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
this.temperature = temperature;
}
@Override
public Float getTopP() {
return topP;
}
@@ -103,6 +107,17 @@ public class BedrockTitanChatOptions implements ChatOptions {
this.topP = topP;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getMaxTokenCount();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setMaxTokenCount(maxTokens);
}
public Integer getMaxTokenCount() {
return maxTokenCount;
}
@@ -111,6 +126,7 @@ public class BedrockTitanChatOptions implements ChatOptions {
this.maxTokenCount = maxTokenCount;
}
@Override
public List<String> getStopSequences() {
return stopSequences;
}
@@ -120,12 +136,27 @@ public class BedrockTitanChatOptions implements ChatOptions {
}
@Override
public Integer getTopK() {
throw new UnsupportedOperationException("Bedrock Titan Chat does not support the 'TopK' option.");
@JsonIgnore
public String getModel() {
return null;
}
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Bedrock Titan Chat does not support the 'TopK' option.'");
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.bedrock.titan;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -64,11 +65,13 @@ public class BedrockTitanEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -77,7 +77,7 @@ class BedrockAi21Jurassic2ChatModelIT {
.applyToEmojis(false)
.build();
BedrockAi21Jurassic2ChatOptions options = new BedrockAi21Jurassic2ChatOptions.Builder()
.withPresencePenalty(penalty)
.withPresencePenaltyOptions(penalty)
.build();
UserMessage userMessage = new UserMessage("Can you express happiness using an emoji like 😄 ?");
@@ -94,7 +94,7 @@ class BedrockAi21Jurassic2ChatModelIT {
// applyToEmojis is by default true
BedrockAi21Jurassic2ChatOptions.Penalty penalty = new BedrockAi21Jurassic2ChatOptions.Penalty.Builder().build();
BedrockAi21Jurassic2ChatOptions options = new BedrockAi21Jurassic2ChatOptions.Builder()
.withPresencePenalty(penalty)
.withPresencePenaltyOptions(penalty)
.build();
UserMessage userMessage = new UserMessage("Can you express happiness using an emoji like 😄?");

View File

@@ -39,6 +39,7 @@ import org.springframework.util.Assert;
* @see FunctionCallingOptions
* @see ChatOptions
* @author Geng Rong
* @author Thomas Vitale
* @since 1.0.0 M1
*/
@JsonInclude(Include.NON_NULL)
@@ -236,6 +237,7 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
}
@Override
public String getModel() {
return this.model;
}
@@ -244,6 +246,7 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@@ -252,6 +255,7 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -268,6 +272,7 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
this.n = n;
}
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
@@ -292,6 +297,17 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
this.seed = seed;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -356,12 +372,7 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
return null;
}
@Override

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.minimax;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -70,6 +71,7 @@ public class MiniMaxEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -36,6 +36,7 @@ import org.springframework.util.Assert;
/**
* @author Ricken Bazolo
* @author Christian Tzolov
* @author Thomas Vitale
* @since 0.8.1
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -85,6 +86,13 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
*/
private @JsonProperty("response_format") ResponseFormat responseFormat;
/**
* Stop generation if this token is detected. Or if one of these tokens is detected
* when providing an array.
*/
@NestedConfigurationProperty
private @JsonProperty("stop") List<String> stop;
/**
* A list of tools the model may call. Currently, only functions are supported as a
* tool. Use this to provide a list of functions the model may generate JSON inputs
@@ -160,6 +168,11 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
return this;
}
public Builder withStop(List<String> stop) {
this.options.setStop(stop);
return this;
}
public Builder withTemperature(Float temperature) {
this.options.setTemperature(temperature);
return this;
@@ -208,6 +221,7 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
}
@Override
public String getModel() {
return this.model;
}
@@ -216,6 +230,7 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
this.model = model;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -248,6 +263,25 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
this.responseFormat = responseFormat;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
public void setStop(List<String> stop) {
this.stop = stop;
}
public void setTools(List<FunctionTool> tools) {
this.tools = tools;
}
@@ -282,17 +316,6 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
this.topP = topP;
}
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
}
@Override
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
@@ -315,6 +338,24 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
this.functions = functions;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public MistralAiChatOptions copy() {
return fromOptions(this);
@@ -328,6 +369,7 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withResponseFormat(fromOptions.getResponseFormat())
.withStop(fromOptions.getStop())
.withTools(fromOptions.getTools())
.withToolChoice(fromOptions.getToolChoice())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.mistralai;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -60,6 +61,7 @@ public class MistralAiEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -31,6 +31,7 @@ import java.util.Set;
/**
* @author Geng Rong
* @author Thomas Vitale
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MoonshotChatOptions implements ChatOptions {
@@ -229,6 +230,7 @@ public class MoonshotChatOptions implements ChatOptions {
}
@Override
public String getModel() {
return this.model;
}
@@ -237,6 +239,7 @@ public class MoonshotChatOptions implements ChatOptions {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@@ -245,6 +248,7 @@ public class MoonshotChatOptions implements ChatOptions {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -261,6 +265,7 @@ public class MoonshotChatOptions implements ChatOptions {
this.n = n;
}
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
@@ -269,6 +274,17 @@ public class MoonshotChatOptions implements ChatOptions {
this.presencePenalty = presencePenalty;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -303,6 +319,12 @@ public class MoonshotChatOptions implements ChatOptions {
this.user = user;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public MoonshotChatOptions copy() {
return builder().withModel(this.model)
@@ -402,15 +424,4 @@ public class MoonshotChatOptions implements ChatOptions {
return true;
}
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
}

View File

@@ -635,6 +635,17 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.seed = seed;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getNumPredict();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setNumPredict(maxTokens);
}
public Integer getNumPredict() {
return this.numPredict;
}
@@ -643,6 +654,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.numPredict = numPredict;
}
@Override
public Integer getTopK() {
return this.topK;
}
@@ -651,6 +663,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.topK = topK;
}
@Override
public Float getTopP() {
return this.topP;
}
@@ -683,6 +696,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.repeatLastN = repeatLastN;
}
@Override
public Float getTemperature() {
return this.temperature;
}
@@ -699,6 +713,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.repeatPenalty = repeatPenalty;
}
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
@@ -707,6 +722,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.presencePenalty = presencePenalty;
}
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@@ -747,6 +763,17 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.penalizeNewline = penalizeNewline;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -763,11 +790,6 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.truncate = truncate;
}
@Override
public Integer getDimensions() {
return null;
}
@Override
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
@@ -776,7 +798,6 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
@Override
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.functionCallbacks = functionCallbacks;
}
@Override
@@ -789,6 +810,12 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed
this.functions = functions;
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}
/**
* Convert the {@link OllamaOptions} object to a {@link Map} of key/value pairs.
* @return The {@link Map} of key/value pairs.

View File

@@ -44,16 +44,13 @@ import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
import org.springframework.ai.chat.observation.ChatModelRequestOptions;
import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion.Choice;
@@ -62,6 +59,7 @@ import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage.ChatCom
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage.MediaContent;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage.ToolCall;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.openai.metadata.OpenAiUsage;
import org.springframework.ai.openai.metadata.support.OpenAiResponseHeaderExtractor;
import org.springframework.ai.retry.RetryUtils;
@@ -214,7 +212,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.operationMetadata(buildOperationMetadata())
.provider(OpenAiApiConstants.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.build();
@@ -287,7 +285,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.operationMetadata(buildOperationMetadata())
.provider(OpenAiApiConstants.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.build();
@@ -356,9 +354,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on
return new MessageAggregator().aggregate(flux, mergedChatResponse -> {
observationContext.setResponse(mergedChatResponse);
});
return new MessageAggregator().aggregate(flux, observationContext::setResponse);
});
}
@@ -370,7 +366,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
headers.putAll(chatOptions.getHttpHeaders());
}
return CollectionUtils.toMultiValueMap(
headers.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> List.of(e.getValue()))));
headers.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> List.of(e.getValue()))));
}
private Generation buildGeneration(Choice choice, Map<String, Object> metadata) {
@@ -536,22 +532,15 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
}).toList();
}
private AiOperationMetadata buildOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OPENAI.value())
.build();
}
private ChatModelRequestOptions buildRequestOptions(OpenAiApi.ChatCompletionRequest request) {
return ChatModelRequestOptions.builder()
.model(StringUtils.hasText(request.model()) ? request.model() : "unknown")
.frequencyPenalty(request.frequencyPenalty())
.maxTokens(request.maxTokens())
.presencePenalty(request.presencePenalty())
.stopSequences(request.stop())
.temperature(request.temperature())
.topP(request.topP())
private ChatOptions buildRequestOptions(OpenAiApi.ChatCompletionRequest request) {
return ChatOptionsBuilder.builder()
.withModel(request.model())
.withFrequencyPenalty(request.frequencyPenalty())
.withMaxTokens(request.maxTokens())
.withPresencePenalty(request.presencePenalty())
.withStopSequences(request.stop())
.withTemperature(request.temperature())
.withTopP(request.topP())
.build();
}

View File

@@ -327,6 +327,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.streamOptions = (enableStreamUsage) ? StreamOptions.INCLUDE_USAGE : null;
}
@Override
public String getModel() {
return this.model;
}
@@ -335,6 +336,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@@ -367,6 +369,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.topLogprobs = topLogprobs;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -383,6 +386,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.n = n;
}
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
@@ -415,6 +419,17 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.seed = seed;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -500,6 +515,12 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.httpHeaders = httpHeaders;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
@@ -646,17 +667,6 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
return true;
}
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
@Override
public OpenAiChatOptions copy() {
return OpenAiChatOptions.fromOptions(this);

View File

@@ -31,11 +31,9 @@ import org.springframework.ai.embedding.observation.EmbeddingModelObservationCon
import org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.EmbeddingList;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.openai.metadata.OpenAiUsage;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.lang.Nullable;
@@ -151,7 +149,7 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel {
var observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(request)
.operationMetadata(buildOperationMetadata())
.provider(OpenAiApiConstants.PROVIDER_NAME)
.requestOptions(requestOptions)
.build();
@@ -195,25 +193,22 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel {
*/
private OpenAiEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions,
OpenAiEmbeddingOptions defaultOptions) {
if (runtimeOptions == null) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, EmbeddingOptions.class,
OpenAiEmbeddingOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return OpenAiEmbeddingOptions.builder()
// Handle portable embedding options
.withModel(ModelOptionsUtils.mergeOption(runtimeOptions.getModel(), defaultOptions.getModel()))
.withDimensions(
ModelOptionsUtils.mergeOption(runtimeOptions.getDimensions(), defaultOptions.getDimensions()))
.withModel(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.withDimensions(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getDimensions(),
defaultOptions.getDimensions()))
// Handle OpenAI specific embedding options
.withEncodingFormat(defaultOptions.getEncodingFormat())
.withUser(defaultOptions.getUser())
.build();
}
private AiOperationMetadata buildOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.EMBEDDING.value())
.provider(AiProvider.OPENAI.value())
.withEncodingFormat(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getEncodingFormat(),
defaultOptions.getEncodingFormat()))
.withUser(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getUser(), defaultOptions.getUser()))
.build();
}

View File

@@ -30,13 +30,12 @@ import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationContext;
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
@@ -131,7 +130,7 @@ public class OpenAiImageModel implements ImageModel {
var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt)
.operationMetadata(buildOperationMetadata())
.provider(OpenAiApiConstants.PROVIDER_NAME)
.requestOptions(requestImageOptions)
.build();
@@ -181,30 +180,28 @@ public class OpenAiImageModel implements ImageModel {
* Merge runtime and default {@link ImageOptions} to compute the final options to use
* in the request.
*/
private OpenAiImageOptions mergeOptions(ImageOptions runtimeOptions, OpenAiImageOptions defaultOptions) {
if (runtimeOptions == null) {
private OpenAiImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, OpenAiImageOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class,
OpenAiImageOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return OpenAiImageOptions.builder()
// Handle portable image options
.withModel(ModelOptionsUtils.mergeOption(runtimeOptions.getModel(), defaultOptions.getModel()))
.withN(ModelOptionsUtils.mergeOption(runtimeOptions.getN(), defaultOptions.getN()))
.withResponseFormat(ModelOptionsUtils.mergeOption(runtimeOptions.getResponseFormat(),
.withModel(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.withN(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getN(), defaultOptions.getN()))
.withResponseFormat(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getResponseFormat(),
defaultOptions.getResponseFormat()))
.withWidth(ModelOptionsUtils.mergeOption(runtimeOptions.getWidth(), defaultOptions.getWidth()))
.withHeight(ModelOptionsUtils.mergeOption(runtimeOptions.getHeight(), defaultOptions.getHeight()))
.withStyle(ModelOptionsUtils.mergeOption(runtimeOptions.getStyle(), defaultOptions.getStyle()))
.withWidth(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getWidth(), defaultOptions.getWidth()))
.withHeight(
ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getHeight(), defaultOptions.getHeight()))
.withStyle(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getStyle(), defaultOptions.getStyle()))
// Handle OpenAI specific image options
.withQuality(defaultOptions.getQuality())
.withUser(defaultOptions.getUser())
.build();
}
private AiOperationMetadata buildOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.IMAGE.value())
.provider(AiProvider.OPENAI.value())
.withQuality(
ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getQuality(), defaultOptions.getQuality()))
.withUser(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getUser(), defaultOptions.getUser()))
.build();
}

View File

@@ -1,13 +1,18 @@
package org.springframework.ai.openai.api.common;
import org.springframework.ai.observation.conventions.AiProvider;
/**
* Common value constants for OpenAI api.
*
* @author Piotr Olaszewski
* @author Thomas Vitale
* @since 1.0.0 M2
*/
public final class OpenAiApiConstants {
public static final String DEFAULT_BASE_URL = "https://api.openai.com";
public static final String PROVIDER_NAME = AiProvider.OPENAI.value();
}

View File

@@ -17,6 +17,7 @@ package org.springframework.ai.postgresml;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -132,11 +133,13 @@ public class PostgresMlEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -143,6 +143,7 @@ public class QianFanChatOptions implements ChatOptions {
}
@Override
public String getModel() {
return this.model;
}
@@ -151,6 +152,7 @@ public class QianFanChatOptions implements ChatOptions {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@@ -159,6 +161,7 @@ public class QianFanChatOptions implements ChatOptions {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -167,6 +170,7 @@ public class QianFanChatOptions implements ChatOptions {
this.maxTokens = maxTokens;
}
@Override
public Float getPresencePenalty() {
return this.presencePenalty;
}
@@ -183,6 +187,17 @@ public class QianFanChatOptions implements ChatOptions {
this.responseFormat = responseFormat;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -212,7 +227,7 @@ public class QianFanChatOptions implements ChatOptions {
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
return null;
}
@Override

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.qianfan;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -89,6 +90,7 @@ public class QianFanEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.qianfan;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.image.ImageOptions;
@@ -170,6 +171,7 @@ public class QianFanImageOptions implements ImageOptions {
}
@Override
@JsonIgnore
public String getResponseFormat() {
return null;
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.stabilityai.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.image.ImageOptions;
@@ -452,10 +453,16 @@ public class StabilityAiImageOptions implements ImageOptions {
}
@Override
@JsonIgnore
public String getStyle() {
return getStylePreset();
}
@JsonIgnore
public void setStyle(String style) {
setStylePreset(style);
}
public String getStylePreset() {
return stylePreset;
}

View File

@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
/**
* @author Christian Tzolov
* @author Thomas Vitale
* @since 0.8.1
*/
@JsonInclude(Include.NON_NULL)
@@ -173,6 +174,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
}
@Override
public List<String> getStopSequences() {
return this.stopSequences;
}
@@ -200,7 +202,6 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
}
@Override
@JsonIgnore
public Integer getTopK() {
return (this.topK != null) ? this.topK.intValue() : null;
}
@@ -222,6 +223,17 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
this.candidateCount = candidateCount;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getMaxOutputTokens();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setMaxOutputTokens(maxTokens);
}
public Integer getMaxOutputTokens() {
return this.maxOutputTokens;
}
@@ -230,6 +242,7 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
this.maxOutputTokens = maxOutputTokens;
}
@Override
public String getModel() {
return this.model;
}
@@ -254,6 +267,18 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
this.functions = functions;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;

View File

@@ -15,14 +15,18 @@
*/
package org.springframework.ai.vertexai.palm2;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import java.util.List;
/**
* @author Christian Tzolov
* @author Thomas Vitale
*/
@JsonInclude(Include.NON_NULL)
public class VertexAiPaLm2ChatOptions implements ChatOptions {
@@ -127,6 +131,36 @@ public class VertexAiPaLm2ChatOptions implements ChatOptions {
this.topK = topK;
}
@Override
@JsonIgnore
public String getModel() {
return null;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return null;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return null;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
public VertexAiPaLm2ChatOptions copy() {
return fromOptions(this);

View File

@@ -34,6 +34,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
*
* @author Pablo Sanchidrian Herrera
* @author John Jairo Moreno Rojas
* @author Thomas Vitale
* @since 1.0.0
* @see <a href=
* "https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-model-parameters.html?context=wx&audience=wdp">watsonx.ai
@@ -124,6 +125,7 @@ public class WatsonxAiChatOptions implements ChatOptions {
@JsonIgnore
private ObjectMapper mapper = new ObjectMapper();
@Override
public Float getTemperature() {
return temperature;
}
@@ -132,6 +134,7 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.temperature = temperature;
}
@Override
public Float getTopP() {
return topP;
}
@@ -140,6 +143,7 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.topP = topP;
}
@Override
public Integer getTopK() {
return topK;
}
@@ -156,6 +160,17 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.decodingMethod = decodingMethod;
}
@Override
@JsonIgnore
public Integer getMaxTokens() {
return getMaxNewTokens();
}
@JsonIgnore
public void setMaxTokens(Integer maxTokens) {
setMaxNewTokens(maxTokens);
}
public Integer getMaxNewTokens() {
return maxNewTokens;
}
@@ -172,7 +187,8 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.minNewTokens = minNewTokens;
}
public List<String> getStopSequences() {
@Override
public List<String> getStopSequences() {
return stopSequences;
}
@@ -180,7 +196,18 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.stopSequences = stopSequences;
}
public Float getRepetitionPenalty() {
@Override
@JsonIgnore
public Float getPresencePenalty() {
return getRepetitionPenalty();
}
@JsonIgnore
public void setPresencePenalty(Float presencePenalty) {
setRepetitionPenalty(presencePenalty);
}
public Float getRepetitionPenalty() {
return repetitionPenalty;
}
@@ -196,6 +223,7 @@ public class WatsonxAiChatOptions implements ChatOptions {
this.randomSeed = randomSeed;
}
@Override
public String getModel() {
return model;
}
@@ -218,6 +246,12 @@ public class WatsonxAiChatOptions implements ChatOptions {
additional.put(key, value);
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
public static Builder builder() {
return new Builder();
}

View File

@@ -36,6 +36,7 @@ import java.util.Set;
* ZhiPuAiChatOptions represents the options for the ZhiPuAiChat model.
*
* @author Geng Rong
* @author Thomas Vitale
* @since 1.0.0 M1
*/
@JsonInclude(Include.NON_NULL)
@@ -213,6 +214,7 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
}
@Override
public String getModel() {
return this.model;
}
@@ -221,6 +223,7 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.model = model;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
@@ -229,6 +232,17 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.maxTokens = maxTokens;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
@@ -314,6 +328,24 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
this.functions = functionNames;
}
@Override
@JsonIgnore
public Float getFrequencyPenalty() {
return null;
}
@Override
@JsonIgnore
public Float getPresencePenalty() {
return null;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
@@ -401,17 +433,6 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
return true;
}
@Override
@JsonIgnore
public Integer getTopK() {
throw new UnsupportedOperationException("Unimplemented method 'getTopK'");
}
@JsonIgnore
public void setTopK(Integer topK) {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
@Override
public ZhiPuAiChatOptions copy() {
return fromOptions(this);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.zhipuai;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -70,6 +71,7 @@ public class ZhiPuAiEmbeddingOptions implements EmbeddingOptions {
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.zhipuai;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.image.ImageOptions;
@@ -88,6 +89,7 @@ public class ZhiPuAiImageOptions implements ImageOptions {
}
@Override
@JsonIgnore
public Integer getN() {
return null;
}
@@ -102,21 +104,25 @@ public class ZhiPuAiImageOptions implements ImageOptions {
}
@Override
@JsonIgnore
public Integer getWidth() {
return null;
}
@Override
@JsonIgnore
public Integer getHeight() {
return null;
}
@Override
@JsonIgnore
public String getResponseFormat() {
return null;
}
@Override
@JsonIgnore
public String getStyle() {
return null;
}

View File

@@ -16,9 +16,11 @@
package org.springframework.ai.chat.observation;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.observation.ModelObservationContext;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.util.Assert;
/**
@@ -29,16 +31,16 @@ import org.springframework.util.Assert;
*/
public class ChatModelObservationContext extends ModelObservationContext<Prompt, ChatResponse> {
private final ChatModelRequestOptions requestOptions;
private final ChatOptions requestOptions;
ChatModelObservationContext(Prompt prompt, AiOperationMetadata operationMetadata,
ChatModelRequestOptions requestOptions) {
super(prompt, operationMetadata);
ChatModelObservationContext(Prompt prompt, String provider, ChatOptions requestOptions) {
super(prompt,
AiOperationMetadata.builder().operationType(AiOperationType.CHAT.value()).provider(provider).build());
Assert.notNull(requestOptions, "requestOptions cannot be null");
this.requestOptions = requestOptions;
}
public ChatModelRequestOptions getRequestOptions() {
public ChatOptions getRequestOptions() {
return this.requestOptions;
}
@@ -50,9 +52,9 @@ public class ChatModelObservationContext extends ModelObservationContext<Prompt,
private Prompt prompt;
private AiOperationMetadata operationMetadata;
private String provider;
private ChatModelRequestOptions requestOptions;
private ChatOptions requestOptions;
private Builder() {
}
@@ -62,18 +64,18 @@ public class ChatModelObservationContext extends ModelObservationContext<Prompt,
return this;
}
public Builder operationMetadata(AiOperationMetadata operationMetadata) {
this.operationMetadata = operationMetadata;
public Builder provider(String provider) {
this.provider = provider;
return this;
}
public Builder requestOptions(ChatModelRequestOptions requestOptions) {
public Builder requestOptions(ChatOptions requestOptions) {
this.requestOptions = requestOptions;
return this;
}
public ChatModelObservationContext build() {
return new ChatModelObservationContext(prompt, operationMetadata, requestOptions);
return new ChatModelObservationContext(prompt, provider, requestOptions);
}
}

View File

@@ -1,201 +0,0 @@
/*
* Copyright 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.chat.observation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.List;
/**
* Represents client-side options for chat model requests.
*
* @author Thomas Vitale
* @since 1.0.0
*/
public class ChatModelRequestOptions implements ChatOptions {
private final String model;
@Nullable
private final Float frequencyPenalty;
@Nullable
private final Integer maxTokens;
@Nullable
private final Float presencePenalty;
@Nullable
private final List<String> stopSequences;
@Nullable
private final Float temperature;
@Nullable
private final Integer topK;
@Nullable
private final Float topP;
ChatModelRequestOptions(Builder builder) {
Assert.hasText(builder.model, "model cannot be null or empty");
this.model = builder.model;
this.frequencyPenalty = builder.frequencyPenalty;
this.maxTokens = builder.maxTokens;
this.presencePenalty = builder.presencePenalty;
this.stopSequences = builder.stopSequences;
this.temperature = builder.temperature;
this.topK = builder.topK;
this.topP = builder.topP;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String model;
@Nullable
private Float frequencyPenalty;
@Nullable
private Integer maxTokens;
@Nullable
private Float presencePenalty;
@Nullable
private List<String> stopSequences;
@Nullable
private Float temperature;
@Nullable
private Integer topK;
@Nullable
private Float topP;
private Builder() {
}
public Builder model(String model) {
this.model = model;
return this;
}
public Builder frequencyPenalty(@Nullable Float frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
return this;
}
public Builder maxTokens(@Nullable Integer maxTokens) {
this.maxTokens = maxTokens;
return this;
}
public Builder presencePenalty(@Nullable Float presencePenalty) {
this.presencePenalty = presencePenalty;
return this;
}
public Builder stopSequences(@Nullable List<String> stopSequences) {
this.stopSequences = stopSequences;
return this;
}
public Builder temperature(@Nullable Float temperature) {
this.temperature = temperature;
return this;
}
public Builder topK(@Nullable Integer topK) {
this.topK = topK;
return this;
}
public Builder topP(@Nullable Float topP) {
this.topP = topP;
return this;
}
public ChatModelRequestOptions build() {
return new ChatModelRequestOptions(this);
}
}
public String getModel() {
return this.model;
}
@Nullable
public Float getFrequencyPenalty() {
return this.frequencyPenalty;
}
@Nullable
public Integer getMaxTokens() {
return this.maxTokens;
}
@Nullable
public Float getPresencePenalty() {
return this.presencePenalty;
}
@Nullable
public List<String> getStopSequences() {
return this.stopSequences;
}
@Override
@Nullable
public Float getTemperature() {
return this.temperature;
}
@Override
@Nullable
public Integer getTopK() {
return this.topK;
}
@Override
@Nullable
public Float getTopP() {
return this.topP;
}
@Override
public ChatOptions copy() {
return builder().model(this.model)
.frequencyPenalty(this.frequencyPenalty)
.maxTokens(this.maxTokens)
.presencePenalty(this.presencePenalty)
.stopSequences(this.stopSequences != null ? List.copyOf(this.stopSequences) : null)
.temperature(this.temperature)
.topK(this.topK)
.topP(this.topP)
.build();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.ai.chat.observation;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.StringJoiner;
@@ -29,6 +30,9 @@ import java.util.StringJoiner;
*/
public class DefaultChatModelObservationConvention implements ChatModelObservationConvention {
private static final KeyValue REQUEST_MODEL_NONE = KeyValue
.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, KeyValue.NONE_VALUE);
private static final KeyValue RESPONSE_MODEL_NONE = KeyValue
.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL, KeyValue.NONE_VALUE);
@@ -77,8 +81,11 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
@Override
public String getContextualName(ChatModelObservationContext context) {
return "%s %s".formatted(context.getOperationMetadata().operationType(),
context.getRequestOptions().getModel());
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
return "%s %s".formatted(context.getOperationMetadata().operationType(),
context.getRequestOptions().getModel());
}
return context.getOperationMetadata().operationType();
}
@Override
@@ -98,8 +105,11 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
}
protected KeyValue requestModel(ChatModelObservationContext context) {
return KeyValue.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL,
context.getRequestOptions().getModel());
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
return KeyValue.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL,
context.getRequestOptions().getModel());
}
return REQUEST_MODEL_NONE;
}
protected KeyValue responseModel(ChatModelObservationContext context) {

View File

@@ -16,18 +16,39 @@
package org.springframework.ai.chat.prompt;
import org.springframework.ai.model.ModelOptions;
import org.springframework.lang.Nullable;
import java.util.List;
/**
* The ChatOptions represent the common options, portable across different chat models.
*/
public interface ChatOptions extends ModelOptions {
@Nullable
String getModel();
@Nullable
Float getFrequencyPenalty();
@Nullable
Integer getMaxTokens();
@Nullable
Float getPresencePenalty();
@Nullable
List<String> getStopSequences();
@Nullable
Float getTemperature();
Float getTopP();
@Nullable
Integer getTopK();
@Nullable
Float getTopP();
ChatOptions copy();
}

View File

@@ -15,15 +15,72 @@
*/
package org.springframework.ai.chat.prompt;
import java.util.List;
public class ChatOptionsBuilder {
private class ChatOptionsImpl implements ChatOptions {
private static class DefaultChatOptions implements ChatOptions {
private String model;
private Float frequencyPenalty;
private Integer maxTokens;
private Float presencePenalty;
private List<String> stopSequences;
private Float temperature;
private Integer topK;
private Float topP;
private Integer topK;
@Override
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return frequencyPenalty;
}
public void setFrequencyPenalty(Float frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
@Override
public Float getPresencePenalty() {
return presencePenalty;
}
public void setPresencePenalty(Float presencePenalty) {
this.presencePenalty = presencePenalty;
}
@Override
public List<String> getStopSequences() {
return stopSequences;
}
public void setStopSequences(List<String> stopSequences) {
this.stopSequences = stopSequences;
}
@Override
public Float getTemperature() {
@@ -34,15 +91,6 @@ public class ChatOptionsBuilder {
this.temperature = temperature;
}
@Override
public Float getTopP() {
return topP;
}
public void setTopP(Float topP) {
this.topP = topP;
}
@Override
public Integer getTopK() {
return topK;
@@ -52,14 +100,31 @@ public class ChatOptionsBuilder {
this.topK = topK;
}
@Override
public Float getTopP() {
return topP;
}
public void setTopP(Float topP) {
this.topP = topP;
}
@Override
public ChatOptions copy() {
return builder().withTemperature(this.temperature).withTopP(this.topP).withTopK(this.topK).build();
return builder().withModel(this.model)
.withFrequencyPenalty(this.frequencyPenalty)
.withMaxTokens(this.maxTokens)
.withPresencePenalty(this.presencePenalty)
.withStopSequences(this.stopSequences != null ? List.copyOf(this.stopSequences) : null)
.withTemperature(this.temperature)
.withTopK(this.topK)
.withTopP(this.topP)
.build();
}
}
private final ChatOptionsImpl options = new ChatOptionsImpl();
private final DefaultChatOptions options = new DefaultChatOptions();
private ChatOptionsBuilder() {
}
@@ -68,13 +133,33 @@ public class ChatOptionsBuilder {
return new ChatOptionsBuilder();
}
public ChatOptionsBuilder withTemperature(Float temperature) {
options.setTemperature(temperature);
public ChatOptionsBuilder withModel(String model) {
options.setModel(model);
return this;
}
public ChatOptionsBuilder withTopP(Float topP) {
options.setTopP(topP);
public ChatOptionsBuilder withFrequencyPenalty(Float frequencyPenalty) {
options.setFrequencyPenalty(frequencyPenalty);
return this;
}
public ChatOptionsBuilder withMaxTokens(Integer maxTokens) {
options.setMaxTokens(maxTokens);
return this;
}
public ChatOptionsBuilder withPresencePenalty(Float presencePenalty) {
options.setPresencePenalty(presencePenalty);
return this;
}
public ChatOptionsBuilder withStopSequences(List<String> stop) {
options.setStopSequences(stop);
return this;
}
public ChatOptionsBuilder withTemperature(Float temperature) {
options.setTemperature(temperature);
return this;
}
@@ -83,8 +168,13 @@ public class ChatOptionsBuilder {
return this;
}
public ChatOptionsBuilder withTopP(Float topP) {
options.setTopP(topP);
return this;
}
public ChatOptions build() {
return options;
}
}
}

View File

@@ -67,7 +67,7 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode
protected KeyValue aiOperationType(EmbeddingModelObservationContext context) {
return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE,
context.getOperationType());
context.getOperationMetadata().operationType());
}
protected KeyValue aiProvider(EmbeddingModelObservationContext context) {

View File

@@ -33,9 +33,13 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
private final EmbeddingOptions requestOptions;
EmbeddingModelObservationContext(EmbeddingRequest embeddingRequest, AiOperationMetadata operationMetadata,
EmbeddingModelObservationContext(EmbeddingRequest embeddingRequest, String provider,
EmbeddingOptions requestOptions) {
super(embeddingRequest, operationMetadata);
super(embeddingRequest,
AiOperationMetadata.builder()
.operationType(AiOperationType.EMBEDDING.value())
.provider(provider)
.build());
Assert.notNull(requestOptions, "requestOptions cannot be null");
this.requestOptions = requestOptions;
}
@@ -44,10 +48,6 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
return requestOptions;
}
public String getOperationType() {
return AiOperationType.EMBEDDING.value();
}
public static Builder builder() {
return new Builder();
}
@@ -56,7 +56,7 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
private EmbeddingRequest embeddingRequest;
private AiOperationMetadata operationMetadata;
private String provider;
private EmbeddingOptions requestOptions;
@@ -68,8 +68,8 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
return this;
}
public Builder operationMetadata(AiOperationMetadata operationMetadata) {
this.operationMetadata = operationMetadata;
public Builder provider(String provider) {
this.provider = provider;
return this;
}
@@ -79,7 +79,7 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
}
public EmbeddingModelObservationContext build() {
return new EmbeddingModelObservationContext(embeddingRequest, operationMetadata, requestOptions);
return new EmbeddingModelObservationContext(embeddingRequest, provider, requestOptions);
}
}

View File

@@ -63,7 +63,7 @@ public class DefaultImageModelObservationConvention implements ImageModelObserva
protected KeyValue aiOperationType(ImageModelObservationContext context) {
return KeyValue.of(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE,
context.getOperationType());
context.getOperationMetadata().operationType());
}
protected KeyValue aiProvider(ImageModelObservationContext context) {

View File

@@ -33,9 +33,9 @@ public class ImageModelObservationContext extends ModelObservationContext<ImageP
private final ImageOptions requestOptions;
ImageModelObservationContext(ImagePrompt imagePrompt, AiOperationMetadata operationMetadata,
ImageOptions requestOptions) {
super(imagePrompt, operationMetadata);
ImageModelObservationContext(ImagePrompt imagePrompt, String provider, ImageOptions requestOptions) {
super(imagePrompt,
AiOperationMetadata.builder().operationType(AiOperationType.IMAGE.value()).provider(provider).build());
Assert.notNull(requestOptions, "requestOptions cannot be null");
this.requestOptions = requestOptions;
}
@@ -56,7 +56,7 @@ public class ImageModelObservationContext extends ModelObservationContext<ImageP
private ImagePrompt imagePrompt;
private AiOperationMetadata operationMetadata;
private String provider;
private ImageOptions requestOptions;
@@ -68,8 +68,8 @@ public class ImageModelObservationContext extends ModelObservationContext<ImageP
return this;
}
public Builder operationMetadata(AiOperationMetadata operationMetadata) {
this.operationMetadata = operationMetadata;
public Builder provider(String provider) {
this.provider = provider;
return this;
}
@@ -79,7 +79,7 @@ public class ImageModelObservationContext extends ModelObservationContext<ImageP
}
public ImageModelObservationContext build() {
return new ImageModelObservationContext(imagePrompt, operationMetadata, requestOptions);
return new ImageModelObservationContext(imagePrompt, provider, requestOptions);
}
}

View File

@@ -61,18 +61,43 @@ public class FunctionCallingOptionsBuilder {
return this;
}
public FunctionCallingOptionsBuilder withModel(String model) {
this.options.setModel(model);
return this;
}
public FunctionCallingOptionsBuilder withFrequencyPenalty(Float frequencyPenalty) {
this.options.setFrequencyPenalty(frequencyPenalty);
return this;
}
public FunctionCallingOptionsBuilder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
public FunctionCallingOptionsBuilder withPresencePenalty(Float presencePenalty) {
this.options.setPresencePenalty(presencePenalty);
return this;
}
public FunctionCallingOptionsBuilder withStopSequences(List<String> stopSequences) {
this.options.setStopSequences(stopSequences);
return this;
}
public FunctionCallingOptionsBuilder withTemperature(Float temperature) {
this.options.setTemperature(temperature);
return this;
}
public FunctionCallingOptionsBuilder withTopP(Float topP) {
this.options.setTopP(topP);
public FunctionCallingOptionsBuilder withTopK(Integer topK) {
this.options.setTopK(topK);
return this;
}
public FunctionCallingOptionsBuilder withTopK(Integer topK) {
this.options.setTopK(topK);
public FunctionCallingOptionsBuilder withTopP(Float topP) {
this.options.setTopP(topP);
return this;
}
@@ -86,12 +111,22 @@ public class FunctionCallingOptionsBuilder {
private Set<String> functions = new HashSet<>();
private String model;
private Float frequencyPenalty;
private Integer maxTokens;
private Float presencePenalty;
private List<String> stopSequences;
private Float temperature;
private Float topP;
private Integer topK;
private Float topP;
@Override
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
@@ -112,38 +147,88 @@ public class FunctionCallingOptionsBuilder {
this.functions = functions;
}
@Override
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
@Override
public Float getFrequencyPenalty() {
return frequencyPenalty;
}
public void setFrequencyPenalty(Float frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
@Override
public Float getPresencePenalty() {
return presencePenalty;
}
public void setPresencePenalty(Float presencePenalty) {
this.presencePenalty = presencePenalty;
}
@Override
public List<String> getStopSequences() {
return stopSequences;
}
public void setStopSequences(List<String> stopSequences) {
this.stopSequences = stopSequences;
}
@Override
public Float getTemperature() {
return this.temperature;
return temperature;
}
public void setTemperature(Float temperature) {
this.temperature = temperature;
}
@Override
public Float getTopP() {
return this.topP;
}
public void setTopP(Float topP) {
this.topP = topP;
}
@Override
public Integer getTopK() {
return this.topK;
return topK;
}
public void setTopK(Integer topK) {
this.topK = topK;
}
@Override
public Float getTopP() {
return topP;
}
public void setTopP(Float topP) {
this.topP = topP;
}
@Override
public ChatOptions copy() {
return new FunctionCallingOptionsBuilder().withTemperature(this.temperature)
.withTopP(this.topP)
return new FunctionCallingOptionsBuilder().withModel(this.model)
.withFrequencyPenalty(this.frequencyPenalty)
.withMaxTokens(this.maxTokens)
.withPresencePenalty(this.presencePenalty)
.withStopSequences(this.stopSequences)
.withTemperature(this.temperature)
.withTopK(this.topK)
.withTopP(this.topP)
.withFunctions(this.functions)
.withFunctionCallbacks(this.functionCallbacks)
.build();
@@ -151,4 +236,4 @@ public class FunctionCallingOptionsBuilder {
}
}
}

View File

@@ -21,10 +21,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
@@ -52,8 +50,8 @@ class ChatModelCompletionObservationFilterTests {
void whenEmptyResponseThenReturnOriginalContext() {
var expectedContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
var actualContext = observationFilter.map(expectedContext);
@@ -64,8 +62,8 @@ class ChatModelCompletionObservationFilterTests {
void whenEmptyCompletionThenReturnOriginalContext() {
var expectedContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
expectedContext.setResponse(new ChatResponse(List.of(new Generation(new AssistantMessage("")))));
var actualContext = observationFilter.map(expectedContext);
@@ -77,8 +75,8 @@ class ChatModelCompletionObservationFilterTests {
void whenCompletionWithTextThenAugmentContext() {
var originalContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
originalContext.setResponse(new ChatResponse(List.of(new Generation(new AssistantMessage("say please")),
new Generation(new AssistantMessage("seriously, say please")))));
@@ -92,11 +90,4 @@ class ChatModelCompletionObservationFilterTests {
return new Prompt("supercalifragilisticexpialidocious");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -26,8 +26,8 @@ import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.*;
import java.util.List;
@@ -70,7 +70,7 @@ class ChatModelMeterObservationHandlerTests {
assertThat(meterRegistry.get(AiObservationMetricNames.TOKEN_USAGE.value()).meters()).hasSize(3);
assertThat(meterRegistry.get(AiObservationMetricNames.TOKEN_USAGE.value())
.tag(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), AiOperationType.CHAT.value())
.tag(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.OLLAMA.value())
.tag(LowCardinalityKeyNames.AI_PROVIDER.asString(), "superprovider")
.tag(LowCardinalityKeyNames.REQUEST_MODEL.asString(), "mistral")
.tag(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), "mistral-42")
.meters()).hasSize(3);
@@ -88,8 +88,8 @@ class ChatModelMeterObservationHandlerTests {
private ChatModelObservationContext generateObservationContext() {
return ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
}
@@ -97,13 +97,6 @@ class ChatModelMeterObservationHandlerTests {
return new Prompt("hello");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
static class TestUsage implements Usage {
@Override

View File

@@ -16,10 +16,8 @@
package org.springframework.ai.chat.observation;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -35,8 +33,8 @@ class ChatModelObservationContextTests {
void whenMandatoryRequestOptionsThenReturn() {
var observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("supermodel").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("supermodel").build())
.build();
assertThat(observationContext).isNotNull();
@@ -46,7 +44,7 @@ class ChatModelObservationContextTests {
void whenRequestOptionsIsNullThenThrow() {
assertThatThrownBy(() -> ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(null)
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("requestOptions cannot be null");
@@ -56,11 +54,4 @@ class ChatModelObservationContextTests {
return new Prompt("hello");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -20,10 +20,8 @@ import io.micrometer.observation.Observation;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
@@ -51,8 +49,8 @@ class ChatModelPromptContentObservationFilterTests {
void whenEmptyPromptThenReturnOriginalContext() {
var expectedContext = ChatModelObservationContext.builder()
.prompt(new Prompt(List.of()))
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
var actualContext = observationFilter.map(expectedContext);
@@ -63,8 +61,8 @@ class ChatModelPromptContentObservationFilterTests {
void whenPromptWithTextThenAugmentContext() {
var originalContext = ChatModelObservationContext.builder()
.prompt(new Prompt("supercalifragilisticexpialidocious"))
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
var augmentedContext = observationFilter.map(originalContext);
@@ -77,8 +75,8 @@ class ChatModelPromptContentObservationFilterTests {
var originalContext = ChatModelObservationContext.builder()
.prompt(new Prompt(List.of(new SystemMessage("you're a chimney sweep"),
new UserMessage("supercalifragilisticexpialidocious"))))
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
var augmentedContext = observationFilter.map(originalContext);
@@ -87,11 +85,4 @@ class ChatModelPromptContentObservationFilterTests {
"[\"you're a chimney sweep\", \"supercalifragilisticexpialidocious\"]"));
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -1,50 +0,0 @@
/*
* Copyright 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.chat.observation;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link ChatModelRequestOptions}.
*
* @author Thomas Vitale
*/
class ChatModelRequestOptionsTests {
@Test
void whenMandatoryRequestOptionsThenReturn() {
var requestOptions = ChatModelRequestOptions.builder().model("rowena").build();
assertThat(requestOptions).isNotNull();
}
@Test
void whenModelIsNullThenThrow() {
assertThatThrownBy(() -> ChatModelRequestOptions.builder().build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("model cannot be null or empty");
}
@Test
void whenModelIsEmptyThenThrow() {
assertThatThrownBy(() -> ChatModelRequestOptions.builder().model("").build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("model cannot be null or empty");
}
}

View File

@@ -24,10 +24,8 @@ import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
@@ -50,53 +48,63 @@ class DefaultChatModelObservationConventionTests {
}
@Test
void shouldHaveContextualName() {
void contextualNameWhenModelIsDefined() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("chat mistral");
}
@Test
void contextualNameWhenModelIsNotDefined() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("chat");
}
@Test
void supportsOnlyChatModelObservationContext() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
}
@Test
void shouldHaveRequiredKeyValues() {
void shouldHaveLowCardinalityKeyValuesWhenDefined() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
KeyValue.of(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), "chat"),
KeyValue.of(LowCardinalityKeyNames.AI_PROVIDER.asString(), "ollama"),
KeyValue.of(LowCardinalityKeyNames.AI_PROVIDER.asString(), "superprovider"),
KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), "mistral"));
}
@Test
void shouldHaveOptionalKeyValues() {
void shouldHaveKeyValuesWhenDefinedAndResponse() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder()
.model("mistral")
.frequencyPenalty(0.8f)
.maxTokens(200)
.presencePenalty(1.0f)
.stopSequences(List.of("addio", "bye"))
.temperature(0.5f)
.topK(1)
.topP(0.9f)
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder()
.withModel("mistral")
.withFrequencyPenalty(0.8f)
.withMaxTokens(200)
.withPresencePenalty(1.0f)
.withStopSequences(List.of("addio", "bye"))
.withTemperature(0.5f)
.withTopK(1)
.withTopP(0.9f)
.build())
.build();
observationContext.setResponse(new ChatResponse(
@@ -125,14 +133,15 @@ class DefaultChatModelObservationConventionTests {
}
@Test
void shouldHaveMissingKeyValues() {
void shouldHaveNoneKeyValuesWhenMissing() {
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(generatePrompt())
.operationMetadata(generateOperationMetadata())
.requestOptions(ChatModelRequestOptions.builder().model("mistral").build())
.provider("superprovider")
.requestOptions(ChatOptionsBuilder.builder().build())
.build();
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext))
.contains(KeyValue.of(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), KeyValue.NONE_VALUE));
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE),
KeyValue.of(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), KeyValue.NONE_VALUE));
assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains(
KeyValue.of(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), KeyValue.NONE_VALUE),
KeyValue.of(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), KeyValue.NONE_VALUE),
@@ -152,13 +161,6 @@ class DefaultChatModelObservationConventionTests {
return new Prompt("Who let the dogs out?");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.CHAT.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
static class TestUsage implements Usage {
@Override

View File

@@ -23,9 +23,6 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
import java.util.Map;
@@ -53,7 +50,7 @@ class DefaultEmbeddingModelObservationConventionTests {
void contextualNameWhenModelIsDefined() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("embedding mistral");
@@ -63,7 +60,7 @@ class DefaultEmbeddingModelObservationConventionTests {
void contextualNameWhenModelIsNotDefined() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("embedding");
@@ -73,7 +70,7 @@ class DefaultEmbeddingModelObservationConventionTests {
void supportsOnlyEmbeddingModelObservationContext() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("supermodel").build())
.build();
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
@@ -84,12 +81,12 @@ class DefaultEmbeddingModelObservationConventionTests {
void shouldHaveLowCardinalityKeyValuesWhenDefined() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
KeyValue.of(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), "embedding"),
KeyValue.of(LowCardinalityKeyNames.AI_PROVIDER.asString(), "ollama"),
KeyValue.of(LowCardinalityKeyNames.AI_PROVIDER.asString(), "superprovider"),
KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), "mistral"));
}
@@ -97,7 +94,7 @@ class DefaultEmbeddingModelObservationConventionTests {
void shouldHaveLowCardinalityKeyValuesWhenDefinedAndResponse() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").withDimensions(1492).build())
.build();
observationContext.setResponse(new EmbeddingResponse(List.of(),
@@ -114,7 +111,7 @@ class DefaultEmbeddingModelObservationConventionTests {
void shouldHaveNoneKeyValuesWhenMissing() {
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().build())
.build();
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
@@ -130,13 +127,6 @@ class DefaultEmbeddingModelObservationConventionTests {
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.EMBEDDING.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
static class TestUsage implements Usage {
@Override

View File

@@ -26,7 +26,6 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.*;
import java.util.List;
@@ -70,7 +69,7 @@ class EmbeddingModelMeterObservationHandlerTests {
assertThat(meterRegistry.get(AiObservationMetricNames.TOKEN_USAGE.value()).meters()).hasSize(3);
assertThat(meterRegistry.get(AiObservationMetricNames.TOKEN_USAGE.value())
.tag(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), AiOperationType.EMBEDDING.value())
.tag(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.OLLAMA.value())
.tag(LowCardinalityKeyNames.AI_PROVIDER.asString(), "superprovider")
.tag(LowCardinalityKeyNames.REQUEST_MODEL.asString(), "mistral")
.tag(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), "mistral-42")
.meters()).hasSize(3);
@@ -88,7 +87,7 @@ class EmbeddingModelMeterObservationHandlerTests {
private EmbeddingModelObservationContext generateObservationContext() {
return EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
.build();
}
@@ -97,13 +96,6 @@ class EmbeddingModelMeterObservationHandlerTests {
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.EMBEDDING.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
static class TestUsage implements Usage {
@Override

View File

@@ -18,9 +18,6 @@ package org.springframework.ai.embedding.observation;
import org.junit.jupiter.api.Test;
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
@@ -38,7 +35,7 @@ class EmbeddingModelObservationContextTests {
void whenMandatoryRequestOptionsThenReturn() {
var observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("supermodel").build())
.build();
@@ -49,7 +46,7 @@ class EmbeddingModelObservationContextTests {
void whenRequestOptionsIsNullThenThrow() {
assertThatThrownBy(() -> EmbeddingModelObservationContext.builder()
.embeddingRequest(generateEmbeddingRequest())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(null)
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("requestOptions cannot be null");
@@ -59,11 +56,4 @@ class EmbeddingModelObservationContextTests {
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.EMBEDDING.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -20,10 +20,7 @@ import io.micrometer.observation.Observation;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiObservationAttributes;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,7 +42,7 @@ class DefaultImageModelObservationConventionTests {
void contextualNameWhenModelIsDefined() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image mistral");
@@ -55,7 +52,7 @@ class DefaultImageModelObservationConventionTests {
void contextualNameWhenModelIsNotDefined() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().build())
.build();
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("image");
@@ -65,7 +62,7 @@ class DefaultImageModelObservationConventionTests {
void supportsOnlyImageModelObservationContext() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
@@ -76,12 +73,12 @@ class DefaultImageModelObservationConventionTests {
void shouldHaveLowCardinalityKeyValuesWhenDefined() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
KeyValue.of(AiObservationAttributes.AI_OPERATION_TYPE.value(), "image"),
KeyValue.of(AiObservationAttributes.AI_PROVIDER.value(), "ollama"),
KeyValue.of(AiObservationAttributes.AI_PROVIDER.value(), "superprovider"),
KeyValue.of(AiObservationAttributes.REQUEST_MODEL.value(), "mistral"));
}
@@ -89,7 +86,7 @@ class DefaultImageModelObservationConventionTests {
void shouldHaveHighCardinalityKeyValuesWhenDefined() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder()
.withModel("mistral")
.withN(1)
@@ -110,7 +107,7 @@ class DefaultImageModelObservationConventionTests {
void shouldHaveNoneKeyValuesWhenMissing() {
ImageModelObservationContext observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().build())
.build();
@@ -126,11 +123,4 @@ class DefaultImageModelObservationConventionTests {
return new ImagePrompt("here comes the sun");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.IMAGE.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}
}

View File

@@ -18,9 +18,6 @@ package org.springframework.ai.image.observation;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -36,7 +33,7 @@ class ImageModelObservationContextTests {
void whenMandatoryRequestOptionsThenReturn() {
var observationContext = ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("supersun").build())
.build();
@@ -47,7 +44,7 @@ class ImageModelObservationContextTests {
void whenRequestOptionsIsNullThenThrow() {
assertThatThrownBy(() -> ImageModelObservationContext.builder()
.imagePrompt(generateImagePrompt())
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(null)
.build()).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("requestOptions cannot be null");
@@ -57,11 +54,4 @@ class ImageModelObservationContextTests {
return new ImagePrompt("here comes the sun");
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.IMAGE.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -21,10 +21,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImageMessage;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.observation.AiOperationMetadata;
import org.springframework.ai.observation.conventions.AiObservationAttributes;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import java.util.List;
@@ -51,7 +48,7 @@ class ImageModelPromptContentObservationFilterTests {
void whenEmptyPromptThenReturnOriginalContext() {
var expectedContext = ImageModelObservationContext.builder()
.imagePrompt(new ImagePrompt(""))
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
var actualContext = observationFilter.map(expectedContext);
@@ -63,7 +60,7 @@ class ImageModelPromptContentObservationFilterTests {
void whenPromptWithTextThenAugmentContext() {
var originalContext = ImageModelObservationContext.builder()
.imagePrompt(new ImagePrompt("supercalifragilisticexpialidocious"))
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
var augmentedContext = observationFilter.map(originalContext);
@@ -77,7 +74,7 @@ class ImageModelPromptContentObservationFilterTests {
var originalContext = ImageModelObservationContext.builder()
.imagePrompt(new ImagePrompt(List.of(new ImageMessage("you're a chimney sweep"),
new ImageMessage("supercalifragilisticexpialidocious"))))
.operationMetadata(generateOperationMetadata())
.provider("superprovider")
.requestOptions(ImageOptionsBuilder.builder().withModel("mistral").build())
.build();
var augmentedContext = observationFilter.map(originalContext);
@@ -87,11 +84,4 @@ class ImageModelPromptContentObservationFilterTests {
"[\"you're a chimney sweep\", \"supercalifragilisticexpialidocious\"]"));
}
private AiOperationMetadata generateOperationMetadata() {
return AiOperationMetadata.builder()
.operationType(AiOperationType.IMAGE.value())
.provider(AiProvider.OLLAMA.value())
.build();
}
}

View File

@@ -97,7 +97,7 @@ The prefix `spring.ai.mistralai.chat` is the property prefix that lets you confi
| spring.ai.mistralai.chat.options.maxTokens | The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. | -
| spring.ai.mistralai.chat.options.safePrompt | Indicates whether to inject a security prompt before all conversations. | false
| spring.ai.mistralai.chat.options.randomSeed | This feature is in Beta. If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return the same result. | -
| spring.ai.mistralai.chat.options.stop | Up to 4 sequences where the API will stop generating further tokens. | -
| spring.ai.mistralai.chat.options.stop | Stop generation if this token is detected. Or if one of these tokens is detected when providing an array. | -
| spring.ai.mistralai.chat.options.topP | An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. | -
| spring.ai.mistralai.chat.options.responseFormat | An object specifying the format that the model must output. Setting to `{ "type": "json_object" }` enables JSON mode, which guarantees the message the model generates is valid JSON.| -
| spring.ai.mistralai.chat.options.tools | A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. | -

View File

@@ -15,6 +15,13 @@ The following are the vector stores that currently don't support the `initialize
2. Pinecone
3. Weaviate
* In Bedrock Jurassic 2, the chat options `countPenalty`, `frequencyPenalty`, and `presencePenalty`
have been renamed to `countPenaltyOptions`, `frequencyPenaltyOptions`, and `presencePenaltyOptions`.
Furthermore, the type of the chat option `stopSequences` have been changed from `String[]` to `List<String>`.
* In Azure OpenAI, the type of the chat options `frequencyPenalty` and `presencePenalty`
has been changed from `Double` to `Float`, consistently with all the other implementations.
== Upgrading to 1.0.0.M1
On our march to release 1.0.0 M1 we have made several breaking changes. Apologies, it is for the best!

View File

@@ -205,13 +205,7 @@ public class ZhiPuAiPropertiesTests {
"spring.ai.zhipuai.base-url=TEST_BASE_URL",
"spring.ai.zhipuai.chat.options.model=MODEL_XYZ",
"spring.ai.zhipuai.chat.options.frequencyPenalty=-1.5",
"spring.ai.zhipuai.chat.options.logitBias.myTokenId=-5",
"spring.ai.zhipuai.chat.options.maxTokens=123",
"spring.ai.zhipuai.chat.options.n=10",
"spring.ai.zhipuai.chat.options.presencePenalty=0",
"spring.ai.zhipuai.chat.options.responseFormat.type=json",
"spring.ai.zhipuai.chat.options.seed=66",
"spring.ai.zhipuai.chat.options.stop=boza,koza",
"spring.ai.zhipuai.chat.options.temperature=0.55",
"spring.ai.zhipuai.chat.options.topP=0.56",