Add observability to minimax chat model
- add observability to minimax embedding model - fix chat web search result error - fix minimax auto configuration
This commit is contained in:
@@ -54,6 +54,11 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-observation-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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<FunctionCallback> 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<ChatCompletion> 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<ChatCompletion> completionEntity = this.retryTemplate
|
||||
.execute(ctx -> this.miniMaxApi.chatCompletionEntity(request));
|
||||
|
||||
List<Choice> 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<Generation> generations = choices.stream().map(choice -> {
|
||||
if (chatCompletion == null) {
|
||||
logger.warn("No chat completion returned for prompt: {}", prompt);
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
|
||||
List<Choice> choices = chatCompletion.choices();
|
||||
if (choices == null) {
|
||||
logger.warn("No choices returned for prompt: {}, because: {}}", prompt,
|
||||
chatCompletion.baseResponse().message());
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
|
||||
List<Generation> 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<String, Object> 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<String, Object> 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<ChatResponse> stream(Prompt prompt) {
|
||||
ChatCompletionRequest request = createRequest(prompt, true);
|
||||
return Flux.deferContextual(contextView -> {
|
||||
ChatCompletionRequest request = createRequest(prompt, true);
|
||||
|
||||
Flux<ChatCompletionChunk> completionChunks = this.retryTemplate
|
||||
.execute(ctx -> this.miniMaxApi.chatCompletionStream(request));
|
||||
Flux<ChatCompletionChunk> 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<String, String> 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<String, String> roleMap = new ConcurrentHashMap<>();
|
||||
|
||||
// Convert the ChatCompletionChunk into a ChatCompletion to be able to reuse
|
||||
// the function call handling logic.
|
||||
Flux<ChatResponse> 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<Generation> generations = chatCompletion2.choices().stream().map(choice -> {
|
||||
if (choice.message().role() != null) {
|
||||
roleMap.putIfAbsent(id, choice.message().role().name());
|
||||
}
|
||||
Map<String, Object> 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> 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<Generation> generations = chatCompletion2.choices().stream().map(choice -> {
|
||||
if (choice.message().role() != null) {
|
||||
roleMap.putIfAbsent(id, choice.message().role().name());
|
||||
}
|
||||
Map<String, Object> 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<ChatResponse> 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<String, Object> metadata) {
|
||||
if (message == null || message.role() == Role.TOOL) {
|
||||
return null;
|
||||
}
|
||||
List<AssistantMessage.ToolCall> 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<String, Object> metadata) {
|
||||
List<AssistantMessage.ToolCall> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Embedding> 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<Embedding> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ChatResponse> chatResponseFlux = chatModel.stream(prompt);
|
||||
|
||||
List<ChatResponse> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
|
||||
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
|
||||
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
|
||||
ObjectProvider<ObservationRegistry> observationRegistry,
|
||||
ObjectProvider<ChatModelObservationConvention> 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
|
||||
|
||||
Reference in New Issue
Block a user