From ee7d2b25567fbb72d1ca891e16c5864842024548 Mon Sep 17 00:00:00 2001 From: GR Date: Mon, 7 Oct 2024 12:18:12 +0800 Subject: [PATCH] Add observability to minimax chat model - add observability to minimax embedding model - fix chat web search result error - fix minimax auto configuration --- models/spring-ai-minimax/pom.xml | 5 + .../ai/minimax/MiniMaxChatModel.java | 260 ++++++++++++------ .../ai/minimax/MiniMaxEmbeddingModel.java | 113 ++++++-- .../ai/minimax/api/MiniMaxApiConstants.java | 4 + .../ai/minimax/metadata/MiniMaxUsage.java | 12 +- .../ai/minimax/api/MiniMaxRetryTests.java | 2 +- .../chat/MiniMaxChatModelObservationIT.java | 177 ++++++++++++ .../minimax/{ => embedding}/EmbeddingIT.java | 3 +- .../MiniMaxEmbeddingModelObservationIT.java | 112 ++++++++ .../observation/conventions/AiProvider.java | 1 + .../minimax/MiniMaxAutoConfiguration.java | 14 +- 11 files changed, 582 insertions(+), 121 deletions(-) create mode 100644 models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/chat/MiniMaxChatModelObservationIT.java rename models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/{ => embedding}/EmbeddingIT.java (95%) create mode 100644 models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/embedding/MiniMaxEmbeddingModelObservationIT.java 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