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 <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2024-10-05 23:28:18 +02:00
committed by Christian Tzolov
parent dc0476478c
commit 3b1c68ac10
19 changed files with 206 additions and 208 deletions

View File

@@ -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")

View File

@@ -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(

View File

@@ -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()))

View File

@@ -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(),

View File

@@ -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()))

View File

@@ -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(),

View File

@@ -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");
}

View File

@@ -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\"]")

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -29,6 +29,7 @@ public enum AiOperationType {
// @formatter:off
// Please, keep the alphabetical sorting.
CHAT("chat"),
EMBEDDING("embedding"),
FRAMEWORK("framework"),

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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";
}

View File

@@ -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;