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 e257d8a4fbe..17ef19c909e 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.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 @@ private void validate(ChatResponseMetadata responseMetadata, String finishReason .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 df4907a09ee..5a24b5cc87b 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.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 @@ private void validate(ChatResponseMetadata responseMetadata) { "[\"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 00f56ba0dad..38b3c9026fb 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 @@ private void validate(ChatResponseMetadata responseMetadata) { .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 a49370ce273..55f634c0ca1 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 @@ void observationForEmbeddingOperation() { .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 81fdbb365c5..d72c7067a31 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 @@ private void validate(ChatResponseMetadata responseMetadata) { .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 90e52030e2b..581d5d74e07 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.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 @@ void observationForEmbeddingOperation() { .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 ef7b6725036..3a0f3859125 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 @@ */ 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 3868ebf8ef5..362135eb4f4 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 @@ private void validate(ChatResponseMetadata responseMetadata) { .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 892c865c2f3..3e7e31e81ce 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 957899c287f..ad8b80d5e44 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 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 String getContextualName(ChatModelObservationContext context) { @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 @@ protected KeyValue requestModel(ChatModelObservationContext context) { 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 @@ protected KeyValue responseFinishReasons(ChatModelObservationContext context) { .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 10117088f42..9b4c3163a82 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 String getContextualName(EmbeddingModelObservationContext context) { @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 @@ protected KeyValue requestModel(EmbeddingModelObservationContext context) { 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 cc7e6e40f3a..2413e51e41c 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 @@ protected KeyValue requestModel(ImageModelObservationContext context) { @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 7d6fe52ff4a..85fa4f2a26a 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 4d0a6acbe71..7a7fad4c816 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 dda0cc95b95..2885a35a0d7 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 @@ void shouldHaveKeyValuesWhenDefinedAndResponse() { } @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 @@ void shouldHaveNoneKeyValuesWhenEmptyValues() { 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 d6d4ceb7abc..ff101b31edf 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 @@ void shouldHaveLowCardinalityKeyValuesWhenDefinedAndResponse() { } @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 10ddafbc1c0..5c8951de3b3 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.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 @@ void shouldHaveHighCardinalityKeyValuesWhenDefined() { } @Test - void shouldHaveNoneKeyValuesWhenMissing() { + void shouldNotHaveKeyValuesWhenEmptyValues() { ImageModelObservationContext observationContext = ImageModelObservationContext.builder() .imagePrompt(generateImagePrompt()) .provider("superprovider") @@ -113,10 +114,12 @@ void shouldHaveNoneKeyValuesWhenMissing() { 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 087dcff9ebb..873c3e2ef09 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 @@ 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 ba23c134706..b5f4fd2967b 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;