From 8bfb25da86d241f7602575de97c107493214c078 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Tue, 17 Sep 2024 14:50:54 -0400 Subject: [PATCH] Fix streaming function call for MiniMax chat model Sometimes, the MiniMax stream mode function calls might get split, resulting in an empty tool call ID. This indicates that the previous call is not finished, which is an unusual API design. The issue occurs when tool calls return in a format like: [{"id":"1","function":{"name":"a"}},{"id":"","function":{"arguments":"[1]"}}] These need to be merged into: [{"id":"1","name":"a","arguments":"[1]"}] This commit addresses the merging process to handle split function calls. authored-by: mxsl-gr --- .../ai/minimax/MiniMaxChatModel.java | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) 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 d6d647c79..ee3f75846 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 @@ -22,7 +22,6 @@ 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.RateLimit; import org.springframework.ai.chat.model.AbstractToolCallSupport; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; @@ -53,6 +52,7 @@ import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -185,7 +185,7 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod "role", message != null && message.role() != null ? message.role().name() : "", "finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""); // @formatter:on - return buildGeneration(message, choice.finishReason(), metadata); + return buildGeneration(choice, metadata); }).toList(); ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody())); @@ -283,17 +283,6 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod .anyMatch(toolCall -> TOOL_CALL_FUNCTION_TYPE.equals(toolCall.type())); } - private ChatResponseMetadata from(ChatCompletion result, RateLimit rateLimit) { - Assert.notNull(result, "MiniMax ChatCompletionResult must not be null"); - return ChatResponseMetadata.builder() - .withId(result.id()) - .withUsage(MiniMaxUsage.from(result.usage())) - .withModel(result.model()) - .withRateLimit(rateLimit) - .withKeyValue("created", result.created()) - .build(); - } - private ChatResponseMetadata from(ChatCompletion result) { Assert.notNull(result, "MiniMax ChatCompletionResult must not be null"); return ChatResponseMetadata.builder() @@ -304,28 +293,40 @@ public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatMod .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() + private static Generation buildGeneration(Choice choice, Map metadata) { + List toolCalls = choice.message().toolCalls() == null ? List.of() + : choice.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() : ""); + // the MiniMax's stream function calls response are really odd + // occasionally, tool call might get split. + // for example, id empty means the previous tool call is not finished, + // the toolCalls: + // [{id:'1',function:{name:'a'}},{id:'',function:{arguments:'[1]'}}] + // these need to be merged into [{id:'1', name:'a', arguments:'[1]'}] + // it worked before, maybe the model provider made some adjustments + .reduce(new ArrayList<>(), (acc, current) -> { + if (!acc.isEmpty() && current.id().isEmpty()) { + AssistantMessage.ToolCall prev = acc.get(acc.size() - 1); + acc.set(acc.size() - 1, new AssistantMessage.ToolCall(prev.id(), prev.type(), prev.name(), + current.function().arguments())); + } + else { + AssistantMessage.ToolCall currentToolCall = new AssistantMessage.ToolCall(current.id(), + current.type(), current.function().name(), current.function().arguments()); + acc.add(currentToolCall); + } + return acc; + }, (acc1, acc2) -> { + acc1.addAll(acc2); + return acc1; + }); + var assistantMessage = new AssistantMessage(choice.message().content(), metadata, toolCalls); + String finishReason = (choice.finishReason() != null ? choice.finishReason().name() : ""); var generationMetadata = ChatGenerationMetadata.from(finishReason, null); return new Generation(assistantMessage, generationMetadata); } - private Generation buildGeneration(Choice choice, Map metadata) { - return buildGeneration(choice.message(), choice.finishReason(), metadata); - } - /** * Convert the ChatCompletionChunk into a ChatCompletion. The Usage is set to null. * @param chunk the ChatCompletionChunk to convert