From 3b1c68ac10e2f3fc51775d52f368bd9bec9d18c6 Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Sat, 5 Oct 2024 23:28:18 +0200 Subject: [PATCH] Improve optional values handling in chat model observations Chat model observations support several key-value pairs, coming from the Spring AI abstractions. Currently, whenever a value is not available (either because not configured by the user or not supported by the model provider), a span/metrics is generated anyway with value none. That causes several issues, including an unneeded increase in time series, challenges in alerting/monitoring (especially for integer/double attributes that suddenly are populated with a string), and incompliance with the OpenTelemetry Semantic Conventions (according to which, attributes should be excluded altogether if there's no value). This pull request changes the conventions for chat model observation to exclude the generation of span/metrics attributes for optional values which don't have any value. Signed-off-by: Thomas Vitale --- .../AnthropicChatModelObservationIT.java | 7 +- .../AzureOpenAiChatModelObservationIT.java | 6 +- .../MistralAiChatModelObservationIT.java | 36 +++- .../MistralAiEmbeddingModelObservationIT.java | 4 +- .../ollama/OllamaChatModelObservationIT.java | 3 +- .../OllamaEmbeddingModelObservationIT.java | 6 +- .../ai/ollama/OllamaImage.java | 2 +- .../chat/OpenAiChatModelObservationIT.java | 3 +- .../proxy/OllamaWithOpenAiChatModelIT.java | 2 +- ...DefaultChatModelObservationConvention.java | 158 ++++++++---------- ...ltEmbeddingModelObservationConvention.java | 57 +++---- ...efaultImageModelObservationConvention.java | 38 ++--- .../conventions/AiOperationType.java | 1 + .../observation/conventions/AiProvider.java | 7 +- ...ltChatModelObservationConventionTests.java | 48 +++--- ...eddingModelObservationConventionTests.java | 19 ++- ...tImageModelObservationConventionTests.java | 13 +- .../ai/autoconfigure/ollama/OllamaImage.java | 2 +- ...ContainerConnectionDetailsFactoryTest.java | 2 +- 19 files changed, 206 insertions(+), 208 deletions(-) diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelObservationIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelObservationIT.java index e257d8a4f..17ef19c90 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelObservationIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelObservationIT.java @@ -39,7 +39,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import reactor.core.publisher.Flux; @@ -133,11 +132,9 @@ public class AnthropicChatModelObservationIT { .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(), AnthropicApi.ChatModel.CLAUDE_3_HAIKU.getValue()) .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel()) - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), "[\"this-is-the-end\"]") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7") diff --git a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelObservationIT.java b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelObservationIT.java index df4907a09..5a24b5cc8 100644 --- a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelObservationIT.java +++ b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelObservationIT.java @@ -39,7 +39,6 @@ import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.OpenAIServiceVersion; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.http.policy.HttpLogOptions; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; @@ -106,9 +105,8 @@ class AzureOpenAiChatModelObservationIT { "[\"this-is-the-end\"]") .hasHighCardinalityKeyValue( ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7") - .hasHighCardinalityKeyValue( - ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K.asString()) .hasHighCardinalityKeyValue( ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_P.asString(), "1.0") .hasHighCardinalityKeyValue( diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelObservationIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelObservationIT.java index 00f56ba0d..38b3c9026 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelObservationIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatModelObservationIT.java @@ -15,7 +15,6 @@ */ package org.springframework.ai.mistralai; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.BeforeEach; @@ -128,19 +127,38 @@ public class MistralAiChatModelObservationIT { .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.MISTRAL_AI.value()) .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(), MistralAiApi.ChatModel.OPEN_MISTRAL_7B.getValue()) - .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel()) - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), - KeyValue.NONE_VALUE) + .matches(contextView -> { + var keyValue = contextView.getLowCardinalityKeyValues() + .stream() + .filter(tag -> tag.getKey().equals(LowCardinalityKeyNames.RESPONSE_MODEL.asString())) + .findFirst(); + if (StringUtils.hasText(responseMetadata.getModel())) { + return keyValue.isPresent() && keyValue.get().getValue().equals(responseMetadata.getModel()); + } + else { + return keyValue.isEmpty(); + } + }) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), "[\"this-is-the-end\"]") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_K.asString(), KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_TOP_K.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_P.asString(), "1.0") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_ID.asString(), - StringUtils.hasText(responseMetadata.getId()) ? responseMetadata.getId() : KeyValue.NONE_VALUE) + .matches(contextView -> { + var keyValue = contextView.getHighCardinalityKeyValues() + .stream() + .filter(tag -> tag.getKey().equals(HighCardinalityKeyNames.RESPONSE_ID.asString())) + .findFirst(); + if (StringUtils.hasText(responseMetadata.getId())) { + return keyValue.isPresent() && keyValue.get().getValue().equals(responseMetadata.getId()); + } + else { + return keyValue.isEmpty(); + } + }) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), "[\"STOP\"]") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(responseMetadata.getUsage().getPromptTokens())) diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiEmbeddingModelObservationIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiEmbeddingModelObservationIT.java index a49370ce2..55f634c0c 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiEmbeddingModelObservationIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiEmbeddingModelObservationIT.java @@ -15,7 +15,6 @@ */ package org.springframework.ai.mistralai; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Test; @@ -81,8 +80,7 @@ public class MistralAiEmbeddingModelObservationIT { .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(), MistralAiApi.EmbeddingModel.EMBED.getValue()) .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel()) - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(responseMetadata.getUsage().getPromptTokens())) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelObservationIT.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelObservationIT.java index 81fdbb365..d72c7067a 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelObservationIT.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaChatModelObservationIT.java @@ -141,8 +141,7 @@ public class OllamaChatModelObservationIT extends BaseOllamaIT { .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_K.asString(), "1") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_P.asString(), "1.0") - // .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_ID.asString(), - // responseMetadata.getId()) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.RESPONSE_ID.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), "[\"stop\"]") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(responseMetadata.getUsage().getPromptTokens())) diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaEmbeddingModelObservationIT.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaEmbeddingModelObservationIT.java index 90e52030e..581d5d74e 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaEmbeddingModelObservationIT.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaEmbeddingModelObservationIT.java @@ -15,7 +15,6 @@ */ package org.springframework.ai.ollama; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; @@ -45,8 +44,6 @@ import java.io.IOException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation.HighCardinalityKeyNames; -import static org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation.LowCardinalityKeyNames; /** * Integration tests for observation instrumentation in {@link OllamaEmbeddingModel}. @@ -101,8 +98,7 @@ public class OllamaEmbeddingModelObservationIT extends BaseOllamaIT { .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(), OllamaModel.NOMIC_EMBED_TEXT.getName()) .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel()) - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString(), - KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(responseMetadata.getUsage().getPromptTokens())) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), diff --git a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaImage.java b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaImage.java index ef7b67250..3a0f38591 100644 --- a/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaImage.java +++ b/models/spring-ai-ollama/src/test/java/org/springframework/ai/ollama/OllamaImage.java @@ -22,6 +22,6 @@ import org.testcontainers.utility.DockerImageName; */ public class OllamaImage { - public static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("ollama/ollama:0.3.6"); + public static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("ollama/ollama:0.3.9"); } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelObservationIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelObservationIT.java index 3868ebf8e..362135eb4 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelObservationIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelObservationIT.java @@ -15,7 +15,6 @@ */ package org.springframework.ai.openai.chat; -import io.micrometer.common.KeyValue; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import reactor.core.publisher.Flux; @@ -143,7 +142,7 @@ public class OpenAiChatModelObservationIT { .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), "[\"this-is-the-end\"]") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7") - .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_K.asString(), KeyValue.NONE_VALUE) + .doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_TOP_K.asString()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_P.asString(), "1.0") .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_ID.asString(), responseMetadata.getId()) .hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), "[\"STOP\"]") diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java index 892c865c2..3e7e31e81 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java @@ -77,7 +77,7 @@ class OllamaWithOpenAiChatModelIT { private static final String DEFAULT_OLLAMA_MODEL = "mistral"; @Container - static OllamaContainer ollamaContainer = new OllamaContainer("ollama/ollama:0.3.6"); + static OllamaContainer ollamaContainer = new OllamaContainer("ollama/ollama:0.3.9"); static String baseUrl = "http://localhost:11434"; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.java index 957899c28..ad8b80d5e 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.java @@ -20,6 +20,7 @@ import io.micrometer.common.KeyValues; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.util.Objects; import java.util.StringJoiner; /** @@ -33,45 +34,6 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati private static final KeyValue REQUEST_MODEL_NONE = KeyValue .of(ChatModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, KeyValue.NONE_VALUE); - private static final KeyValue RESPONSE_MODEL_NONE = KeyValue - .of(ChatModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_FREQUENCY_PENALTY_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_MAX_TOKENS_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_PRESENCE_PENALTY_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_STOP_SEQUENCES_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_TEMPERATURE_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TEMPERATURE, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_TOP_K_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_TOP_P_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_P, KeyValue.NONE_VALUE); - - private static final KeyValue RESPONSE_FINISH_REASONS_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_FINISH_REASONS, KeyValue.NONE_VALUE); - - private static final KeyValue RESPONSE_ID_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_ID, KeyValue.NONE_VALUE); - - private static final KeyValue USAGE_INPUT_TOKENS_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS, KeyValue.NONE_VALUE); - - private static final KeyValue USAGE_OUTPUT_TOKENS_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS, KeyValue.NONE_VALUE); - - private static final KeyValue USAGE_TOTAL_TOKENS_NONE = KeyValue - .of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS, KeyValue.NONE_VALUE); - public static final String DEFAULT_NAME = "gen_ai.client.operation"; @Override @@ -90,8 +52,9 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati @Override public KeyValues getLowCardinalityKeyValues(ChatModelObservationContext context) { - return KeyValues.of(aiOperationType(context), aiProvider(context), requestModel(context), - responseModel(context)); + var keyValues = KeyValues.of(aiOperationType(context), aiProvider(context), requestModel(context)); + keyValues = responseModel(keyValues, context); + return keyValues; } protected KeyValue aiOperationType(ChatModelObservationContext context) { @@ -112,88 +75,107 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati return REQUEST_MODEL_NONE; } - protected KeyValue responseModel(ChatModelObservationContext context) { + protected KeyValues responseModel(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null - && context.getResponse().getMetadata().getModel() != null) { - return KeyValue.of(ChatModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL, + && StringUtils.hasText(context.getResponse().getMetadata().getModel())) { + return keyValues.and(ChatModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL.asString(), context.getResponse().getMetadata().getModel()); } - return RESPONSE_MODEL_NONE; + return keyValues; } @Override public KeyValues getHighCardinalityKeyValues(ChatModelObservationContext context) { - return KeyValues.of(requestFrequencyPenalty(context), requestMaxTokens(context), - requestPresencePenalty(context), requestStopSequences(context), requestTemperature(context), - requestTopK(context), requestTopP(context), responseFinishReasons(context), responseId(context), - usageInputTokens(context), usageOutputTokens(context), usageTotalTokens(context)); + var keyValues = KeyValues.empty(); + // Request + keyValues = requestFrequencyPenalty(keyValues, context); + keyValues = requestMaxTokens(keyValues, context); + keyValues = requestPresencePenalty(keyValues, context); + keyValues = requestStopSequences(keyValues, context); + keyValues = requestTemperature(keyValues, context); + keyValues = requestTopK(keyValues, context); + keyValues = requestTopP(keyValues, context); + // Response + keyValues = responseFinishReasons(keyValues, context); + keyValues = responseId(keyValues, context); + keyValues = usageInputTokens(keyValues, context); + keyValues = usageOutputTokens(keyValues, context); + keyValues = usageTotalTokens(keyValues, context); + return keyValues; } // Request - protected KeyValue requestFrequencyPenalty(ChatModelObservationContext context) { + protected KeyValues requestFrequencyPenalty(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getFrequencyPenalty() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), String.valueOf(context.getRequestOptions().getFrequencyPenalty())); } - return REQUEST_FREQUENCY_PENALTY_NONE; + return keyValues; } - protected KeyValue requestMaxTokens(ChatModelObservationContext context) { + protected KeyValues requestMaxTokens(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getMaxTokens() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), String.valueOf(context.getRequestOptions().getMaxTokens())); } - return REQUEST_MAX_TOKENS_NONE; + return keyValues; } - protected KeyValue requestPresencePenalty(ChatModelObservationContext context) { + protected KeyValues requestPresencePenalty(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getPresencePenalty() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), String.valueOf(context.getRequestOptions().getPresencePenalty())); } - return REQUEST_PRESENCE_PENALTY_NONE; + return keyValues; } - protected KeyValue requestStopSequences(ChatModelObservationContext context) { + protected KeyValues requestStopSequences(KeyValues keyValues, ChatModelObservationContext context) { if (!CollectionUtils.isEmpty(context.getRequestOptions().getStopSequences())) { StringJoiner stopSequencesJoiner = new StringJoiner(", ", "[", "]"); context.getRequestOptions() .getStopSequences() .forEach(value -> stopSequencesJoiner.add("\"" + value + "\"")); - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES, + KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES, + context.getRequestOptions().getStopSequences(), Objects::nonNull); + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), stopSequencesJoiner.toString()); } - return REQUEST_STOP_SEQUENCES_NONE; + return keyValues; } - protected KeyValue requestTemperature(ChatModelObservationContext context) { + protected KeyValues requestTemperature(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getTemperature() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TEMPERATURE, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), String.valueOf(context.getRequestOptions().getTemperature())); } - return REQUEST_TEMPERATURE_NONE; + return keyValues; } - protected KeyValue requestTopK(ChatModelObservationContext context) { + protected KeyValues requestTopK(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getTopK() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K, + return keyValues.and(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_K.asString(), String.valueOf(context.getRequestOptions().getTopK())); } - return REQUEST_TOP_K_NONE; + return keyValues; } - protected KeyValue requestTopP(ChatModelObservationContext context) { + protected KeyValues requestTopP(KeyValues keyValues, ChatModelObservationContext context) { if (context.getRequestOptions().getTopP() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_P, + return keyValues.and(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOP_P.asString(), String.valueOf(context.getRequestOptions().getTopP())); } - return REQUEST_TOP_P_NONE; + return keyValues; } // Response - protected KeyValue responseFinishReasons(ChatModelObservationContext context) { + protected KeyValues responseFinishReasons(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && !CollectionUtils.isEmpty(context.getResponse().getResults())) { var finishReasons = context.getResponse() .getResults() @@ -202,53 +184,57 @@ public class DefaultChatModelObservationConvention implements ChatModelObservati .map(generation -> generation.getMetadata().getFinishReason()) .toList(); if (CollectionUtils.isEmpty(finishReasons)) { - return RESPONSE_FINISH_REASONS_NONE; + return keyValues; } StringJoiner finishReasonsJoiner = new StringJoiner(", ", "[", "]"); finishReasons.forEach(finishReason -> finishReasonsJoiner.add("\"" + finishReason + "\"")); - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_FINISH_REASONS, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), finishReasonsJoiner.toString()); } - return RESPONSE_FINISH_REASONS_NONE; + return keyValues; } - protected KeyValue responseId(ChatModelObservationContext context) { + protected KeyValues responseId(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && StringUtils.hasText(context.getResponse().getMetadata().getId())) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_ID, + return keyValues.and(ChatModelObservationDocumentation.HighCardinalityKeyNames.RESPONSE_ID.asString(), context.getResponse().getMetadata().getId()); } - return RESPONSE_ID_NONE; + return keyValues; } - protected KeyValue usageInputTokens(ChatModelObservationContext context) { + protected KeyValues usageInputTokens(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && context.getResponse().getMetadata().getUsage() != null && context.getResponse().getMetadata().getUsage().getPromptTokens() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(context.getResponse().getMetadata().getUsage().getPromptTokens())); } - return USAGE_INPUT_TOKENS_NONE; + return keyValues; } - protected KeyValue usageOutputTokens(ChatModelObservationContext context) { + protected KeyValues usageOutputTokens(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && context.getResponse().getMetadata().getUsage() != null && context.getResponse().getMetadata().getUsage().getGenerationTokens() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS.asString(), String.valueOf(context.getResponse().getMetadata().getUsage().getGenerationTokens())); } - return USAGE_OUTPUT_TOKENS_NONE; + return keyValues; } - protected KeyValue usageTotalTokens(ChatModelObservationContext context) { + protected KeyValues usageTotalTokens(KeyValues keyValues, ChatModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && context.getResponse().getMetadata().getUsage() != null && context.getResponse().getMetadata().getUsage().getTotalTokens() != null) { - return KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS, + return keyValues.and( + ChatModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), String.valueOf(context.getResponse().getMetadata().getUsage().getTotalTokens())); } - return USAGE_TOTAL_TOKENS_NONE; + return keyValues; } } 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 10117088f..9b4c3163a 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 @@ -30,19 +30,6 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode 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); - - private static final KeyValue REQUEST_EMBEDDING_DIMENSION_NONE = KeyValue.of( - EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS, - KeyValue.NONE_VALUE); - - private static final KeyValue USAGE_INPUT_TOKENS_NONE = KeyValue - .of(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS, KeyValue.NONE_VALUE); - - private static final KeyValue USAGE_TOTAL_TOKENS_NONE = KeyValue - .of(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS, KeyValue.NONE_VALUE); - public static final String DEFAULT_NAME = "gen_ai.client.operation"; @Override @@ -61,8 +48,9 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode @Override public KeyValues getLowCardinalityKeyValues(EmbeddingModelObservationContext context) { - return KeyValues.of(aiOperationType(context), aiProvider(context), requestModel(context), - responseModel(context)); + var keyValues = KeyValues.of(aiOperationType(context), aiProvider(context), requestModel(context)); + keyValues = responseModel(keyValues, context); + return keyValues; } protected KeyValue aiOperationType(EmbeddingModelObservationContext context) { @@ -83,51 +71,60 @@ public class DefaultEmbeddingModelObservationConvention implements EmbeddingMode return REQUEST_MODEL_NONE; } - protected KeyValue responseModel(EmbeddingModelObservationContext context) { + protected KeyValues responseModel(KeyValues keyValues, EmbeddingModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && StringUtils.hasText(context.getResponse().getMetadata().getModel())) { - return KeyValue.of(EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL, + return keyValues.and( + EmbeddingModelObservationDocumentation.LowCardinalityKeyNames.RESPONSE_MODEL.asString(), context.getResponse().getMetadata().getModel()); } - return RESPONSE_MODEL_NONE; + return keyValues; } @Override public KeyValues getHighCardinalityKeyValues(EmbeddingModelObservationContext context) { - return KeyValues.of(requestEmbeddingDimension(context), usageInputTokens(context), usageTotalTokens(context)); + var keyValues = KeyValues.empty(); + // Request + keyValues = requestEmbeddingDimension(keyValues, context); + // Response + keyValues = usageInputTokens(keyValues, context); + keyValues = usageTotalTokens(keyValues, context); + return keyValues; } // Request - protected KeyValue requestEmbeddingDimension(EmbeddingModelObservationContext context) { + protected KeyValues requestEmbeddingDimension(KeyValues keyValues, EmbeddingModelObservationContext context) { if (context.getRequestOptions().getDimensions() != null) { - return KeyValue.of( - EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS, - String.valueOf(context.getRequestOptions().getDimensions())); + return keyValues + .and(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS + .asString(), String.valueOf(context.getRequestOptions().getDimensions())); } - return REQUEST_EMBEDDING_DIMENSION_NONE; + return keyValues; } // Response - protected KeyValue usageInputTokens(EmbeddingModelObservationContext context) { + protected KeyValues usageInputTokens(KeyValues keyValues, EmbeddingModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && context.getResponse().getMetadata().getUsage() != null && context.getResponse().getMetadata().getUsage().getPromptTokens() != null) { - return KeyValue.of(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS, + return keyValues.and( + EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), String.valueOf(context.getResponse().getMetadata().getUsage().getPromptTokens())); } - return USAGE_INPUT_TOKENS_NONE; + return keyValues; } - protected KeyValue usageTotalTokens(EmbeddingModelObservationContext context) { + protected KeyValues usageTotalTokens(KeyValues keyValues, EmbeddingModelObservationContext context) { if (context.getResponse() != null && context.getResponse().getMetadata() != null && context.getResponse().getMetadata().getUsage() != null && context.getResponse().getMetadata().getUsage().getTotalTokens() != null) { - return KeyValue.of(EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS, + return keyValues.and( + EmbeddingModelObservationDocumentation.HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), String.valueOf(context.getResponse().getMetadata().getUsage().getTotalTokens())); } - return USAGE_TOTAL_TOKENS_NONE; + return keyValues; } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/image/observation/DefaultImageModelObservationConvention.java b/spring-ai-core/src/main/java/org/springframework/ai/image/observation/DefaultImageModelObservationConvention.java index cc7e6e40f..2413e51e4 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/image/observation/DefaultImageModelObservationConvention.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/image/observation/DefaultImageModelObservationConvention.java @@ -30,16 +30,6 @@ public class DefaultImageModelObservationConvention implements ImageModelObserva private static final KeyValue REQUEST_MODEL_NONE = KeyValue .of(ImageModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL, KeyValue.NONE_VALUE); - private static final KeyValue REQUEST_IMAGE_RESPONSE_FORMAT_NONE = KeyValue.of( - ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT, - KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_IMAGE_SIZE_NONE = KeyValue - .of(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE, KeyValue.NONE_VALUE); - - private static final KeyValue REQUEST_IMAGE_STYLE_NONE = KeyValue - .of(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_STYLE, KeyValue.NONE_VALUE); - public static final String DEFAULT_NAME = "gen_ai.client.operation"; @Override @@ -81,33 +71,41 @@ public class DefaultImageModelObservationConvention implements ImageModelObserva @Override public KeyValues getHighCardinalityKeyValues(ImageModelObservationContext context) { - return KeyValues.of(requestImageFormat(context), requestImageSize(context), requestImageStyle(context)); + var keyValues = KeyValues.empty(); + // Request + keyValues = requestImageFormat(keyValues, context); + keyValues = requestImageSize(keyValues, context); + keyValues = requestImageStyle(keyValues, context); + return keyValues; } // Request - protected KeyValue requestImageFormat(ImageModelObservationContext context) { + protected KeyValues requestImageFormat(KeyValues keyValues, ImageModelObservationContext context) { if (StringUtils.hasText(context.getRequestOptions().getResponseFormat())) { - return KeyValue.of(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT, + return keyValues.and( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT.asString(), context.getRequestOptions().getResponseFormat()); } - return REQUEST_IMAGE_RESPONSE_FORMAT_NONE; + return keyValues; } - protected KeyValue requestImageSize(ImageModelObservationContext context) { + protected KeyValues requestImageSize(KeyValues keyValues, ImageModelObservationContext context) { if (context.getRequestOptions().getWidth() != null && context.getRequestOptions().getHeight() != null) { - return KeyValue.of(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE, + return keyValues.and( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), "%sx%s".formatted(context.getRequestOptions().getWidth(), context.getRequestOptions().getHeight())); } - return REQUEST_IMAGE_SIZE_NONE; + return keyValues; } - protected KeyValue requestImageStyle(ImageModelObservationContext context) { + protected KeyValues requestImageStyle(KeyValues keyValues, ImageModelObservationContext context) { if (StringUtils.hasText(context.getRequestOptions().getStyle())) { - return KeyValue.of(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_STYLE, + return keyValues.and( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString(), context.getRequestOptions().getStyle()); } - return REQUEST_IMAGE_STYLE_NONE; + return keyValues; } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiOperationType.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiOperationType.java index 7d6fe52ff..85fa4f2a2 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiOperationType.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiOperationType.java @@ -29,6 +29,7 @@ public enum AiOperationType { // @formatter:off + // Please, keep the alphabetical sorting. CHAT("chat"), EMBEDDING("embedding"), FRAMEWORK("framework"), diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java index 4d0a6acbe..7a7fad4c8 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java @@ -29,14 +29,15 @@ public enum AiProvider { // @formatter:off + // Please, keep the alphabetical sorting. ANTHROPIC("anthropic"), + AZURE_OPENAI("azure-openai"), MISTRAL_AI("mistral_ai"), + OCI_GENAI("oci_genai"), OLLAMA("ollama"), OPENAI("openai"), SPRING_AI("spring_ai"), - VERTEX_AI("vertex_ai"), - OCI_GENAI("oci_genai"), - AZURE_OPENAI("azure-openai"); + VERTEX_AI("vertex_ai"); private final String value; diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConventionTests.java index dda0cc95b..2885a35a0 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConventionTests.java @@ -133,32 +133,34 @@ class DefaultChatModelObservationConventionTests { } @Test - void shouldHaveNoneKeyValuesWhenMissing() { + void shouldNotHaveKeyValuesWhenMissing() { ChatModelObservationContext observationContext = ChatModelObservationContext.builder() .prompt(generatePrompt()) .provider("superprovider") .requestOptions(ChatOptionsBuilder.builder().build()) .build(); - assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains( - KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE), - KeyValue.of(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), KeyValue.NONE_VALUE)); - assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( - KeyValue.of(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_TOP_K.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.REQUEST_TOP_P.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.RESPONSE_ID.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .contains(KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .noneMatch(keyValue -> keyValue.getKey().equals(LowCardinalityKeyNames.RESPONSE_MODEL.asString())); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), + HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), + HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), + HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), + HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), + HighCardinalityKeyNames.REQUEST_TOP_K.asString(), HighCardinalityKeyNames.REQUEST_TOP_P.asString(), + HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), + HighCardinalityKeyNames.RESPONSE_ID.asString(), + HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), + HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS.asString(), + HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString()); } @Test - void shouldHaveNoneKeyValuesWhenEmptyValues() { + void shouldNotHaveKeyValuesWhenEmptyValues() { ChatModelObservationContext observationContext = ChatModelObservationContext.builder() .prompt(generatePrompt()) .provider("superprovider") @@ -167,10 +169,12 @@ class DefaultChatModelObservationConventionTests { observationContext.setResponse(new ChatResponse( List.of(new Generation(new AssistantMessage("response"), ChatGenerationMetadata.from("", null))), ChatResponseMetadata.builder().withId("").build())); - assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( - KeyValue.of(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.RESPONSE_ID.asString(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(), + HighCardinalityKeyNames.RESPONSE_FINISH_REASONS.asString(), + HighCardinalityKeyNames.RESPONSE_ID.asString()); } private Prompt generatePrompt() { diff --git a/spring-ai-core/src/test/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConventionTests.java index d6d4ceb7a..ff101b31e 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/embedding/observation/DefaultEmbeddingModelObservationConventionTests.java @@ -108,19 +108,22 @@ class DefaultEmbeddingModelObservationConventionTests { } @Test - void shouldHaveNoneKeyValuesWhenMissing() { + void shouldNotHaveKeyValuesWhenMissing() { EmbeddingModelObservationContext observationContext = EmbeddingModelObservationContext.builder() .embeddingRequest(generateEmbeddingRequest()) .provider("superprovider") .requestOptions(EmbeddingOptionsBuilder.builder().build()) .build(); - assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains( - KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE), - KeyValue.of(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), KeyValue.NONE_VALUE)); - assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( - KeyValue.of(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), KeyValue.NONE_VALUE), - KeyValue.of(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .contains(KeyValue.of(LowCardinalityKeyNames.REQUEST_MODEL.asString(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) + .noneMatch(keyValue -> keyValue.getKey().equals(LowCardinalityKeyNames.RESPONSE_MODEL.asString())); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_EMBEDDING_DIMENSIONS.asString(), + HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(), + HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString()); } private EmbeddingRequest generateEmbeddingRequest() { diff --git a/spring-ai-core/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java index 10ddafbc1..5c8951de3 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/image/observation/DefaultImageModelObservationConventionTests.java @@ -23,6 +23,7 @@ import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.observation.conventions.AiObservationAttributes; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.image.observation.ImageModelObservationDocumentation.HighCardinalityKeyNames; /** * Unit tests for {@link DefaultImageModelObservationConvention}. @@ -104,7 +105,7 @@ class DefaultImageModelObservationConventionTests { } @Test - void shouldHaveNoneKeyValuesWhenMissing() { + void shouldNotHaveKeyValuesWhenEmptyValues() { ImageModelObservationContext observationContext = ImageModelObservationContext.builder() .imagePrompt(generateImagePrompt()) .provider("superprovider") @@ -113,10 +114,12 @@ class DefaultImageModelObservationConventionTests { assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)) .contains(KeyValue.of(AiObservationAttributes.REQUEST_MODEL.value(), KeyValue.NONE_VALUE)); - assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains( - KeyValue.of(AiObservationAttributes.REQUEST_IMAGE_RESPONSE_FORMAT.value(), KeyValue.NONE_VALUE), - KeyValue.of(AiObservationAttributes.REQUEST_IMAGE_SIZE.value(), KeyValue.NONE_VALUE), - KeyValue.of(AiObservationAttributes.REQUEST_IMAGE_STYLE.value(), KeyValue.NONE_VALUE)); + assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext) + .stream() + .map(KeyValue::getKey) + .toList()).doesNotContain(HighCardinalityKeyNames.REQUEST_IMAGE_RESPONSE_FORMAT.asString(), + HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), + HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString()); } private ImagePrompt generateImagePrompt() { diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java index 087dcff9e..873c3e2ef 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java @@ -17,6 +17,6 @@ package org.springframework.ai.autoconfigure.ollama; public class OllamaImage { - public static final String IMAGE = "ollama/ollama:0.3.6"; + public static final String IMAGE = "ollama/ollama:0.3.9"; } diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java index ba23c1347..b5f4fd296 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryTest.java @@ -55,7 +55,7 @@ class OllamaContainerConnectionDetailsFactoryTest { @Container @ServiceConnection - static OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.3.6"); + static OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.3.9"); @Autowired private OllamaEmbeddingModel embeddingModel;