diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java index d0efd22cd..48ec23816 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java @@ -23,6 +23,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; * The configuration information for the embedding requests. * * @author Christian Tzolov + * @author Thomas Vitale * @since 0.8.0 */ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions { @@ -123,6 +124,11 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions { } + @Override + public String getModel() { + return getDeploymentName(); + } + public String getUser() { return this.user; } @@ -147,6 +153,7 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions { this.inputType = inputType; } + @Override public Integer getDimensions() { return this.dimensions; } diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingOptions.java index 420aff28d..04a2b37d6 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereEmbeddingOptions.java @@ -25,6 +25,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; /** * @author Christian Tzolov + * @author Thomas Vitale */ @JsonInclude(Include.NON_NULL) public class BedrockCohereEmbeddingOptions implements EmbeddingOptions { @@ -86,4 +87,14 @@ public class BedrockCohereEmbeddingOptions implements EmbeddingOptions { this.truncate = truncate; } + @Override + public String getModel() { + return null; + } + + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingOptions.java index d2770f245..51a957b71 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanEmbeddingOptions.java @@ -24,6 +24,7 @@ import org.springframework.util.Assert; /** * @author Wei Jiang + * @author Thomas Vitale */ @JsonInclude(Include.NON_NULL) public class BedrockTitanEmbeddingOptions implements EmbeddingOptions { @@ -62,4 +63,14 @@ public class BedrockTitanEmbeddingOptions implements EmbeddingOptions { this.inputType = inputType; } + @Override + public String getModel() { + return null; + } + + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingOptions.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingOptions.java index 37a043fc3..15143b1c8 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingOptions.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingOptions.java @@ -24,6 +24,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; * This class represents the options for MiniMax embedding. * * @author Geng Rong + * @author Thomas Vitale * @since 1.0.0 M1 */ @JsonInclude(Include.NON_NULL) @@ -59,6 +60,7 @@ public class MiniMaxEmbeddingOptions implements EmbeddingOptions { } + @Override public String getModel() { return this.model; } @@ -67,4 +69,9 @@ public class MiniMaxEmbeddingOptions implements EmbeddingOptions { this.model = model; } + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiEmbeddingOptions.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiEmbeddingOptions.java index 1603ac186..8f66999e9 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiEmbeddingOptions.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiEmbeddingOptions.java @@ -22,6 +22,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; /** * @author Ricken Bazolo + * @author Thomas Vitale * @since 0.8.1 */ @JsonInclude(Include.NON_NULL) @@ -41,6 +42,7 @@ public class MistralAiEmbeddingOptions implements EmbeddingOptions { return new Builder(); } + @Override public String getModel() { return this.model; } @@ -57,6 +59,11 @@ public class MistralAiEmbeddingOptions implements EmbeddingOptions { this.encodingFormat = encodingFormat; } + @Override + public Integer getDimensions() { + return null; + } + public static class Builder { protected MistralAiEmbeddingOptions options; diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java index 0cc2dfcbf..4485be23f 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java @@ -498,6 +498,7 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed // ------------------- // Getters and Setters // ------------------- + @Override public String getModel() { return model; } @@ -762,6 +763,11 @@ public class OllamaOptions implements FunctionCallingOptions, ChatOptions, Embed this.truncate = truncate; } + @Override + public Integer getDimensions() { + return null; + } + @Override public List getFunctionCallbacks() { return this.functionCallbacks; diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingModel.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingModel.java index 5ec103d5d..2b61674c2 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingModel.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingModel.java @@ -30,7 +30,6 @@ import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObserva import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation; import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext; -import org.springframework.ai.embedding.observation.EmbeddingModelRequestOptions; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.observation.AiOperationMetadata; import org.springframework.ai.observation.conventions.AiOperationType; @@ -39,9 +38,9 @@ import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi.EmbeddingList; import org.springframework.ai.openai.metadata.OpenAiUsage; import org.springframework.ai.retry.RetryUtils; +import org.springframework.lang.Nullable; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import java.util.List; @@ -126,7 +125,7 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel { */ public OpenAiEmbeddingModel(OpenAiApi openAiApi, MetadataMode metadataMode, OpenAiEmbeddingOptions options, RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { - Assert.notNull(openAiApi, "OpenAiService must not be null"); + Assert.notNull(openAiApi, "openAiApi must not be null"); Assert.notNull(metadataMode, "metadataMode must not be null"); Assert.notNull(options, "options must not be null"); Assert.notNull(retryTemplate, "retryTemplate must not be null"); @@ -147,12 +146,13 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel { @Override public EmbeddingResponse call(EmbeddingRequest request) { - org.springframework.ai.openai.api.OpenAiApi.EmbeddingRequest> apiRequest = createRequest(request); + OpenAiEmbeddingOptions requestOptions = mergeOptions(request.getOptions(), this.defaultOptions); + OpenAiApi.EmbeddingRequest> apiRequest = createRequest(request, requestOptions); var observationContext = EmbeddingModelObservationContext.builder() .embeddingRequest(request) .operationMetadata(buildOperationMetadata()) - .requestOptions(buildRequestOptions(apiRequest)) + .requestOptions(requestOptions) .build(); return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION @@ -183,21 +183,31 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel { }); } - @SuppressWarnings("unchecked") - private OpenAiApi.EmbeddingRequest> createRequest(EmbeddingRequest request) { - org.springframework.ai.openai.api.OpenAiApi.EmbeddingRequest> apiRequest = (this.defaultOptions != null) - ? new org.springframework.ai.openai.api.OpenAiApi.EmbeddingRequest<>(request.getInstructions(), - this.defaultOptions.getModel(), this.defaultOptions.getEncodingFormat(), - this.defaultOptions.getDimensions(), this.defaultOptions.getUser()) - : new org.springframework.ai.openai.api.OpenAiApi.EmbeddingRequest<>(request.getInstructions(), - OpenAiApi.DEFAULT_EMBEDDING_MODEL); + private OpenAiApi.EmbeddingRequest> createRequest(EmbeddingRequest request, + OpenAiEmbeddingOptions requestOptions) { + return new OpenAiApi.EmbeddingRequest<>(request.getInstructions(), requestOptions.getModel(), + requestOptions.getEncodingFormat(), requestOptions.getDimensions(), requestOptions.getUser()); + } - if (request.getOptions() != null && !EmbeddingOptions.EMPTY.equals(request.getOptions())) { - apiRequest = ModelOptionsUtils.merge(request.getOptions(), apiRequest, - org.springframework.ai.openai.api.OpenAiApi.EmbeddingRequest.class); + /** + * Merge runtime and default {@link EmbeddingOptions} to compute the final options to + * use in the request. + */ + private OpenAiEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions, + OpenAiEmbeddingOptions defaultOptions) { + if (runtimeOptions == null) { + return defaultOptions; } - return apiRequest; + return OpenAiEmbeddingOptions.builder() + // Handle portable embedding options + .withModel(ModelOptionsUtils.mergeOption(runtimeOptions.getModel(), defaultOptions.getModel())) + .withDimensions( + ModelOptionsUtils.mergeOption(runtimeOptions.getDimensions(), defaultOptions.getDimensions())) + // Handle OpenAI specific embedding options + .withEncodingFormat(defaultOptions.getEncodingFormat()) + .withUser(defaultOptions.getUser()) + .build(); } private AiOperationMetadata buildOperationMetadata() { @@ -207,14 +217,6 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel { .build(); } - private EmbeddingModelRequestOptions buildRequestOptions(OpenAiApi.EmbeddingRequest> request) { - return EmbeddingModelRequestOptions.builder() - .model(StringUtils.hasText(request.model()) ? request.model() : "unknown") - .dimensions(request.dimensions()) - .encodingFormat(request.encodingFormat()) - .build(); - } - /** * Use the provided convention for reporting observation data * @param observationConvention The provided convention diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingOptions.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingOptions.java index b82594987..dbd0a2979 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingOptions.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingOptions.java @@ -85,6 +85,7 @@ public class OpenAiEmbeddingOptions implements EmbeddingOptions { } + @Override public String getModel() { return this.model; } @@ -101,6 +102,7 @@ public class OpenAiEmbeddingOptions implements EmbeddingOptions { this.encodingFormat = encodingFormat; } + @Override public Integer getDimensions() { return this.dimensions; } diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiImageOptions.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiImageOptions.java index 9b7e30e82..949614dae 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiImageOptions.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiImageOptions.java @@ -190,7 +190,18 @@ public class OpenAiImageOptions implements ImageOptions { @Override public Integer getWidth() { - return this.width; + if (this.width != null) { + return this.width; + } + else if (this.size != null) { + try { + return Integer.parseInt(this.size.split("x")[0]); + } + catch (NumberFormatException ex) { + return null; + } + } + return null; } public void setWidth(Integer width) { @@ -200,7 +211,18 @@ public class OpenAiImageOptions implements ImageOptions { @Override public Integer getHeight() { - return this.height; + if (this.height != null) { + return this.height; + } + else if (this.size != null) { + try { + return Integer.parseInt(this.size.split("x")[1]); + } + catch (NumberFormatException ex) { + return null; + } + } + return null; } public void setHeight(Integer height) { @@ -230,7 +252,6 @@ public class OpenAiImageOptions implements ImageOptions { } public String getSize() { - if (this.size != null) { return this.size; } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiImageOptionsTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiImageOptionsTests.java new file mode 100644 index 000000000..7083d7980 --- /dev/null +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiImageOptionsTests.java @@ -0,0 +1,76 @@ +/* + * 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.openai; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link OpenAiImageOptions}. + * + * @author Thomas Vitale + */ +class OpenAiImageOptionsTests { + + @Test + void whenImageDimensionsAreAllUnset() { + OpenAiImageOptions options = new OpenAiImageOptions(); + assertThat(options.getHeight()).isEqualTo(null); + assertThat(options.getWidth()).isEqualTo(null); + assertThat(options.getSize()).isEqualTo(null); + } + + @Test + void whenSizeIsSet() { + OpenAiImageOptions options = new OpenAiImageOptions(); + options.setSize("1920x1080"); + assertThat(options.getHeight()).isEqualTo(1080); + assertThat(options.getWidth()).isEqualTo(1920); + assertThat(options.getSize()).isEqualTo("1920x1080"); + } + + @Test + void whenWidthAndHeightAreSet() { + OpenAiImageOptions options = new OpenAiImageOptions(); + options.setWidth(1920); + options.setHeight(1080); + assertThat(options.getHeight()).isEqualTo(1080); + assertThat(options.getWidth()).isEqualTo(1920); + assertThat(options.getSize()).isEqualTo("1920x1080"); + } + + @Test + void whenWidthIsSet() { + OpenAiImageOptions options = new OpenAiImageOptions(); + options.setWidth(1920); + assertThat(options.getHeight()).isEqualTo(null); + assertThat(options.getWidth()).isEqualTo(1920); + // This is because "setWidth()" computes "size" without checking for null values. + assertThat(options.getSize()).isEqualTo("1920xnull"); + } + + @Test + void whenHeightIsSet() { + OpenAiImageOptions options = new OpenAiImageOptions(); + options.setHeight(1080); + assertThat(options.getHeight()).isEqualTo(1080); + assertThat(options.getWidth()).isEqualTo(null); + // This is because "setHeight()" computes "size" without checking for null values. + assertThat(options.getSize()).isEqualTo("nullx1080"); + } + +} diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/embedding/OpenAiEmbeddingModelObservationIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/embedding/OpenAiEmbeddingModelObservationIT.java index 3b4bc8070..6a3e4d036 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/embedding/OpenAiEmbeddingModelObservationIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/embedding/OpenAiEmbeddingModelObservationIT.java @@ -84,7 +84,6 @@ public class OpenAiEmbeddingModelObservationIT { OpenAiApi.EmbeddingModel.TEXT_EMBEDDING_3_SMALL.getValue()) .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString(), "1536") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_EMBEDDING_ENCODING_FORMAT.asString(), "float") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(responseMetadata.getUsage().getPromptTokens())) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), diff --git a/models/spring-ai-postgresml/src/main/java/org/springframework/ai/postgresml/PostgresMlEmbeddingOptions.java b/models/spring-ai-postgresml/src/main/java/org/springframework/ai/postgresml/PostgresMlEmbeddingOptions.java index 220d5d283..c7059f153 100644 --- a/models/spring-ai-postgresml/src/main/java/org/springframework/ai/postgresml/PostgresMlEmbeddingOptions.java +++ b/models/spring-ai-postgresml/src/main/java/org/springframework/ai/postgresml/PostgresMlEmbeddingOptions.java @@ -28,6 +28,7 @@ import org.springframework.ai.postgresml.PostgresMlEmbeddingModel.VectorType; /** * @author Christian Tzolov + * @author Thomas Vitale */ @JsonInclude(Include.NON_NULL) public class PostgresMlEmbeddingOptions implements EmbeddingOptions { @@ -130,4 +131,14 @@ public class PostgresMlEmbeddingOptions implements EmbeddingOptions { this.metadataMode = metadataMode; } + @Override + public String getModel() { + return null; + } + + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanEmbeddingOptions.java b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanEmbeddingOptions.java index c4dedc384..48864e2ad 100644 --- a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanEmbeddingOptions.java +++ b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanEmbeddingOptions.java @@ -24,6 +24,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; * This class represents the options for QianFan embedding. * * @author Geng Rong + * @author Thomas Vitale * @since 1.0 */ @JsonInclude(Include.NON_NULL) @@ -70,6 +71,7 @@ public class QianFanEmbeddingOptions implements EmbeddingOptions { } + @Override public String getModel() { return this.model; } @@ -86,4 +88,9 @@ public class QianFanEmbeddingOptions implements EmbeddingOptions { this.user = user; } + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/multimodal/VertexAiMultimodalEmbeddingOptions.java b/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/multimodal/VertexAiMultimodalEmbeddingOptions.java index 1e6bc8802..78a75fef2 100644 --- a/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/multimodal/VertexAiMultimodalEmbeddingOptions.java +++ b/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/multimodal/VertexAiMultimodalEmbeddingOptions.java @@ -168,6 +168,7 @@ public class VertexAiMultimodalEmbeddingOptions implements EmbeddingOptions { } + @Override public String getModel() { return this.model; } @@ -176,6 +177,7 @@ public class VertexAiMultimodalEmbeddingOptions implements EmbeddingOptions { this.model = model; } + @Override public Integer getDimensions() { return this.dimensions; } diff --git a/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingOptions.java b/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingOptions.java index a7b2e1402..fe08b2d4b 100644 --- a/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingOptions.java +++ b/models/spring-ai-vertex-ai-embedding/src/main/java/org/springframework/ai/vertexai/embedding/text/VertexAiTextEmbeddingOptions.java @@ -183,6 +183,7 @@ public class VertexAiTextEmbeddingOptions implements EmbeddingOptions { return this; } + @Override public String getModel() { return this.model; } @@ -199,6 +200,7 @@ public class VertexAiTextEmbeddingOptions implements EmbeddingOptions { this.taskType = taskType; } + @Override public Integer getDimensions() { return this.dimensions; } diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiEmbeddingOptions.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiEmbeddingOptions.java index e36a1c4c9..388384f23 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiEmbeddingOptions.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiEmbeddingOptions.java @@ -24,6 +24,7 @@ import org.springframework.ai.embedding.EmbeddingOptions; * The ZhiPuAiEmbeddingOptions class represents the options for ZhiPuAI embedding. * * @author Geng Rong + * @author Thomas Vitale * @since 1.0.0 M1 */ @JsonInclude(Include.NON_NULL) @@ -59,6 +60,7 @@ public class ZhiPuAiEmbeddingOptions implements EmbeddingOptions { } + @Override public String getModel() { return this.model; } @@ -67,4 +69,9 @@ public class ZhiPuAiEmbeddingOptions implements EmbeddingOptions { this.model = model; } + @Override + public Integer getDimensions() { + return null; + } + } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/DocumentEmbeddingRequest.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/DocumentEmbeddingRequest.java index c68a418be..8227b2910 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/embedding/DocumentEmbeddingRequest.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/DocumentEmbeddingRequest.java @@ -25,6 +25,7 @@ import org.springframework.ai.model.ModelRequest; * Represents a request to embed a list of documents. * * @author Christian Tzolov + * @author Thomas Vitale * @since 1.0.0 */ public class DocumentEmbeddingRequest implements ModelRequest> { @@ -34,11 +35,11 @@ public class DocumentEmbeddingRequest implements ModelRequest> { private final EmbeddingOptions options; public DocumentEmbeddingRequest(Document... inputs) { - this(Arrays.asList(inputs), EmbeddingOptions.EMPTY); + this(Arrays.asList(inputs), EmbeddingOptionsBuilder.builder().build()); } public DocumentEmbeddingRequest(List inputs) { - this(inputs, EmbeddingOptions.EMPTY); + this(inputs, EmbeddingOptionsBuilder.builder().build()); } public DocumentEmbeddingRequest(List inputs, EmbeddingOptions options) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingModel.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingModel.java index 66a972f62..a40ee22a6 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingModel.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingModel.java @@ -53,7 +53,7 @@ public interface EmbeddingModel extends Model> embed(List texts) { Assert.notNull(texts, "Texts must not be null"); - return this.call(new EmbeddingRequest(texts, EmbeddingOptions.EMPTY)) + return this.call(new EmbeddingRequest(texts, EmbeddingOptionsBuilder.builder().build())) .getResults() .stream() .map(Embedding::getOutput) @@ -67,7 +67,7 @@ public interface EmbeddingModel extends Model texts) { Assert.notNull(texts, "Texts must not be null"); - return this.call(new EmbeddingRequest(texts, EmbeddingOptions.EMPTY)); + return this.call(new EmbeddingRequest(texts, EmbeddingOptionsBuilder.builder().build())); } /** diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptions.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptions.java index 1152248e0..3fac81190 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptions.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptions.java @@ -16,16 +16,24 @@ package org.springframework.ai.embedding; import org.springframework.ai.model.ModelOptions; +import org.springframework.lang.Nullable; /** * @author Christian Tzolov + * @author Thomas Vitale */ public interface EmbeddingOptions extends ModelOptions { - class EmptyEmbeddingOptions implements EmbeddingOptions { + /** + * Use the {@link EmbeddingOptionsBuilder} instead. + */ + @Deprecated(since = "1.0.0", forRemoval = true) + EmbeddingOptions EMPTY = EmbeddingOptionsBuilder.builder().build(); - } + @Nullable + String getModel(); - EmbeddingOptions EMPTY = new EmptyEmbeddingOptions(); + @Nullable + Integer getDimensions(); } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptionsBuilder.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptionsBuilder.java new file mode 100644 index 000000000..ab13dff1a --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/EmbeddingOptionsBuilder.java @@ -0,0 +1,73 @@ +/* + * 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.embedding; + +/** + * @author Thomas Vitale + * @since 1.0.0 + */ +public class EmbeddingOptionsBuilder { + + private final DefaultEmbeddingOptions embeddingOptions = new DefaultEmbeddingOptions(); + + private EmbeddingOptionsBuilder() { + } + + public static EmbeddingOptionsBuilder builder() { + return new EmbeddingOptionsBuilder(); + } + + public EmbeddingOptionsBuilder withModel(String model) { + embeddingOptions.setModel(model); + return this; + } + + public EmbeddingOptionsBuilder withDimensions(Integer dimensions) { + embeddingOptions.setDimensions(dimensions); + return this; + } + + public EmbeddingOptions build() { + return embeddingOptions; + } + + private static class DefaultEmbeddingOptions implements EmbeddingOptions { + + private String model; + + private Integer dimensions; + + @Override + public String getModel() { + return this.model; + } + + public void setModel(String model) { + this.model = model; + } + + @Override + public Integer getDimensions() { + return this.dimensions; + } + + public void setDimensions(Integer dimensions) { + this.dimensions = dimensions; + } + + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConvention.java index b2103b155..1331b2027 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConvention.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConvention.java @@ -27,6 +27,9 @@ import org.springframework.util.StringUtils; */ public class DefaultEmbeddingModelObservationConvention implements EmbeddingModelObservationConvention { + private static final KeyValue REQUEST_MODEL_NONE = KeyValue + .of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, KeyValue.NONE_VALUE); + private static final KeyValue RESPONSE_MODEL_NONE = KeyValue .of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL, KeyValue.NONE_VALUE); @@ -34,10 +37,6 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS, KeyValue.NONE_VALUE); - private static final KeyValue REQUEST_EMBEDDING_ENCODING_FORMAT_NONE = KeyValue.of( - EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_ENCODING_FORMAT, - KeyValue.NONE_VALUE); - private static final KeyValue USAGE_INPUT_TOKENS_NONE = KeyValue .of(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS, KeyValue.NONE_VALUE); @@ -53,8 +52,11 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode @Override public String getContextualName(EmbeddingModelObservationContext 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 @@ -65,7 +67,7 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode protected KeyValue aiOperationType(EmbeddingModelObservationContext context) { return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE, - context.getOperationMetadata().operationType()); + context.getOperationType()); } protected KeyValue aiProvider(EmbeddingModelObservationContext context) { @@ -74,8 +76,11 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode } protected KeyValue requestModel(EmbeddingModelObservationContext context) { - return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, - context.getRequestOptions().getModel()); + if (StringUtils.hasText(context.getRequestOptions().getModel())) { + return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, + context.getRequestOptions().getModel()); + } + return REQUEST_MODEL_NONE; } protected KeyValue responseModel(EmbeddingModelObservationContext context) { @@ -89,8 +94,7 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode @Override public KeyValues getHighCardinalityKeyValues(EmbeddingModelObservationContext context) { - return KeyValues.of(requestEmbeddingDimension(context), requestEmbeddingFormat(context), - usageInputTokens(context), usageTotalTokens(context)); + return KeyValues.of(requestEmbeddingDimension(context), usageInputTokens(context), usageTotalTokens(context)); } // Request @@ -104,15 +108,6 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode return REQUEST_EMBEDDING_DIMENSION_NONE; } - protected KeyValue requestEmbeddingFormat(EmbeddingModelObservationContext context) { - if (StringUtils.hasText(context.getRequestOptions().getEncodingFormat())) { - return KeyValue.of( - EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_ENCODING_FORMAT, - context.getRequestOptions().getEncodingFormat()); - } - return REQUEST_EMBEDDING_ENCODING_FORMAT_NONE; - } - // Response protected KeyValue usageInputTokens(EmbeddingModelObservationContext context) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/EmbeddingModelObservationContext.java b/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/EmbeddingModelObservationContext.java index 5273675d7..2e209d35f 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/EmbeddingModelObservationContext.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/embedding/observation/EmbeddingModelObservationContext.java @@ -15,10 +15,12 @@ */ package org.springframework.ai.embedding.observation; +import org.springframework.ai.embedding.EmbeddingOptions; import org.springframework.ai.embedding.EmbeddingRequest; import org.springframework.ai.embedding.EmbeddingResponse; 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,19 +31,23 @@ import org.springframework.util.Assert; */ public class EmbeddingModelObservationContext extends ModelObservationContext { - private final EmbeddingModelRequestOptions requestOptions; + private final EmbeddingOptions requestOptions; EmbeddingModelObservationContext(EmbeddingRequest embeddingRequest, AiOperationMetadata operationMetadata, - EmbeddingModelRequestOptions requestOptions) { + EmbeddingOptions requestOptions) { super(embeddingRequest, operationMetadata); Assert.notNull(requestOptions, "requestOptions cannot be null"); this.requestOptions = requestOptions; } - public EmbeddingModelRequestOptions getRequestOptions() { + public EmbeddingOptions getRequestOptions() { return requestOptions; } + public String getOperationType() { + return AiOperationType.EMBEDDING.value(); + } + public static Builder builder() { return new Builder(); } @@ -52,7 +58,7 @@ public class EmbeddingModelObservationContext extends ModelObservationContext EmbeddingModelRequestOptions.builder().build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("model cannot be null or empty"); - } - - @Test - void whenModelIsEmptyThenThrow() { - assertThatThrownBy(() -> EmbeddingModelRequestOptions.builder().model("").build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("model cannot be null or empty"); - } - -} \ No newline at end of file