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:
committed by
Christian Tzolov
parent
187360d132
commit
81dfd3b6b5
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user