Fix Mistral AI Chat model function call usage calculation

- Fix the chat model's call() to calculate the cumulative usage
   - Use an explicit internalCall to pass the previous chat response so that accumulation can be done

 - Fix the chat model's stream() to calculate the cumulative usage
   - Fix MistralAi API to include usgae in ChatCompletionChunk
   - Use internalStream() to accumulate the usage

Add/update tests
This commit is contained in:
Ilayaperumal Gopinathan
2024-12-11 12:33:36 +00:00
committed by Christian Tzolov
parent 187360d132
commit 81dfd3b6b5
5 changed files with 70 additions and 9 deletions

View File

@@ -36,6 +36,8 @@ import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.metadata.UsageUtils;
import org.springframework.ai.chat.model.AbstractToolCallSupport;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
@@ -74,6 +76,7 @@ import org.springframework.util.CollectionUtils;
* @author Grogdunn
* @author Thomas Vitale
* @author luocongqiu
* @author Ilayaperumal Gopinathan
* @since 1.0.0
*/
public class MistralAiChatModel extends AbstractToolCallSupport implements ChatModel {
@@ -155,8 +158,22 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
.build();
}
public static ChatResponseMetadata from(MistralAiApi.ChatCompletion result, Usage usage) {
Assert.notNull(result, "Mistral AI ChatCompletion must not be null");
return ChatResponseMetadata.builder()
.withId(result.id())
.withModel(result.model())
.withUsage(usage)
.withKeyValue("created", result.created())
.build();
}
@Override
public ChatResponse call(Prompt prompt) {
return this.internalCall(prompt, null);
}
public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {
MistralAiApi.ChatCompletionRequest request = createRequest(prompt, false);
@@ -192,7 +209,10 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
return buildGeneration(choice, metadata);
}).toList();
ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody()));
MistralAiUsage usage = MistralAiUsage.from(completionEntity.getBody().usage());
Usage cumulativeUsage = UsageUtils.getCumulativeUsage(usage, previousChatResponse);
ChatResponse chatResponse = new ChatResponse(generations,
from(completionEntity.getBody(), cumulativeUsage));
observationContext.setResponse(chatResponse);
@@ -205,7 +225,7 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
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 this.internalCall(new Prompt(toolCallConversation, prompt.getOptions()), response);
}
return response;
@@ -213,6 +233,10 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return this.internalStream(prompt, null);
}
public Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousChatResponse) {
return Flux.deferContextual(contextView -> {
var request = createRequest(prompt, true);
@@ -258,7 +282,9 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
// @formatter:on
if (chatCompletion2.usage() != null) {
return new ChatResponse(generations, from(chatCompletion2));
MistralAiUsage usage = MistralAiUsage.from(chatCompletion2.usage());
Usage cumulativeUsage = UsageUtils.getCumulativeUsage(usage, previousChatResponse);
return new ChatResponse(generations, from(chatCompletion2, cumulativeUsage));
}
else {
return new ChatResponse(generations);
@@ -276,7 +302,7 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
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 this.internalStream(new Prompt(toolCallConversation, prompt.getOptions()), response);
}
else {
return Flux.just(response);
@@ -313,7 +339,8 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM
.map(cc -> new Choice(cc.index(), cc.delta(), cc.finishReason(), cc.logprobs()))
.toList();
return new ChatCompletion(chunk.id(), "chat.completion", chunk.created(), chunk.model(), choices, null);
return new ChatCompletion(chunk.id(), "chat.completion", chunk.created(), chunk.model(), choices,
chunk.usage());
}
/**

View File

@@ -209,7 +209,8 @@ public class MistralAiApi {
return !isInsideTool.get();
})
.concatMapIterable(window -> {
Mono<ChatCompletionChunk> mono1 = window.reduce(new ChatCompletionChunk(null, null, null, null, null),
Mono<ChatCompletionChunk> mono1 = window.reduce(
new ChatCompletionChunk(null, null, null, null, null, null),
(previous, current) -> this.chunkMerger.merge(previous, current));
return List.of(mono1);
})
@@ -934,6 +935,7 @@ public class MistralAiApi {
* @param model The model used for the chat completion.
* @param choices A list of chat completion choices. Can be more than one if n is
* greater than 1.
* @param usage usage metrics for the chat completion.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionChunk(
@@ -942,7 +944,8 @@ public class MistralAiApi {
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("model") String model,
@JsonProperty("choices") List<ChunkChoice> choices) {
@JsonProperty("choices") List<ChunkChoice> choices,
@JsonProperty("usage") Usage usage) {
// @formatter:on
/**

View File

@@ -63,7 +63,9 @@ public class MistralAiStreamFunctionCallingHelper {
ChunkChoice choice = merge(previousChoice0, currentChoice0);
return new ChatCompletionChunk(id, object, created, model, List.of(choice));
MistralAiApi.Usage usage = (current.usage() != null ? current.usage() : previous.usage());
return new ChatCompletionChunk(id, object, created, model, List.of(choice), usage);
}
private ChunkChoice merge(ChunkChoice previous, ChunkChoice current) {

View File

@@ -205,6 +205,9 @@ class MistralAiChatModelIT {
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).containsAnyOf("30.0", "30");
assertThat(response.getMetadata()).isNotNull();
assertThat(response.getMetadata().getUsage()).isNotNull();
assertThat(response.getMetadata().getUsage().getTotalTokens()).isLessThan(1050).isGreaterThan(800);
}
@Test
@@ -238,6 +241,32 @@ class MistralAiChatModelIT {
assertThat(content).containsAnyOf("10.0", "10");
}
@Test
void streamFunctionCallUsageTest() {
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Response in Celsius");
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = MistralAiChatOptions.builder()
.withModel(MistralAiApi.ChatModel.SMALL.getValue())
.withFunctionCallbacks(List.of(FunctionCallback.builder()
.function("getCurrentWeather", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build();
Flux<ChatResponse> response = this.streamingChatModel.stream(new Prompt(messages, promptOptions));
ChatResponse chatResponse = response.last().block();
logger.info("Response: {}", chatResponse);
assertThat(chatResponse.getMetadata()).isNotNull();
assertThat(chatResponse.getMetadata().getUsage()).isNotNull();
assertThat(chatResponse.getMetadata().getUsage().getTotalTokens()).isLessThan(1050).isGreaterThan(800);
}
record ActorsFilmsRecord(String actor, List<String> movies) {
}

View File

@@ -124,7 +124,7 @@ public class MistralAiRetryTests {
var choice = new ChatCompletionChunk.ChunkChoice(0, new ChatCompletionMessage("Response", Role.ASSISTANT),
ChatCompletionFinishReason.STOP, null);
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", "chat.completion.chunk", 789L,
"model", List.of(choice));
"model", List.of(choice), null);
given(this.mistralAiApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))