GH-2518: Remove requestOptions from observation context objects
Fixes: #2518 Issue: https://github.com/spring-projects/spring-ai/issues/2518 This commit removes the deprecated requestOptions field from ChatModelObservationContext and EmbeddingModelObservationContext classes. Instead of passing options separately, the code now retrieves them directly from the request objects (prompt.getOptions() or embeddingRequest.getOptions()). Key changes: - Removed requestOptions parameter from observation context builders - Updated all model implementations to stop passing options separately - Fixed EmbeddingRequest handling in several model implementations - Added buildEmbeddingRequest method in models to properly merge options This change simplifies the API and removes duplication, as options are already available in the request objects themselves. Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
This commit is contained in:
committed by
Ilayaperumal Gopinathan
parent
4eeeb83d3f
commit
f18aac07b7
@@ -177,7 +177,6 @@ public class AnthropicChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AnthropicApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -240,7 +239,6 @@ public class AnthropicChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AnthropicApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -245,7 +245,6 @@ public class AzureOpenAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AiProvider.AZURE_OPENAI.value())
|
||||
.requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions)
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -300,7 +299,6 @@ public class AzureOpenAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AiProvider.AZURE_OPENAI.value())
|
||||
.requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions)
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -51,6 +51,7 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Mark Pollack
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class AzureOpenAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
@@ -124,12 +125,15 @@ public class AzureOpenAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
.from(this.defaultOptions)
|
||||
.merge(embeddingRequest.getOptions())
|
||||
.build();
|
||||
EmbeddingsOptions azureOptions = options.toAzureOptions(embeddingRequest.getInstructions());
|
||||
|
||||
EmbeddingRequest embeddingRequestWithMergedOptions = new EmbeddingRequest(embeddingRequest.getInstructions(),
|
||||
options);
|
||||
|
||||
EmbeddingsOptions azureOptions = options.toAzureOptions(embeddingRequestWithMergedOptions.getInstructions());
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.embeddingRequest(embeddingRequestWithMergedOptions)
|
||||
.provider(AiProvider.AZURE_OPENAI.value())
|
||||
.requestOptions(options)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -220,7 +220,6 @@ public class BedrockProxyChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AiProvider.BEDROCK_CONVERSE.value())
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse chatResponse = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -647,7 +646,6 @@ public class BedrockProxyChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(AiProvider.BEDROCK_CONVERSE.value())
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -241,7 +241,6 @@ public class MiniMaxChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(requestPrompt)
|
||||
.provider(MiniMaxApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(requestPrompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -334,7 +333,6 @@ public class MiniMaxChatModel implements ChatModel {
|
||||
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(requestPrompt)
|
||||
.provider(MiniMaxApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(requestPrompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -43,13 +43,15 @@ 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;
|
||||
|
||||
/**
|
||||
* MiniMax Embedding Model implementation.
|
||||
*
|
||||
* @author Geng Rong
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0 M1
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
|
||||
|
||||
@@ -149,14 +151,15 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
|
||||
|
||||
@Override
|
||||
public EmbeddingResponse call(EmbeddingRequest request) {
|
||||
MiniMaxEmbeddingOptions requestOptions = mergeOptions(request.getOptions(), this.defaultOptions);
|
||||
|
||||
EmbeddingRequest embeddingRequest = buildEmbeddingRequest(request);
|
||||
|
||||
MiniMaxApi.EmbeddingRequest apiRequest = new MiniMaxApi.EmbeddingRequest(request.getInstructions(),
|
||||
requestOptions.getModel());
|
||||
embeddingRequest.getOptions().getModel());
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.provider(MiniMaxApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(requestOptions)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
@@ -188,26 +191,24 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
|
||||
return new DefaultUsage(0, 0, apiEmbeddingList.totalTokens());
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge runtime and default {@link EmbeddingOptions} to compute the final options to
|
||||
* use in the request.
|
||||
*/
|
||||
private MiniMaxEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions,
|
||||
MiniMaxEmbeddingOptions defaultOptions) {
|
||||
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, EmbeddingOptions.class,
|
||||
EmbeddingRequest buildEmbeddingRequest(EmbeddingRequest embeddingRequest) {
|
||||
// Process runtime options
|
||||
MiniMaxEmbeddingOptions runtimeOptions = null;
|
||||
if (embeddingRequest.getOptions() != null) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(embeddingRequest.getOptions(), EmbeddingOptions.class,
|
||||
MiniMaxEmbeddingOptions.class);
|
||||
}
|
||||
|
||||
// Define request options by merging runtime options and default options
|
||||
MiniMaxEmbeddingOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
|
||||
MiniMaxEmbeddingOptions.class);
|
||||
|
||||
var optionBuilder = MiniMaxEmbeddingOptions.builder();
|
||||
if (runtimeOptionsForProvider != null && runtimeOptionsForProvider.getModel() != null) {
|
||||
optionBuilder.model(runtimeOptionsForProvider.getModel());
|
||||
// Validate request options
|
||||
if (!StringUtils.hasText(requestOptions.getModel())) {
|
||||
throw new IllegalArgumentException("model cannot be null or empty");
|
||||
}
|
||||
else if (defaultOptions.getModel() != null) {
|
||||
optionBuilder.model(defaultOptions.getModel());
|
||||
}
|
||||
else {
|
||||
optionBuilder.model(MiniMaxApi.DEFAULT_EMBEDDING_MODEL);
|
||||
}
|
||||
return optionBuilder.build();
|
||||
|
||||
return new EmbeddingRequest(embeddingRequest.getInstructions(), requestOptions);
|
||||
}
|
||||
|
||||
public void setObservationConvention(EmbeddingModelObservationConvention observationConvention) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -29,6 +29,7 @@ import reactor.core.publisher.Flux;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.minimax.MiniMaxChatModel;
|
||||
import org.springframework.ai.minimax.MiniMaxChatOptions;
|
||||
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
|
||||
@@ -57,6 +58,7 @@ import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* @author Geng Rong
|
||||
* @author Soby Chacko
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -150,8 +152,9 @@ public class MiniMaxRetryTests {
|
||||
.willThrow(new TransientAiException("Transient Error 2"))
|
||||
.willReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
|
||||
|
||||
EmbeddingOptions options = MiniMaxEmbeddingOptions.builder().model("model").build();
|
||||
var result = this.embeddingModel
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null));
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), options));
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getResult().getOutput()).isEqualTo(new float[] { 9.9f, 8.8f });
|
||||
@@ -163,8 +166,9 @@ public class MiniMaxRetryTests {
|
||||
public void miniMaxEmbeddingNonTransientError() {
|
||||
given(this.miniMaxApi.embeddings(isA(EmbeddingRequest.class)))
|
||||
.willThrow(new RuntimeException("Non Transient Error"));
|
||||
EmbeddingOptions options = MiniMaxEmbeddingOptions.builder().model("model").build();
|
||||
assertThrows(RuntimeException.class, () -> this.embeddingModel
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), options)));
|
||||
}
|
||||
|
||||
private class TestRetryListener implements RetryListener {
|
||||
|
||||
@@ -187,7 +187,6 @@ public class MistralAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(MistralAiApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -260,7 +259,6 @@ public class MistralAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(MistralAiApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -116,9 +116,8 @@ public class MistralAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
var apiRequest = createRequest(embeddingRequest);
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.provider(MistralAiApi.PROVIDER_NAME)
|
||||
.requestOptions(embeddingRequest.getOptions())
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -43,6 +43,7 @@ import org.springframework.ai.embedding.observation.EmbeddingModelObservationDoc
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.ai.embedding.EmbeddingModel} implementation that uses the
|
||||
@@ -83,13 +84,15 @@ public class OCIEmbeddingModel extends AbstractEmbeddingModel {
|
||||
@Override
|
||||
public EmbeddingResponse call(EmbeddingRequest request) {
|
||||
Assert.notEmpty(request.getInstructions(), "At least one text is required!");
|
||||
OCIEmbeddingOptions runtimeOptions = mergeOptions(request.getOptions(), this.options);
|
||||
List<EmbedTextRequest> embedTextRequests = createRequests(request.getInstructions(), runtimeOptions);
|
||||
|
||||
EmbeddingRequest embeddingRequest = buildEmbeddingRequest(request);
|
||||
|
||||
List<EmbedTextRequest> embedTextRequests = createRequests(embeddingRequest.getInstructions(),
|
||||
(OCIEmbeddingOptions) embeddingRequest.getOptions());
|
||||
|
||||
EmbeddingModelObservationContext context = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.provider(AiProvider.OCI_GENAI.value())
|
||||
.requestOptions(runtimeOptions)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
@@ -158,6 +161,26 @@ public class OCIEmbeddingModel extends AbstractEmbeddingModel {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
EmbeddingRequest buildEmbeddingRequest(EmbeddingRequest embeddingRequest) {
|
||||
// Process runtime options
|
||||
OCIEmbeddingOptions runtimeOptions = null;
|
||||
if (embeddingRequest.getOptions() != null) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(embeddingRequest.getOptions(), EmbeddingOptions.class,
|
||||
OCIEmbeddingOptions.class);
|
||||
}
|
||||
|
||||
// Define request options by merging runtime options and default options
|
||||
OCIEmbeddingOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.options,
|
||||
OCIEmbeddingOptions.class);
|
||||
|
||||
// Validate request options
|
||||
if (!StringUtils.hasText(requestOptions.getModel())) {
|
||||
throw new IllegalArgumentException("model cannot be null or empty");
|
||||
}
|
||||
|
||||
return new EmbeddingRequest(embeddingRequest.getInstructions(), requestOptions);
|
||||
}
|
||||
|
||||
private float[] toFloats(List<Float> embedding) {
|
||||
float[] floats = new float[embedding.size()];
|
||||
for (int i = 0; i < embedding.size(); i++) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.springframework.ai.chat.observation.DefaultChatModelObservationConven
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
import org.springframework.ai.oci.ServingModeHelper;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -104,10 +105,10 @@ public class OCICohereChatModel implements ChatModel {
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
Prompt requestPrompt = this.buildRequestPrompt(prompt);
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.prompt(requestPrompt)
|
||||
.provider(AiProvider.OCI_GENAI.value())
|
||||
.requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions)
|
||||
.build();
|
||||
|
||||
return ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -120,6 +121,21 @@ public class OCICohereChatModel implements ChatModel {
|
||||
});
|
||||
}
|
||||
|
||||
Prompt buildRequestPrompt(Prompt prompt) {
|
||||
// Process runtime options
|
||||
OCICohereChatOptions runtimeOptions = null;
|
||||
if (prompt.getOptions() != null) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
|
||||
OCICohereChatOptions.class);
|
||||
}
|
||||
|
||||
// Define request options by merging runtime options and default options
|
||||
OCICohereChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
|
||||
OCICohereChatOptions.class);
|
||||
|
||||
return new Prompt(prompt.getInstructions(), requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatOptions getDefaultOptions() {
|
||||
return OCICohereChatOptions.fromOptions(this.defaultOptions);
|
||||
|
||||
@@ -225,7 +225,6 @@ public class OllamaChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -296,7 +295,6 @@ public class OllamaChatModel implements ChatModel {
|
||||
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -113,7 +113,6 @@ public class OllamaEmbeddingModel extends AbstractEmbeddingModel {
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.requestOptions(embeddingRequest.getOptions())
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -186,7 +186,6 @@ public class OpenAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OpenAiApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -289,7 +288,6 @@ public class OpenAiChatModel implements ChatModel {
|
||||
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OpenAiApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -156,9 +156,8 @@ public class OpenAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
OpenAiApi.EmbeddingRequest<List<String>> apiRequest = createRequest(embeddingRequest);
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.provider(OpenAiApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(embeddingRequest.getOptions())
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -289,7 +289,6 @@ public class TransformersEmbeddingModel extends AbstractEmbeddingModel implement
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.provider(AiProvider.ONNX.value())
|
||||
.requestOptions(request.getOptions())
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.ai.chat.metadata.Usage;
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.embedding.AbstractEmbeddingModel;
|
||||
import org.springframework.ai.embedding.Embedding;
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Christian Tzolov
|
||||
* @author Mark Pollack
|
||||
* @author Rodrigo Malara
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class VertexAiTextEmbeddingModel extends AbstractEmbeddingModel {
|
||||
@@ -117,12 +119,11 @@ public class VertexAiTextEmbeddingModel extends AbstractEmbeddingModel {
|
||||
@Override
|
||||
public EmbeddingResponse call(EmbeddingRequest request) {
|
||||
|
||||
final VertexAiTextEmbeddingOptions finalOptions = mergedOptions(request);
|
||||
EmbeddingRequest embeddingRequest = buildEmbeddingRequest(request);
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.provider(AiProvider.VERTEX_AI.value())
|
||||
.requestOptions(finalOptions)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
@@ -131,10 +132,11 @@ public class VertexAiTextEmbeddingModel extends AbstractEmbeddingModel {
|
||||
.observe(() -> {
|
||||
try (PredictionServiceClient client = createPredictionServiceClient()) {
|
||||
|
||||
EndpointName endpointName = this.connectionDetails.getEndpointName(finalOptions.getModel());
|
||||
EmbeddingOptions options = embeddingRequest.getOptions();
|
||||
EndpointName endpointName = this.connectionDetails.getEndpointName(options.getModel());
|
||||
|
||||
PredictRequest.Builder predictRequestBuilder = getPredictRequestBuilder(request, endpointName,
|
||||
finalOptions);
|
||||
(VertexAiTextEmbeddingOptions) options);
|
||||
|
||||
PredictResponse embeddingResponse = this.retryTemplate
|
||||
.execute(context -> getPredictResponse(client, predictRequestBuilder));
|
||||
@@ -155,7 +157,7 @@ public class VertexAiTextEmbeddingModel extends AbstractEmbeddingModel {
|
||||
embeddingList.add(new Embedding(vectorValues, index++));
|
||||
}
|
||||
EmbeddingResponse response = new EmbeddingResponse(embeddingList,
|
||||
generateResponseMetadata(finalOptions.getModel(), totalTokenCount));
|
||||
generateResponseMetadata(options.getModel(), totalTokenCount));
|
||||
|
||||
observationContext.setResponse(response);
|
||||
|
||||
@@ -164,17 +166,24 @@ public class VertexAiTextEmbeddingModel extends AbstractEmbeddingModel {
|
||||
});
|
||||
}
|
||||
|
||||
private VertexAiTextEmbeddingOptions mergedOptions(EmbeddingRequest request) {
|
||||
|
||||
VertexAiTextEmbeddingOptions mergedOptions = this.defaultOptions;
|
||||
|
||||
if (request.getOptions() != null) {
|
||||
var defaultOptionsCopy = VertexAiTextEmbeddingOptions.builder().from(this.defaultOptions).build();
|
||||
mergedOptions = ModelOptionsUtils.merge(request.getOptions(), defaultOptionsCopy,
|
||||
EmbeddingRequest buildEmbeddingRequest(EmbeddingRequest embeddingRequest) {
|
||||
// Process runtime options
|
||||
VertexAiTextEmbeddingOptions runtimeOptions = null;
|
||||
if (embeddingRequest.getOptions() != null) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(embeddingRequest.getOptions(), EmbeddingOptions.class,
|
||||
VertexAiTextEmbeddingOptions.class);
|
||||
}
|
||||
|
||||
return mergedOptions;
|
||||
// Define request options by merging runtime options and default options
|
||||
VertexAiTextEmbeddingOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
|
||||
VertexAiTextEmbeddingOptions.class);
|
||||
|
||||
// Validate request options
|
||||
if (!StringUtils.hasText(requestOptions.getModel())) {
|
||||
throw new IllegalArgumentException("model cannot be null or empty");
|
||||
}
|
||||
|
||||
return new EmbeddingRequest(embeddingRequest.getInstructions(), requestOptions);
|
||||
}
|
||||
|
||||
protected PredictRequest.Builder getPredictRequestBuilder(EmbeddingRequest request, EndpointName endpointName,
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
@@ -116,7 +117,8 @@ public class VertexAiTextEmbeddingRetryTests {
|
||||
.willThrow(new TransientAiException("Transient Error 2"))
|
||||
.willReturn(mockResponse);
|
||||
|
||||
EmbeddingResponse result = this.embeddingModel.call(new EmbeddingRequest(List.of("text1", "text2"), null));
|
||||
EmbeddingOptions options = VertexAiTextEmbeddingOptions.builder().model("model").build();
|
||||
EmbeddingResponse result = this.embeddingModel.call(new EmbeddingRequest(List.of("text1", "text2"), options));
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getResults()).hasSize(1);
|
||||
@@ -132,8 +134,9 @@ public class VertexAiTextEmbeddingRetryTests {
|
||||
// Setup the mock PredictionServiceClient to throw a non-transient error
|
||||
given(this.mockPredictionServiceClient.predict(any())).willThrow(new RuntimeException("Non Transient Error"));
|
||||
|
||||
EmbeddingOptions options = VertexAiTextEmbeddingOptions.builder().model("model").build();
|
||||
// Assert that a RuntimeException is thrown and not retried
|
||||
assertThatThrownBy(() -> this.embeddingModel.call(new EmbeddingRequest(List.of("text1", "text2"), null)))
|
||||
assertThatThrownBy(() -> this.embeddingModel.call(new EmbeddingRequest(List.of("text1", "text2"), options)))
|
||||
.isInstanceOf(RuntimeException.class);
|
||||
|
||||
// Verify that predict was called only once (no retries for non-transient errors)
|
||||
|
||||
@@ -366,7 +366,6 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(VertexAiGeminiConstants.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -478,7 +477,6 @@ public class VertexAiGeminiChatModel implements ChatModel, DisposableBean {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(VertexAiGeminiConstants.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -242,7 +242,6 @@ public class ZhiPuAiChatModel implements ChatModel {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(requestPrompt)
|
||||
.provider(ZhiPuApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(prompt.getOptions())
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -319,7 +318,6 @@ public class ZhiPuAiChatModel implements ChatModel {
|
||||
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(requestPrompt)
|
||||
.provider(ZhiPuApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(buildRequestOptions(request))
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -41,15 +41,16 @@ import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
|
||||
import org.springframework.ai.zhipuai.api.ZhiPuApiConstants;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* ZhiPuAI Embedding Model implementation.
|
||||
*
|
||||
* @author Geng Rong
|
||||
* @since 1.0.0 M1
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ZhiPuAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
|
||||
@@ -153,12 +154,12 @@ public class ZhiPuAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
logger.warn(
|
||||
"ZhiPu Embedding does not support batch embedding. Will make multiple API calls to embed(Document)");
|
||||
}
|
||||
ZhiPuAiEmbeddingOptions requestOptions = mergeOptions(request.getOptions(), this.defaultOptions);
|
||||
|
||||
EmbeddingRequest embeddingRequest = buildEmbeddingRequest(request);
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.embeddingRequest(embeddingRequest)
|
||||
.provider(ZhiPuApiConstants.PROVIDER_NAME)
|
||||
.requestOptions(requestOptions)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
@@ -170,7 +171,7 @@ public class ZhiPuAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
var totalUsage = new ZhiPuAiApi.Usage(0, 0, 0);
|
||||
|
||||
for (String inputContent : request.getInstructions()) {
|
||||
var apiRequest = createEmbeddingRequest(inputContent, requestOptions);
|
||||
var apiRequest = createEmbeddingRequest(inputContent, embeddingRequest.getOptions());
|
||||
|
||||
ZhiPuAiApi.EmbeddingList<ZhiPuAiApi.Embedding> response = this.retryTemplate
|
||||
.execute(ctx -> this.zhiPuAiApi.embeddings(apiRequest).getBody());
|
||||
@@ -210,24 +211,24 @@ public class ZhiPuAiEmbeddingModel extends AbstractEmbeddingModel {
|
||||
return new DefaultUsage(usage.promptTokens(), usage.completionTokens(), usage.totalTokens(), usage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge runtime and default {@link EmbeddingOptions} to compute the final options to
|
||||
* use in the request.
|
||||
*/
|
||||
private ZhiPuAiEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions,
|
||||
ZhiPuAiEmbeddingOptions defaultOptions) {
|
||||
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, EmbeddingOptions.class,
|
||||
ZhiPuAiEmbeddingOptions.class);
|
||||
|
||||
if (runtimeOptionsForProvider == null) {
|
||||
return defaultOptions;
|
||||
EmbeddingRequest buildEmbeddingRequest(EmbeddingRequest embeddingRequest) {
|
||||
// Process runtime options
|
||||
ZhiPuAiEmbeddingOptions runtimeOptions = null;
|
||||
if (embeddingRequest.getOptions() != null) {
|
||||
runtimeOptions = ModelOptionsUtils.copyToTarget(embeddingRequest.getOptions(), EmbeddingOptions.class,
|
||||
ZhiPuAiEmbeddingOptions.class);
|
||||
}
|
||||
|
||||
return ZhiPuAiEmbeddingOptions.builder()
|
||||
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
|
||||
.dimensions(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getDimensions(),
|
||||
defaultOptions.getDimensions()))
|
||||
.build();
|
||||
// Define request options by merging runtime options and default options
|
||||
ZhiPuAiEmbeddingOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
|
||||
ZhiPuAiEmbeddingOptions.class);
|
||||
|
||||
// Validate request options
|
||||
if (!StringUtils.hasText(requestOptions.getModel())) {
|
||||
throw new IllegalArgumentException("model cannot be null or empty");
|
||||
}
|
||||
|
||||
return new EmbeddingRequest(embeddingRequest.getInstructions(), requestOptions);
|
||||
}
|
||||
|
||||
private ZhiPuAiApi.EmbeddingRequest<String> createEmbeddingRequest(String text, EmbeddingOptions requestOptions) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import reactor.core.publisher.Flux;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.image.ImageMessage;
|
||||
import org.springframework.ai.image.ImagePrompt;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
@@ -164,9 +165,9 @@ public class ZhiPuAiRetryTests {
|
||||
.willThrow(new TransientAiException("Transient Error 1"))
|
||||
.willThrow(new TransientAiException("Transient Error 2"))
|
||||
.willReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
|
||||
|
||||
EmbeddingOptions options = ZhiPuAiEmbeddingOptions.builder().model("model").build();
|
||||
var result = this.embeddingModel
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null));
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), options));
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getResult().getOutput()).isEqualTo(new float[] { 9.9f, 8.8f });
|
||||
@@ -178,8 +179,9 @@ public class ZhiPuAiRetryTests {
|
||||
public void zhiPuAiEmbeddingNonTransientError() {
|
||||
given(this.zhiPuAiApi.embeddings(isA(EmbeddingRequest.class)))
|
||||
.willThrow(new RuntimeException("Non Transient Error"));
|
||||
EmbeddingOptions options = ZhiPuAiEmbeddingOptions.builder().model("model").build();
|
||||
assertThrows(RuntimeException.class, () -> this.embeddingModel
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
|
||||
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), options)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -32,35 +32,21 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class ChatModelObservationContext extends ModelObservationContext<Prompt, ChatResponse> {
|
||||
|
||||
private final ChatOptions requestOptions;
|
||||
|
||||
ChatModelObservationContext(Prompt prompt, String provider, ChatOptions requestOptions) {
|
||||
ChatModelObservationContext(Prompt prompt, String provider) {
|
||||
super(prompt,
|
||||
AiOperationMetadata.builder().operationType(AiOperationType.CHAT.value()).provider(provider).build());
|
||||
Assert.notNull(requestOptions, "requestOptions cannot be null");
|
||||
this.requestOptions = requestOptions;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getRequest().getOptions()} instead.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public ChatOptions getRequestOptions() {
|
||||
return this.requestOptions;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private Prompt prompt;
|
||||
|
||||
private String provider;
|
||||
|
||||
private ChatOptions requestOptions;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
@@ -74,18 +60,8 @@ public class ChatModelObservationContext extends ModelObservationContext<Prompt,
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ChatOptions are passed in the Prompt object and should not be set
|
||||
* separately anymore.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public Builder requestOptions(ChatOptions requestOptions) {
|
||||
this.requestOptions = requestOptions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatModelObservationContext build() {
|
||||
return new ChatModelObservationContext(this.prompt, this.provider, this.requestOptions);
|
||||
return new ChatModelObservationContext(this.prompt, this.provider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -22,6 +22,7 @@ import java.util.StringJoiner;
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -29,6 +30,7 @@ import org.springframework.util.StringUtils;
|
||||
* Default conventions to populate observations for chat model operations.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DefaultChatModelObservationConvention implements ChatModelObservationConvention {
|
||||
@@ -48,9 +50,9 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
|
||||
|
||||
@Override
|
||||
public String getContextualName(ChatModelObservationContext context) {
|
||||
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
|
||||
return "%s %s".formatted(context.getOperationMetadata().operationType(),
|
||||
context.getRequestOptions().getModel());
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (StringUtils.hasText(options.getModel())) {
|
||||
return "%s %s".formatted(context.getOperationMetadata().operationType(), options.getModel());
|
||||
}
|
||||
return context.getOperationMetadata().operationType();
|
||||
}
|
||||
@@ -72,9 +74,10 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
|
||||
}
|
||||
|
||||
protected KeyValue requestModel(ChatModelObservationContext context) {
|
||||
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (StringUtils.hasText(options.getModel())) {
|
||||
return KeyValue.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL,
|
||||
context.getRequestOptions().getModel());
|
||||
options.getModel());
|
||||
}
|
||||
return REQUEST_MODEL_NONE;
|
||||
}
|
||||
@@ -111,40 +114,42 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
|
||||
// Request
|
||||
|
||||
protected KeyValues requestFrequencyPenalty(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getFrequencyPenalty() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getFrequencyPenalty() != null) {
|
||||
return keyValues.and(
|
||||
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(),
|
||||
String.valueOf(context.getRequestOptions().getFrequencyPenalty()));
|
||||
String.valueOf(options.getFrequencyPenalty()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues requestMaxTokens(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getMaxTokens() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getMaxTokens() != null) {
|
||||
return keyValues.and(
|
||||
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(),
|
||||
String.valueOf(context.getRequestOptions().getMaxTokens()));
|
||||
String.valueOf(options.getMaxTokens()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues requestPresencePenalty(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getPresencePenalty() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getPresencePenalty() != null) {
|
||||
return keyValues.and(
|
||||
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(),
|
||||
String.valueOf(context.getRequestOptions().getPresencePenalty()));
|
||||
String.valueOf(options.getPresencePenalty()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues requestStopSequences(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (!CollectionUtils.isEmpty(context.getRequestOptions().getStopSequences())) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (!CollectionUtils.isEmpty(options.getStopSequences())) {
|
||||
StringJoiner stopSequencesJoiner = new StringJoiner(", ", "[", "]");
|
||||
context.getRequestOptions()
|
||||
.getStopSequences()
|
||||
.forEach(value -> stopSequencesJoiner.add("\"" + value + "\""));
|
||||
options.getStopSequences().forEach(value -> stopSequencesJoiner.add("\"" + value + "\""));
|
||||
KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES,
|
||||
context.getRequestOptions().getStopSequences(), Objects::nonNull);
|
||||
options.getStopSequences(), Objects::nonNull);
|
||||
return keyValues.and(
|
||||
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(),
|
||||
stopSequencesJoiner.toString());
|
||||
@@ -153,26 +158,29 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati
|
||||
}
|
||||
|
||||
protected KeyValues requestTemperature(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getTemperature() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getTemperature() != null) {
|
||||
return keyValues.and(
|
||||
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(),
|
||||
String.valueOf(context.getRequestOptions().getTemperature()));
|
||||
String.valueOf(options.getTemperature()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues requestTopK(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getTopK() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getTopK() != null) {
|
||||
return keyValues.and(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K.asString(),
|
||||
String.valueOf(context.getRequestOptions().getTopK()));
|
||||
String.valueOf(options.getTopK()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues requestTopP(KeyValues keyValues, ChatModelObservationContext context) {
|
||||
if (context.getRequestOptions().getTopP() != null) {
|
||||
ChatOptions options = context.getRequest().getOptions();
|
||||
if (options.getTopP() != null) {
|
||||
return keyValues.and(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_P.asString(),
|
||||
String.valueOf(context.getRequestOptions().getTopP()));
|
||||
String.valueOf(options.getTopP()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -25,6 +25,7 @@ import org.springframework.util.StringUtils;
|
||||
* Default conventions to populate observations for embedding model operations.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DefaultEmbeddingModelObservationConvention implements EmbeddingModelObservationConvention {
|
||||
@@ -44,9 +45,9 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode
|
||||
|
||||
@Override
|
||||
public String getContextualName(EmbeddingModelObservationContext context) {
|
||||
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
|
||||
if (StringUtils.hasText(context.getRequest().getOptions().getModel())) {
|
||||
return "%s %s".formatted(context.getOperationMetadata().operationType(),
|
||||
context.getRequestOptions().getModel());
|
||||
context.getRequest().getOptions().getModel());
|
||||
}
|
||||
return context.getOperationMetadata().operationType();
|
||||
}
|
||||
@@ -68,9 +69,9 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode
|
||||
}
|
||||
|
||||
protected KeyValue requestModel(EmbeddingModelObservationContext context) {
|
||||
if (StringUtils.hasText(context.getRequestOptions().getModel())) {
|
||||
if (StringUtils.hasText(context.getRequest().getOptions().getModel())) {
|
||||
return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL,
|
||||
context.getRequestOptions().getModel());
|
||||
context.getRequest().getOptions().getModel());
|
||||
}
|
||||
return REQUEST_MODEL_NONE;
|
||||
}
|
||||
@@ -98,10 +99,10 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode
|
||||
// Request
|
||||
|
||||
protected KeyValues requestEmbeddingDimension(KeyValues keyValues, EmbeddingModelObservationContext context) {
|
||||
if (context.getRequestOptions().getDimensions() != null) {
|
||||
if (context.getRequest().getOptions().getDimensions() != null) {
|
||||
return keyValues
|
||||
.and(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS
|
||||
.asString(), String.valueOf(context.getRequestOptions().getDimensions()));
|
||||
.asString(), String.valueOf(context.getRequest().getOptions().getDimensions()));
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -22,49 +22,34 @@ 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;
|
||||
|
||||
/**
|
||||
* Context used to store metadata for embedding model exchanges.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @author Soby Chacko
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class EmbeddingModelObservationContext extends ModelObservationContext<EmbeddingRequest, EmbeddingResponse> {
|
||||
|
||||
private final EmbeddingOptions requestOptions;
|
||||
|
||||
EmbeddingModelObservationContext(EmbeddingRequest embeddingRequest, String provider,
|
||||
EmbeddingOptions requestOptions) {
|
||||
EmbeddingModelObservationContext(EmbeddingRequest embeddingRequest, String provider) {
|
||||
super(embeddingRequest,
|
||||
AiOperationMetadata.builder()
|
||||
.operationType(AiOperationType.EMBEDDING.value())
|
||||
.provider(provider)
|
||||
.build());
|
||||
Assert.notNull(requestOptions, "requestOptions cannot be null");
|
||||
this.requestOptions = requestOptions;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getRequest().getOptions()} instead.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public EmbeddingOptions getRequestOptions() {
|
||||
return this.requestOptions;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private EmbeddingRequest embeddingRequest;
|
||||
|
||||
private String provider;
|
||||
|
||||
private EmbeddingOptions requestOptions;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
@@ -78,18 +63,8 @@ public class EmbeddingModelObservationContext extends ModelObservationContext<Em
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated EmbeddingOptions are passed in the EmbeddingRequest object and
|
||||
* should not be set separately anymore.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public Builder requestOptions(EmbeddingOptions requestOptions) {
|
||||
this.requestOptions = requestOptions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EmbeddingModelObservationContext build() {
|
||||
return new EmbeddingModelObservationContext(this.embeddingRequest, this.provider, this.requestOptions);
|
||||
return new EmbeddingModelObservationContext(this.embeddingRequest, this.provider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -51,9 +51,8 @@ class ChatModelCompletionObservationFilterTests {
|
||||
@Test
|
||||
void whenEmptyResponseThenReturnOriginalContext() {
|
||||
var expectedContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
var actualContext = this.observationFilter.map(expectedContext);
|
||||
|
||||
@@ -63,9 +62,8 @@ class ChatModelCompletionObservationFilterTests {
|
||||
@Test
|
||||
void whenEmptyCompletionThenReturnOriginalContext() {
|
||||
var expectedContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
expectedContext.setResponse(new ChatResponse(List.of(new Generation(new AssistantMessage("")))));
|
||||
var actualContext = this.observationFilter.map(expectedContext);
|
||||
@@ -76,9 +74,8 @@ class ChatModelCompletionObservationFilterTests {
|
||||
@Test
|
||||
void whenCompletionWithTextThenAugmentContext() {
|
||||
var originalContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
originalContext.setResponse(new ChatResponse(List.of(new Generation(new AssistantMessage("say please")),
|
||||
new Generation(new AssistantMessage("seriously, say please")))));
|
||||
@@ -88,8 +85,8 @@ class ChatModelCompletionObservationFilterTests {
|
||||
.of(HighCardinalityKeyNames.COMPLETION.asString(), "[\"say please\", \"seriously, say please\"]"));
|
||||
}
|
||||
|
||||
private Prompt generatePrompt() {
|
||||
return new Prompt("supercalifragilisticexpialidocious");
|
||||
private Prompt generatePrompt(ChatOptions chatOptions) {
|
||||
return new Prompt("supercalifragilisticexpialidocious", chatOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -47,9 +47,9 @@ class ChatModelCompletionObservationHandlerTests {
|
||||
@Test
|
||||
void whenCompletionWithTextThenSpanEvent() {
|
||||
var observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious"))
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious",
|
||||
ChatOptions.builder().model("spoonful-of-sugar").build()))
|
||||
.provider("mary-poppins")
|
||||
.requestOptions(ChatOptions.builder().model("spoonful-of-sugar").build())
|
||||
.build();
|
||||
observationContext.setResponse(new ChatResponse(List.of(new Generation(new AssistantMessage("say please")),
|
||||
new Generation(new AssistantMessage("seriously, say please")))));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -95,14 +95,13 @@ class ChatModelMeterObservationHandlerTests {
|
||||
|
||||
private ChatModelObservationContext generateObservationContext() {
|
||||
return ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Prompt generatePrompt() {
|
||||
return new Prompt("hello");
|
||||
private Prompt generatePrompt(ChatOptions chatOptions) {
|
||||
return new Prompt("hello", chatOptions);
|
||||
}
|
||||
|
||||
static class TestUsage implements Usage {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -34,26 +34,15 @@ class ChatModelObservationContextTests {
|
||||
@Test
|
||||
void whenMandatoryRequestOptionsThenReturn() {
|
||||
var observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("supermodel").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("supermodel").build())
|
||||
.build();
|
||||
|
||||
assertThat(observationContext).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenRequestOptionsIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.provider("superprovider")
|
||||
.requestOptions(null)
|
||||
.build()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("requestOptions cannot be null");
|
||||
}
|
||||
|
||||
private Prompt generatePrompt() {
|
||||
return new Prompt("hello");
|
||||
private Prompt generatePrompt(ChatOptions chatOptions) {
|
||||
return new Prompt("hello", chatOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -50,9 +50,8 @@ class ChatModelPromptContentObservationFilterTests {
|
||||
@Test
|
||||
void whenEmptyPromptThenReturnOriginalContext() {
|
||||
var expectedContext = ChatModelObservationContext.builder()
|
||||
.prompt(new Prompt(List.of()))
|
||||
.prompt(new Prompt(List.of(), ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
var actualContext = this.observationFilter.map(expectedContext);
|
||||
|
||||
@@ -62,9 +61,8 @@ class ChatModelPromptContentObservationFilterTests {
|
||||
@Test
|
||||
void whenPromptWithTextThenAugmentContext() {
|
||||
var originalContext = ChatModelObservationContext.builder()
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious"))
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious", ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
var augmentedContext = this.observationFilter.map(originalContext);
|
||||
|
||||
@@ -75,10 +73,11 @@ class ChatModelPromptContentObservationFilterTests {
|
||||
@Test
|
||||
void whenPromptWithMessagesThenAugmentContext() {
|
||||
var originalContext = ChatModelObservationContext.builder()
|
||||
.prompt(new Prompt(List.of(new SystemMessage("you're a chimney sweep"),
|
||||
new UserMessage("supercalifragilisticexpialidocious"))))
|
||||
.prompt(new Prompt(
|
||||
List.of(new SystemMessage("you're a chimney sweep"),
|
||||
new UserMessage("supercalifragilisticexpialidocious")),
|
||||
ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
var augmentedContext = this.observationFilter.map(originalContext);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -42,9 +42,9 @@ class ChatModelPromptContentObservationHandlerTests {
|
||||
@Test
|
||||
void whenPromptWithTextThenSpanEvent() {
|
||||
var observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious"))
|
||||
.prompt(new Prompt("supercalifragilisticexpialidocious",
|
||||
ChatOptions.builder().model("spoonful-of-sugar").build()))
|
||||
.provider("mary-poppins")
|
||||
.requestOptions(ChatOptions.builder().model("spoonful-of-sugar").build())
|
||||
.build();
|
||||
var sdkTracer = SdkTracerProvider.builder().build().get("test");
|
||||
var otelTracer = new OtelTracer(sdkTracer, new OtelCurrentTraceContext(), null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -55,9 +55,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void contextualNameWhenModelIsDefined() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("chat mistral");
|
||||
}
|
||||
@@ -65,9 +64,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void contextualNameWhenModelIsNotDefined() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("chat");
|
||||
}
|
||||
@@ -75,9 +73,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void supportsOnlyChatModelObservationContext() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
|
||||
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
|
||||
@@ -86,9 +83,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldHaveLowCardinalityKeyValuesWhenDefined() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().model("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().model("mistral").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
|
||||
KeyValue.of(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), "chat"),
|
||||
@@ -99,9 +95,7 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldHaveKeyValuesWhenDefinedAndResponse() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder()
|
||||
.prompt(generatePrompt(ChatOptions.builder()
|
||||
.model("mistral")
|
||||
.frequencyPenalty(0.8)
|
||||
.maxTokens(200)
|
||||
@@ -110,7 +104,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
.temperature(0.5)
|
||||
.topK(1)
|
||||
.topP(0.9)
|
||||
.build())
|
||||
.build()))
|
||||
.provider("superprovider")
|
||||
.build();
|
||||
observationContext.setResponse(new ChatResponse(
|
||||
List.of(new Generation(new AssistantMessage("response"),
|
||||
@@ -136,9 +131,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldNotHaveKeyValuesWhenMissing() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext))
|
||||
.contains(KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE))
|
||||
@@ -162,9 +156,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldNotHaveKeyValuesWhenEmptyValues() {
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(generatePrompt())
|
||||
.prompt(generatePrompt(ChatOptions.builder().stopSequences(List.of()).build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(ChatOptions.builder().stopSequences(List.of()).build())
|
||||
.build();
|
||||
observationContext.setResponse(new ChatResponse(
|
||||
List.of(new Generation(new AssistantMessage("response"),
|
||||
@@ -178,8 +171,8 @@ class DefaultChatModelObservationConventionTests {
|
||||
HighCardinalityKeyNames.RESPONSE_ID.asString());
|
||||
}
|
||||
|
||||
private Prompt generatePrompt() {
|
||||
return new Prompt("Who let the dogs out?");
|
||||
private Prompt generatePrompt(ChatOptions chatOptions) {
|
||||
return new Prompt("Who let the dogs out?", chatOptions);
|
||||
}
|
||||
|
||||
static class TestUsage implements Usage {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -22,9 +22,11 @@ import java.util.Map;
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.observation.Observation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.metadata.Usage;
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
@@ -52,9 +54,8 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void contextualNameWhenModelIsDefined() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().withModel("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("embedding mistral");
|
||||
}
|
||||
@@ -62,9 +63,8 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void contextualNameWhenModelIsNotDefined() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("embedding");
|
||||
}
|
||||
@@ -72,9 +72,9 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void supportsOnlyEmbeddingModelObservationContext() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(
|
||||
generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().withModel("supermodel").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("supermodel").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
|
||||
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
|
||||
@@ -83,9 +83,8 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldHaveLowCardinalityKeyValuesWhenDefined() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().withModel("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
|
||||
KeyValue.of(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), "embedding"),
|
||||
@@ -96,9 +95,9 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldHaveLowCardinalityKeyValuesWhenDefinedAndResponse() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(
|
||||
EmbeddingOptionsBuilder.builder().withModel("mistral").withDimensions(1492).build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").withDimensions(1492).build())
|
||||
.build();
|
||||
observationContext.setResponse(new EmbeddingResponse(List.of(),
|
||||
new EmbeddingResponseMetadata("mistral-42", new TestUsage(), Map.of())));
|
||||
@@ -113,9 +112,8 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
@Test
|
||||
void shouldNotHaveKeyValuesWhenMissing() {
|
||||
EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().build())
|
||||
.build();
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext))
|
||||
.contains(KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE))
|
||||
@@ -128,8 +126,8 @@ class DefaultEmbeddingModelObservationConventionTests {
|
||||
HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString());
|
||||
}
|
||||
|
||||
private EmbeddingRequest generateEmbeddingRequest() {
|
||||
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
|
||||
private EmbeddingRequest generateEmbeddingRequest(EmbeddingOptions embeddingOptions) {
|
||||
return new EmbeddingRequest(List.of(), embeddingOptions);
|
||||
}
|
||||
|
||||
static class TestUsage implements Usage {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.metadata.Usage;
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
@@ -92,14 +93,13 @@ class EmbeddingModelMeterObservationHandlerTests {
|
||||
|
||||
private EmbeddingModelObservationContext generateObservationContext() {
|
||||
return EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().withModel("mistral").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("mistral").build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private EmbeddingRequest generateEmbeddingRequest() {
|
||||
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
|
||||
private EmbeddingRequest generateEmbeddingRequest(EmbeddingOptions embeddingOptions) {
|
||||
return new EmbeddingRequest(List.of(), embeddingOptions);
|
||||
}
|
||||
|
||||
static class TestUsage implements Usage {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 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.
|
||||
@@ -20,6 +20,7 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingOptions;
|
||||
import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
|
||||
@@ -36,26 +37,16 @@ class EmbeddingModelObservationContextTests {
|
||||
@Test
|
||||
void whenMandatoryRequestOptionsThenReturn() {
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.embeddingRequest(
|
||||
generateEmbeddingRequest(EmbeddingOptionsBuilder.builder().withModel("supermodel").build()))
|
||||
.provider("superprovider")
|
||||
.requestOptions(EmbeddingOptionsBuilder.builder().withModel("supermodel").build())
|
||||
.build();
|
||||
|
||||
assertThat(observationContext).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenRequestOptionsIsNullThenThrow() {
|
||||
assertThatThrownBy(() -> EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(generateEmbeddingRequest())
|
||||
.provider("superprovider")
|
||||
.requestOptions(null)
|
||||
.build()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("requestOptions cannot be null");
|
||||
}
|
||||
|
||||
private EmbeddingRequest generateEmbeddingRequest() {
|
||||
return new EmbeddingRequest(List.of(), EmbeddingOptionsBuilder.builder().build());
|
||||
private EmbeddingRequest generateEmbeddingRequest(EmbeddingOptions embeddingOptions) {
|
||||
return new EmbeddingRequest(List.of(), embeddingOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user