diff --git a/models/spring-ai-minimax/pom.xml b/models/spring-ai-minimax/pom.xml
index 3ded87596..ee8ea6c07 100644
--- a/models/spring-ai-minimax/pom.xml
+++ b/models/spring-ai-minimax/pom.xml
@@ -54,6 +54,11 @@
test
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java
index e5d1cf72d..e8aca8bdb 100644
--- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java
+++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java
@@ -15,6 +15,9 @@
*/
package org.springframework.ai.minimax;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -22,12 +25,19 @@ import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.EmptyUsage;
import org.springframework.ai.chat.model.AbstractToolCallSupport;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.model.MessageAggregator;
import org.springframework.ai.chat.model.StreamingChatModel;
+import org.springframework.ai.chat.observation.ChatModelObservationContext;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
+import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletion;
@@ -40,6 +50,7 @@ import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.Role;
import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.ToolCall;
import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionRequest;
import org.springframework.ai.minimax.api.MiniMaxApi.FunctionTool;
+import org.springframework.ai.minimax.api.MiniMaxApiConstants;
import org.springframework.ai.minimax.metadata.MiniMaxUsage;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
@@ -57,7 +68,6 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -77,6 +87,8 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
private static final Logger logger = LoggerFactory.getLogger(MiniMaxChatModel.class);
+ private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
+
/**
* The default options used for the chat completion requests.
*/
@@ -92,6 +104,16 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
*/
private final MiniMaxApi miniMaxApi;
+ /**
+ * Observation registry used for instrumentation.
+ */
+ private final ObservationRegistry observationRegistry;
+
+ /**
+ * Conventions to use for generating observations.
+ */
+ private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
+
/**
* Creates an instance of the MiniMaxChatModel.
* @param miniMaxApi The MiniMaxApi instance to be used for interacting with the
@@ -123,7 +145,7 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
*/
public MiniMaxChatModel(MiniMaxApi miniMaxApi, MiniMaxChatOptions options,
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
- this(miniMaxApi, options, functionCallbackContext, List.of(), retryTemplate);
+ this(miniMaxApi, options, functionCallbackContext, List.of(), retryTemplate, ObservationRegistry.NOOP);
}
/**
@@ -134,72 +156,91 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
* @param functionCallbackContext The function callback context.
* @param toolFunctionCallbacks The tool function callbacks.
* @param retryTemplate The retry template.
+ * @param observationRegistry The ObservationRegistry used for instrumentation.
*/
public MiniMaxChatModel(MiniMaxApi miniMaxApi, MiniMaxChatOptions options,
FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks,
- RetryTemplate retryTemplate) {
+ RetryTemplate retryTemplate, ObservationRegistry observationRegistry) {
super(functionCallbackContext, options, toolFunctionCallbacks);
Assert.notNull(miniMaxApi, "MiniMaxApi must not be null");
Assert.notNull(options, "Options must not be null");
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
Assert.isTrue(CollectionUtils.isEmpty(options.getFunctionCallbacks()),
"The default function callbacks must be set via the toolFunctionCallbacks constructor parameter");
+ Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
this.miniMaxApi = miniMaxApi;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
+ this.observationRegistry = observationRegistry;
}
@Override
public ChatResponse call(Prompt prompt) {
ChatCompletionRequest request = createRequest(prompt, false);
- ResponseEntity completionEntity = this.retryTemplate
- .execute(ctx -> this.miniMaxApi.chatCompletionEntity(request));
+ ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
+ .prompt(prompt)
+ .provider(MiniMaxApiConstants.PROVIDER_NAME)
+ .requestOptions(buildRequestOptions(request))
+ .build();
- var chatCompletion = completionEntity.getBody();
+ ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
+ .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
+ this.observationRegistry)
+ .observe(() -> {
- if (chatCompletion == null) {
- logger.warn("No chat completion returned for prompt: {}", prompt);
- return new ChatResponse(List.of());
- }
+ ResponseEntity completionEntity = this.retryTemplate
+ .execute(ctx -> this.miniMaxApi.chatCompletionEntity(request));
- List choices = chatCompletion.choices();
- if (choices == null) {
- logger.warn("No choices returned for prompt: {}, because: {}}", prompt,
- chatCompletion.baseResponse().message());
- return new ChatResponse(List.of());
- }
+ var chatCompletion = completionEntity.getBody();
- List generations = choices.stream().map(choice -> {
+ if (chatCompletion == null) {
+ logger.warn("No chat completion returned for prompt: {}", prompt);
+ return new ChatResponse(List.of());
+ }
+
+ List choices = chatCompletion.choices();
+ if (choices == null) {
+ logger.warn("No choices returned for prompt: {}, because: {}}", prompt,
+ chatCompletion.baseResponse().message());
+ return new ChatResponse(List.of());
+ }
+
+ List generations = choices.stream().map(choice -> {
// @formatter:off
- // if the choice is a web search tool call, return last message of choice.messages
- ChatCompletionMessage message = null;
- if(choice.message() != null) {
- message = choice.message();
- } else if(!CollectionUtils.isEmpty(choice.messages())){
- // the MiniMax web search messages result is ['user message','assistant tool call', 'tool call', 'assistant message']
- // so the last message is the assistant message
- message = choice.messages().get(choice.messages().size() - 1);
- }
- Map metadata = Map.of(
- "id", chatCompletion.id(),
- "role", message != null && message.role() != null ? message.role().name() : "",
- "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "");
- // @formatter:on
- return buildGeneration(choice, metadata);
- }).toList();
+ // if the choice is a web search tool call, return last message of choice.messages
+ ChatCompletionMessage message = null;
+ if(choice.message() != null) {
+ message = choice.message();
+ } else if(!CollectionUtils.isEmpty(choice.messages())){
+ // the MiniMax web search messages result is ['user message','assistant tool call', 'tool call', 'assistant message']
+ // so the last message is the assistant message
+ message = choice.messages().get(choice.messages().size() - 1);
+ }
+ Map metadata = Map.of(
+ "id", chatCompletion.id(),
+ "role", message != null && message.role() != null ? message.role().name() : "",
+ "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "");
+ // @formatter:on
+ return buildGeneration(message, choice.finishReason(), metadata);
+ }).toList();
- ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody()));
+ ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody()));
- if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(chatResponse,
+ observationContext.setResponse(chatResponse);
+
+ return chatResponse;
+ });
+
+ if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response,
Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
- var toolCallConversation = handleToolCalls(prompt, chatResponse);
+ var toolCallConversation = handleToolCalls(prompt, response);
// Recursively call the call method with the tool call message
// conversation that contains the call responses.
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
}
- return chatResponse;
+ return response;
}
@Override
@@ -209,60 +250,70 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
@Override
public Flux stream(Prompt prompt) {
- ChatCompletionRequest request = createRequest(prompt, true);
+ return Flux.deferContextual(contextView -> {
+ ChatCompletionRequest request = createRequest(prompt, true);
- Flux completionChunks = this.retryTemplate
- .execute(ctx -> this.miniMaxApi.chatCompletionStream(request));
+ Flux completionChunks = this.retryTemplate
+ .execute(ctx -> this.miniMaxApi.chatCompletionStream(request));
- // For chunked responses, only the first chunk contains the choice role.
- // The rest of the chunks with same ID share the same role.
- ConcurrentHashMap roleMap = new ConcurrentHashMap<>();
+ // For chunked responses, only the first chunk contains the choice role.
+ // The rest of the chunks with same ID share the same role.
+ ConcurrentHashMap roleMap = new ConcurrentHashMap<>();
- // Convert the ChatCompletionChunk into a ChatCompletion to be able to reuse
- // the function call handling logic.
- Flux chatResponse = completionChunks.map(this::chunkToChatCompletion)
- .switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> {
- try {
- @SuppressWarnings("null")
- String id = chatCompletion2.id();
+ final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
+ .prompt(prompt)
+ .provider(MiniMaxApiConstants.PROVIDER_NAME)
+ .requestOptions(buildRequestOptions(request))
+ .build();
- // @formatter:off
- List generations = chatCompletion2.choices().stream().map(choice -> {
- if (choice.message().role() != null) {
- roleMap.putIfAbsent(id, choice.message().role().name());
- }
- Map metadata = Map.of(
- "id", chatCompletion2.id(),
- "role", roleMap.getOrDefault(id, ""),
- "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "");
- return buildGeneration(choice, metadata);
- }).filter(Objects::nonNull).toList();
- // @formatter:on
+ Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
+ this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
+ this.observationRegistry);
- if (chatCompletion2.usage() != null) {
- return new ChatResponse(generations, from(chatCompletion2));
- }
- else {
- return new ChatResponse(generations);
- }
- }
- catch (Exception e) {
- logger.error("Error processing chat completion", e);
- return new ChatResponse(List.of());
- }
+ observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
- }));
+ // Convert the ChatCompletionChunk into a ChatCompletion to be able to reuse
+ // the function call handling logic.
+ Flux chatResponse = completionChunks.map(this::chunkToChatCompletion)
+ .switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> {
+ try {
+ @SuppressWarnings("null")
+ String id = chatCompletion2.id();
- return chatResponse.flatMap(response -> {
+ // @formatter:off
+ List generations = chatCompletion2.choices().stream().map(choice -> {
+ if (choice.message().role() != null) {
+ roleMap.putIfAbsent(id, choice.message().role().name());
+ }
+ Map metadata = Map.of(
+ "id", chatCompletion2.id(),
+ "role", roleMap.getOrDefault(id, ""),
+ "finishReason", choice.finishReason() != null ? choice.finishReason().name() : "");
+ return buildGeneration(choice, metadata);
+ }).toList();
+ return new ChatResponse(generations, from(chatCompletion2));
+ } catch (Exception e) {
+ logger.error("Error processing chat completion", e);
+ return new ChatResponse(List.of());
+ }
+ }));
- if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response,
- Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
- var toolCallConversation = handleToolCalls(prompt, response);
- // Recursively call the stream method with the tool call message
- // conversation that contains the call responses.
- return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
- }
- return Flux.just(response);
+ Flux flux = chatResponse.flatMap(response -> {
+ if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response,
+ Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) {
+ var toolCallConversation = handleToolCalls(prompt, response);
+ // Recursively call the stream method with the tool call message
+ // conversation that contains the call responses.
+ return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
+ }
+ return Flux.just(response);
+ })
+ .doOnError(observation::error)
+ .doFinally(signalType -> observation.stop())
+ .contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
+ // @formatter:on
+
+ return new MessageAggregator().aggregate(flux, observationContext::setResponse);
});
}
@@ -284,16 +335,47 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
.anyMatch(toolCall -> TOOL_CALL_FUNCTION_TYPE.equals(toolCall.type()));
}
+ private ChatOptions buildRequestOptions(ChatCompletionRequest request) {
+ return ChatOptionsBuilder.builder()
+ .withModel(request.model())
+ .withFrequencyPenalty(request.frequencyPenalty())
+ .withMaxTokens(request.maxTokens())
+ .withPresencePenalty(request.presencePenalty())
+ .withStopSequences(request.stop())
+ .withTemperature(request.temperature())
+ .withTopP(request.topP())
+ .build();
+ }
+
private ChatResponseMetadata from(ChatCompletion result) {
Assert.notNull(result, "MiniMax ChatCompletionResult must not be null");
return ChatResponseMetadata.builder()
- .withId(result.id())
- .withUsage(MiniMaxUsage.from(result.usage()))
- .withModel(result.model())
- .withKeyValue("created", result.created())
+ .withId(result.id() != null ? result.id() : "")
+ .withUsage(result.usage() != null ? MiniMaxUsage.from(result.usage()) : new EmptyUsage())
+ .withModel(result.model() != null ? result.model() : "")
+ .withKeyValue("created", result.created() != null ? result.created() : 0L)
+ .withKeyValue("system-fingerprint", result.systemFingerprint() != null ? result.systemFingerprint() : "")
.build();
}
+ private Generation buildGeneration(ChatCompletionMessage message, ChatCompletionFinishReason completionFinishReason,
+ Map metadata) {
+ if (message == null || message.role() == Role.TOOL) {
+ return null;
+ }
+ List toolCalls = message.toolCalls() == null ? List.of()
+ : message.toolCalls()
+ .stream()
+ .map(toolCall -> new AssistantMessage.ToolCall(toolCall.id(), toolCall.type(),
+ toolCall.function().name(), toolCall.function().arguments()))
+ .toList();
+
+ var assistantMessage = new AssistantMessage(message.content(), metadata, toolCalls);
+ String finishReason = (completionFinishReason != null ? completionFinishReason.name() : "");
+ var generationMetadata = ChatGenerationMetadata.from(finishReason, null);
+ return new Generation(assistantMessage, generationMetadata);
+ }
+
private static Generation buildGeneration(Choice choice, Map metadata) {
List toolCalls = choice.message().toolCalls() == null ? List.of()
: choice.message()
@@ -432,4 +514,8 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod
}).toList();
}
+ public void setObservationConvention(ChatModelObservationConvention observationConvention) {
+ this.observationConvention = observationConvention;
+ }
+
}
diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingModel.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingModel.java
index cef854e5f..06c219a0e 100644
--- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingModel.java
+++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxEmbeddingModel.java
@@ -15,6 +15,7 @@
*/
package org.springframework.ai.minimax;
+import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
@@ -25,10 +26,16 @@ import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
+import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation;
import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.minimax.api.MiniMaxApiConstants;
import org.springframework.ai.minimax.metadata.MiniMaxUsage;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.retry.RetryUtils;
+import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
@@ -46,6 +53,8 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
private static final Logger logger = LoggerFactory.getLogger(MiniMaxEmbeddingModel.class);
+ private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();
+
private final MiniMaxEmbeddingOptions defaultOptions;
private final RetryTemplate retryTemplate;
@@ -54,6 +63,16 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
private final MetadataMode metadataMode;
+ /**
+ * Observation registry used for instrumentation.
+ */
+ private final ObservationRegistry observationRegistry;
+
+ /**
+ * Conventions to use for generating observations.
+ */
+ private EmbeddingModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
+
/**
* Constructor for the MiniMaxEmbeddingModel class.
* @param miniMaxApi The MiniMaxApi instance to use for making API requests.
@@ -70,7 +89,7 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
public MiniMaxEmbeddingModel(MiniMaxApi miniMaxApi, MetadataMode metadataMode) {
this(miniMaxApi, metadataMode,
MiniMaxEmbeddingOptions.builder().withModel(MiniMaxApi.DEFAULT_EMBEDDING_MODEL).build(),
- RetryUtils.DEFAULT_RETRY_TEMPLATE);
+ RetryUtils.DEFAULT_RETRY_TEMPLATE, ObservationRegistry.NOOP);
}
/**
@@ -81,7 +100,20 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
*/
public MiniMaxEmbeddingModel(MiniMaxApi miniMaxApi, MetadataMode metadataMode,
MiniMaxEmbeddingOptions miniMaxEmbeddingOptions) {
- this(miniMaxApi, metadataMode, miniMaxEmbeddingOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE);
+ this(miniMaxApi, metadataMode, miniMaxEmbeddingOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE,
+ ObservationRegistry.NOOP);
+ }
+
+ /**
+ * Initializes a new instance of the MiniMaxEmbeddingModel class.
+ * @param miniMaxApi The MiniMaxApi instance to use for making API requests.
+ * @param metadataMode The mode for generating metadata.
+ * @param miniMaxEmbeddingOptions The options for MiniMax embedding.
+ * @param retryTemplate - The RetryTemplate for retrying failed API requests.
+ */
+ public MiniMaxEmbeddingModel(MiniMaxApi miniMaxApi, MetadataMode metadataMode,
+ MiniMaxEmbeddingOptions miniMaxEmbeddingOptions, RetryTemplate retryTemplate) {
+ this(miniMaxApi, metadataMode, miniMaxEmbeddingOptions, retryTemplate, ObservationRegistry.NOOP);
}
/**
@@ -90,18 +122,21 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
* @param metadataMode - The mode for generating metadata.
* @param options - The options for MiniMax embedding.
* @param retryTemplate - The RetryTemplate for retrying failed API requests.
+ * @param observationRegistry - The ObservationRegistry used for instrumentation.
*/
public MiniMaxEmbeddingModel(MiniMaxApi miniMaxApi, MetadataMode metadataMode, MiniMaxEmbeddingOptions options,
- RetryTemplate retryTemplate) {
+ RetryTemplate retryTemplate, ObservationRegistry observationRegistry) {
Assert.notNull(miniMaxApi, "MiniMaxApi must not be null");
Assert.notNull(metadataMode, "metadataMode must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
+ Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.miniMaxApi = miniMaxApi;
this.metadataMode = metadataMode;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
+ this.observationRegistry = observationRegistry;
}
@Override
@@ -110,38 +145,64 @@ public class MiniMaxEmbeddingModel extends AbstractEmbeddingModel {
return this.embed(document.getFormattedContent(this.metadataMode));
}
- @SuppressWarnings("unchecked")
@Override
public EmbeddingResponse call(EmbeddingRequest request) {
+ MiniMaxEmbeddingOptions requestOptions = mergeOptions(request.getOptions(), this.defaultOptions);
+ MiniMaxApi.EmbeddingRequest apiRequest = new MiniMaxApi.EmbeddingRequest(request.getInstructions(),
+ requestOptions.getModel());
- return this.retryTemplate.execute(ctx -> {
+ var observationContext = EmbeddingModelObservationContext.builder()
+ .embeddingRequest(request)
+ .provider(MiniMaxApiConstants.PROVIDER_NAME)
+ .requestOptions(requestOptions)
+ .build();
- MiniMaxApi.EmbeddingRequest apiRequest = (this.defaultOptions != null)
- ? new MiniMaxApi.EmbeddingRequest(request.getInstructions(), this.defaultOptions.getModel())
- : new MiniMaxApi.EmbeddingRequest(request.getInstructions(), MiniMaxApi.DEFAULT_EMBEDDING_MODEL);
+ return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
+ .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
+ this.observationRegistry)
+ .observe(() -> {
+ MiniMaxApi.EmbeddingList apiEmbeddingResponse = this.retryTemplate
+ .execute(ctx -> this.miniMaxApi.embeddings(apiRequest).getBody());
- if (request.getOptions() != null && !EmbeddingOptions.EMPTY.equals(request.getOptions())) {
- apiRequest = ModelOptionsUtils.merge(request.getOptions(), apiRequest,
- MiniMaxApi.EmbeddingRequest.class);
- }
+ if (apiEmbeddingResponse == null) {
+ logger.warn("No embeddings returned for request: {}", request);
+ return new EmbeddingResponse(List.of());
+ }
- MiniMaxApi.EmbeddingList apiEmbeddingResponse = this.miniMaxApi.embeddings(apiRequest).getBody();
+ var metadata = new EmbeddingResponseMetadata(apiRequest.model(),
+ MiniMaxUsage.from(new MiniMaxApi.Usage(0, 0, apiEmbeddingResponse.totalTokens())));
- if (apiEmbeddingResponse == null) {
- logger.warn("No embeddings returned for request: {}", request);
- return new EmbeddingResponse(List.of());
- }
+ List embeddings = new ArrayList<>();
+ for (int i = 0; i < apiEmbeddingResponse.vectors().size(); i++) {
+ float[] vector = apiEmbeddingResponse.vectors().get(i);
+ embeddings.add(new Embedding(vector, i));
+ }
+ EmbeddingResponse embeddingResponse = new EmbeddingResponse(embeddings, metadata);
+ observationContext.setResponse(embeddingResponse);
+ return embeddingResponse;
+ });
+ }
- var metadata = new EmbeddingResponseMetadata(apiEmbeddingResponse.model(),
- MiniMaxUsage.from(new MiniMaxApi.Usage(0, 0, apiEmbeddingResponse.totalTokens())));
+ /**
+ * Merge runtime and default {@link EmbeddingOptions} to compute the final options to
+ * use in the request.
+ */
+ private MiniMaxEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions,
+ MiniMaxEmbeddingOptions defaultOptions) {
+ var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, EmbeddingOptions.class,
+ MiniMaxEmbeddingOptions.class);
- List embeddings = new ArrayList<>();
- for (int i = 0; i < apiEmbeddingResponse.vectors().size(); i++) {
- float[] vector = apiEmbeddingResponse.vectors().get(i);
- embeddings.add(new Embedding(vector, i));
- }
- return new EmbeddingResponse(embeddings, metadata);
- });
+ var optionBuilder = MiniMaxEmbeddingOptions.builder();
+ if (runtimeOptionsForProvider != null && runtimeOptionsForProvider.getModel() != null) {
+ optionBuilder.withModel(runtimeOptionsForProvider.getModel());
+ }
+ else if (defaultOptions.getModel() != null) {
+ optionBuilder.withModel(defaultOptions.getModel());
+ }
+ else {
+ optionBuilder.withModel(MiniMaxApi.DEFAULT_EMBEDDING_MODEL);
+ }
+ return optionBuilder.build();
}
}
diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApiConstants.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApiConstants.java
index 1b2e797d8..a8ed1b34a 100644
--- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApiConstants.java
+++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApiConstants.java
@@ -1,5 +1,7 @@
package org.springframework.ai.minimax.api;
+import org.springframework.ai.observation.conventions.AiProvider;
+
/**
* Common value constants for MiniMax api.
*
@@ -12,4 +14,6 @@ public final class MiniMaxApiConstants {
public static final String TOOL_CALL_FUNCTION_TYPE = "function";
+ public static final String PROVIDER_NAME = AiProvider.MINIMAX.value();
+
}
diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/metadata/MiniMaxUsage.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/metadata/MiniMaxUsage.java
index 36409e5ef..a720be0e4 100644
--- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/metadata/MiniMaxUsage.java
+++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/metadata/MiniMaxUsage.java
@@ -43,17 +43,23 @@ public class MiniMaxUsage implements Usage {
@Override
public Long getPromptTokens() {
- return getUsage().promptTokens().longValue();
+ Integer promptTokens = getUsage().promptTokens();
+ return promptTokens != null ? promptTokens.longValue() : 0;
}
@Override
public Long getGenerationTokens() {
- return getUsage().completionTokens().longValue();
+ Integer generationTokens = getUsage().completionTokens();
+ return generationTokens != null ? generationTokens.longValue() : 0;
}
@Override
public Long getTotalTokens() {
- return getUsage().totalTokens().longValue();
+ Integer totalTokens = getUsage().totalTokens();
+ if (totalTokens != null) {
+ return totalTokens.longValue();
+ }
+ return getPromptTokens() + getGenerationTokens();
}
@Override
diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxRetryTests.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxRetryTests.java
index 0978b4a8b..a9847114b 100644
--- a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxRetryTests.java
+++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxRetryTests.java
@@ -151,7 +151,7 @@ public class MiniMaxRetryTests {
public void miniMaxChatStreamNonTransientError() {
when(miniMaxApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.thenThrow(new RuntimeException("Non Transient Error"));
- assertThrows(RuntimeException.class, () -> chatModel.stream(new Prompt("text")));
+ assertThrows(RuntimeException.class, () -> chatModel.stream(new Prompt("text")).collectList().block());
}
@Test
diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/chat/MiniMaxChatModelObservationIT.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/chat/MiniMaxChatModelObservationIT.java
new file mode 100644
index 000000000..486456fa2
--- /dev/null
+++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/chat/MiniMaxChatModelObservationIT.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.minimax.chat;
+
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxChatOptions;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.model.function.FunctionCallbackContext;
+import org.springframework.ai.observation.conventions.AiOperationType;
+import org.springframework.ai.observation.conventions.AiProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.ai.chat.observation.ChatModelObservationDocumentation.HighCardinalityKeyNames;
+import static org.springframework.ai.chat.observation.ChatModelObservationDocumentation.LowCardinalityKeyNames;
+
+/**
+ * Integration tests for observation instrumentation in {@link MiniMaxChatModel}.
+ *
+ * @author Geng Rong
+ */
+@SpringBootTest(classes = MiniMaxChatModelObservationIT.Config.class)
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".+")
+public class MiniMaxChatModelObservationIT {
+
+ @Autowired
+ TestObservationRegistry observationRegistry;
+
+ @Autowired
+ MiniMaxChatModel chatModel;
+
+ @BeforeEach
+ void beforeEach() {
+ observationRegistry.clear();
+ }
+
+ @Test
+ void observationForChatOperation() {
+
+ var options = MiniMaxChatOptions.builder()
+ .withModel(MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.getValue())
+ .withFrequencyPenalty(0.0)
+ .withMaxTokens(2048)
+ .withPresencePenalty(0.0)
+ .withStop(List.of("this-is-the-end"))
+ .withTemperature(0.7)
+ .withTopP(1.0)
+ .build();
+
+ Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
+
+ ChatResponse chatResponse = chatModel.call(prompt);
+ assertThat(chatResponse.getResult().getOutput().getContent()).isNotEmpty();
+
+ ChatResponseMetadata responseMetadata = chatResponse.getMetadata();
+ assertThat(responseMetadata).isNotNull();
+
+ validate(responseMetadata);
+ }
+
+ @Test
+ void observationForStreamingChatOperation() {
+ var options = MiniMaxChatOptions.builder()
+ .withModel(MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.getValue())
+ .withFrequencyPenalty(0.0)
+ .withMaxTokens(2048)
+ .withPresencePenalty(0.0)
+ .withStop(List.of("this-is-the-end"))
+ .withTemperature(0.7)
+ .withTopP(1.0)
+ .build();
+
+ Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
+
+ Flux chatResponseFlux = chatModel.stream(prompt);
+
+ List responses = chatResponseFlux.collectList().block();
+ assertThat(responses).isNotEmpty();
+
+ String aggregatedResponse = responses.subList(0, responses.size() - 1)
+ .stream()
+ .map(r -> r.getResult().getOutput().getContent())
+ .collect(Collectors.joining());
+ assertThat(aggregatedResponse).isNotEmpty();
+
+ ChatResponse lastChatResponse = responses.get(responses.size() - 1);
+
+ ChatResponseMetadata responseMetadata = lastChatResponse.getMetadata();
+ assertThat(responseMetadata).isNotNull();
+
+ validate(responseMetadata);
+ }
+
+ private void validate(ChatResponseMetadata responseMetadata) {
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultChatModelObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("chat " + MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.getValue())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
+ AiOperationType.CHAT.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.MINIMAX.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
+ MiniMaxApi.ChatModel.ABAB_6_5_S_Chat.getValue())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel())
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), "0.0")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), "0.0")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(),
+ "[\"this-is-the-end\"]")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7")
+ .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\"]")
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(),
+ String.valueOf(responseMetadata.getUsage().getPromptTokens()))
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS.asString(),
+ String.valueOf(responseMetadata.getUsage().getGenerationTokens()))
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(),
+ String.valueOf(responseMetadata.getUsage().getTotalTokens()))
+ .hasBeenStarted()
+ .hasBeenStopped();
+ }
+
+ @SpringBootConfiguration
+ static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public MiniMaxApi minimaxApi() {
+ return new MiniMaxApi(System.getenv("MINIMAX_API_KEY"));
+ }
+
+ @Bean
+ public MiniMaxChatModel minimaxChatModel(MiniMaxApi minimaxApi, TestObservationRegistry observationRegistry) {
+ return new MiniMaxChatModel(minimaxApi, MiniMaxChatOptions.builder().build(), new FunctionCallbackContext(),
+ List.of(), RetryTemplate.defaultInstance(), observationRegistry);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/EmbeddingIT.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/EmbeddingIT.java
similarity index 95%
rename from models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/EmbeddingIT.java
rename to models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/EmbeddingIT.java
index 740fb5bec..6e2968673 100644
--- a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/EmbeddingIT.java
+++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/EmbeddingIT.java
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.ai.minimax;
+package org.springframework.ai.minimax.embedding;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/MiniMaxEmbeddingModelObservationIT.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/MiniMaxEmbeddingModelObservationIT.java
new file mode 100644
index 000000000..1c0a8bfb0
--- /dev/null
+++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/MiniMaxEmbeddingModelObservationIT.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.minimax.embedding;
+
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.embedding.EmbeddingRequest;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.embedding.EmbeddingResponseMetadata;
+import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObservationConvention;
+import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
+import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.observation.conventions.AiOperationType;
+import org.springframework.ai.observation.conventions.AiProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+
+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 MiniMaxEmbeddingModel}.
+ *
+ * @author Geng Rong
+ */
+@SpringBootTest(classes = MiniMaxEmbeddingModelObservationIT.Config.class)
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".+")
+public class MiniMaxEmbeddingModelObservationIT {
+
+ @Autowired
+ TestObservationRegistry observationRegistry;
+
+ @Autowired
+ MiniMaxEmbeddingModel embeddingModel;
+
+ @Test
+ void observationForEmbeddingOperation() {
+ var options = MiniMaxEmbeddingOptions.builder().withModel(MiniMaxApi.EmbeddingModel.Embo_01.getValue()).build();
+
+ EmbeddingRequest embeddingRequest = new EmbeddingRequest(List.of("Here comes the sun"), options);
+
+ EmbeddingResponse embeddingResponse = embeddingModel.call(embeddingRequest);
+ assertThat(embeddingResponse.getResults()).isNotEmpty();
+
+ EmbeddingResponseMetadata responseMetadata = embeddingResponse.getMetadata();
+ assertThat(responseMetadata).isNotNull();
+
+ TestObservationRegistryAssert.assertThat(observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultEmbeddingModelObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("embedding " + MiniMaxApi.EmbeddingModel.Embo_01.getValue())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
+ AiOperationType.EMBEDDING.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.MINIMAX.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
+ MiniMaxApi.EmbeddingModel.Embo_01.getValue())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel())
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(),
+ String.valueOf(responseMetadata.getUsage().getPromptTokens()))
+ .hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(),
+ String.valueOf(responseMetadata.getUsage().getTotalTokens()))
+ .hasBeenStarted()
+ .hasBeenStopped();
+ }
+
+ @SpringBootConfiguration
+ static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public MiniMaxApi minimaxApi() {
+ return new MiniMaxApi(System.getenv("MINIMAX_API_KEY"));
+ }
+
+ @Bean
+ public MiniMaxEmbeddingModel minimaxEmbeddingModel(MiniMaxApi minimaxApi,
+ TestObservationRegistry observationRegistry) {
+ return new MiniMaxEmbeddingModel(minimaxApi, MetadataMode.EMBED, MiniMaxEmbeddingOptions.builder().build(),
+ RetryTemplate.defaultInstance(), observationRegistry);
+ }
+
+ }
+
+}
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 7a7fad4c8..ae458e6a2 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
@@ -36,6 +36,7 @@ public enum AiProvider {
OCI_GENAI("oci_genai"),
OLLAMA("ollama"),
OPENAI("openai"),
+ MINIMAX("minimax"),
SPRING_AI("spring_ai"),
VERTEX_AI("vertex_ai");
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java
index a4601d082..03ccbdea7 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java
@@ -15,12 +15,15 @@
*/
package org.springframework.ai.autoconfigure.minimax;
+import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -57,13 +60,18 @@ public class MiniMaxAutoConfiguration {
public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties,
MiniMaxChatProperties chatProperties, RestClient.Builder restClientBuilder,
List toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
- RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler);
- return new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackContext,
- toolFunctionCallbacks, retryTemplate);
+ var chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackContext,
+ toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+ return chatModel;
}
@Bean