diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/metadata/AnthropicChatResponseMetadata.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/metadata/AnthropicChatResponseMetadata.java index 513c7dcd5..9c458bd3a 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/metadata/AnthropicChatResponseMetadata.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/metadata/AnthropicChatResponseMetadata.java @@ -30,6 +30,7 @@ import java.util.HashMap; * {@link ChatResponseMetadata} implementation for {@literal AnthropicApi}. * * @author Christian Tzolov + * @author Thomas Vitale * @see ChatResponseMetadata * @see RateLimit * @see Usage @@ -37,35 +38,45 @@ import java.util.HashMap; */ public class AnthropicChatResponseMetadata extends HashMap implements ChatResponseMetadata { - protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }"; + protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, model: %3$s, usage: %4$s, rateLimit: %5$s }"; public static AnthropicChatResponseMetadata from(AnthropicApi.ChatCompletion result) { Assert.notNull(result, "Anthropic ChatCompletionResult must not be null"); AnthropicUsage usage = AnthropicUsage.from(result.usage()); - return new AnthropicChatResponseMetadata(result.id(), usage); + return new AnthropicChatResponseMetadata(result.id(), result.model(), usage); } private final String id; + private final String model; + @Nullable private RateLimit rateLimit; private final Usage usage; - protected AnthropicChatResponseMetadata(String id, AnthropicUsage usage) { - this(id, usage, null); + protected AnthropicChatResponseMetadata(String id, String model, AnthropicUsage usage) { + this(id, model, usage, null); } - protected AnthropicChatResponseMetadata(String id, AnthropicUsage usage, @Nullable AnthropicRateLimit rateLimit) { + protected AnthropicChatResponseMetadata(String id, String model, AnthropicUsage usage, + @Nullable AnthropicRateLimit rateLimit) { this.id = id; + this.model = model; this.usage = usage; this.rateLimit = rateLimit; } + @Override public String getId() { return this.id; } + @Override + public String getModel() { + return this.model; + } + @Override @Nullable public RateLimit getRateLimit() { @@ -86,7 +97,7 @@ public class AnthropicChatResponseMetadata extends HashMap imple @Override public String toString() { - return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getUsage(), getRateLimit()); + return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getModel(), getUsage(), getRateLimit()); } } diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java index 966a99c9d..c775681ac 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatModelIT.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.anthropic.api.AnthropicApi; import org.springframework.ai.anthropic.api.tool.MockWeatherService; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; @@ -241,4 +242,43 @@ class AnthropicChatModelIT { assertThat(generation.getOutput().getContent()).contains("30", "10", "15"); } + @Test + void validateCallResponseMetadata() { + String model = AnthropicApi.ChatModel.CLAUDE_2_1.getModelName(); + // @formatter:off + ChatResponse response = ChatClient.create(chatModel).prompt() + .options(AnthropicChatOptions.builder().withModel(model).build()) + .user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did") + .call() + .chatResponse(); + // @formatter:on + + logger.info(response.toString()); + validateChatResponseMetadata(response, model); + } + + @Test + void validateStreamCallResponseMetadata() { + String model = AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getModelName(); + // @formatter:off + ChatResponse response = ChatClient.create(chatModel).prompt() + .options(AnthropicChatOptions.builder().withModel(model).build()) + .user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did") + .stream() + .chatResponse() + .blockLast(); + // @formatter:on + + logger.info(response.toString()); + validateChatResponseMetadata(response, model); + } + + private static void validateChatResponseMetadata(ChatResponse response, String model) { + assertThat(response.getMetadata().getId()).isNotEmpty(); + assertThat(response.getMetadata().getModel()).containsIgnoringCase(model); + assertThat(response.getMetadata().getUsage().getPromptTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getGenerationTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getTotalTokens()).isPositive(); + } + } \ No newline at end of file diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/metadata/AzureOpenAiChatResponseMetadata.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/metadata/AzureOpenAiChatResponseMetadata.java index fdf8a327e..a20c40ae0 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/metadata/AzureOpenAiChatResponseMetadata.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/metadata/AzureOpenAiChatResponseMetadata.java @@ -29,6 +29,7 @@ import java.util.HashMap; * {@literal Microsoft Azure OpenAI Service}. * * @author John Blum + * @author Thomas Vitale * @see ChatResponseMetadata * @since 0.7.1 */ @@ -59,6 +60,7 @@ public class AzureOpenAiChatResponseMetadata extends HashMap imp this.promptMetadata = promptMetadata; } + @Override public String getId() { return this.id; } diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java index 734056900..4223b93d3 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java @@ -30,6 +30,7 @@ import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionChunk; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.ToolCall; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest; +import org.springframework.ai.mistralai.metadata.MistralAiChatResponseMetadata; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.model.function.AbstractFunctionCallSupport; import org.springframework.ai.model.function.FunctionCallbackContext; @@ -119,7 +120,7 @@ public class MistralAiChatModel extends .withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null))) .toList(); - return new ChatResponse(generations); + return new ChatResponse(generations, MistralAiChatResponseMetadata.from(chatCompletion)); }); } diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiChatResponseMetadata.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiChatResponseMetadata.java new file mode 100644 index 000000000..2ca8bfbf6 --- /dev/null +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiChatResponseMetadata.java @@ -0,0 +1,62 @@ +package org.springframework.ai.mistralai.metadata; + +import org.springframework.ai.chat.metadata.ChatResponseMetadata; +import org.springframework.ai.chat.metadata.EmptyUsage; +import org.springframework.ai.chat.metadata.Usage; +import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.util.Assert; + +import java.util.HashMap; + +/** + * {@link ChatResponseMetadata} implementation for {@literal Mistral AI}. + * + * @author Thomas Vitale + * @see ChatResponseMetadata + * @see Usage + * @since 1.0.0 + */ +public class MistralAiChatResponseMetadata extends HashMap implements ChatResponseMetadata { + + protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, model: %3$s, usage: %4$s }"; + + public static MistralAiChatResponseMetadata from(MistralAiApi.ChatCompletion result) { + Assert.notNull(result, "Mistral AI ChatCompletion must not be null"); + MistralAiUsage usage = MistralAiUsage.from(result.usage()); + return new MistralAiChatResponseMetadata(result.id(), result.model(), usage); + } + + private final String id; + + private final String model; + + private final Usage usage; + + protected MistralAiChatResponseMetadata(String id, String model, MistralAiUsage usage) { + this.id = id; + this.model = model; + this.usage = usage; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getModel() { + return this.model; + } + + @Override + public Usage getUsage() { + Usage usage = this.usage; + return usage != null ? usage : new EmptyUsage(); + } + + @Override + public String toString() { + return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getModel(), getUsage()); + } + +} diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiUsage.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiUsage.java new file mode 100644 index 000000000..c89982349 --- /dev/null +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/metadata/MistralAiUsage.java @@ -0,0 +1,51 @@ +package org.springframework.ai.mistralai.metadata; + +import org.springframework.ai.chat.metadata.Usage; +import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.util.Assert; + +/** + * {@link Usage} implementation for {@literal Mistral AI}. + * + * @author Thomas Vitale + * @since 1.0.0 + * @see Chat Completion API + */ +public class MistralAiUsage implements Usage { + + public static MistralAiUsage from(MistralAiApi.Usage usage) { + return new MistralAiUsage(usage); + } + + private final MistralAiApi.Usage usage; + + protected MistralAiUsage(MistralAiApi.Usage usage) { + Assert.notNull(usage, "Mistral AI Usage must not be null"); + this.usage = usage; + } + + protected MistralAiApi.Usage getUsage() { + return this.usage; + } + + @Override + public Long getPromptTokens() { + return getUsage().promptTokens().longValue(); + } + + @Override + public Long getGenerationTokens() { + return getUsage().completionTokens().longValue(); + } + + @Override + public Long getTotalTokens() { + return getUsage().totalTokens().longValue(); + } + + @Override + public String toString() { + return getUsage().toString(); + } + +} diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java index e3c1b92c4..23cbb15c3 100644 --- a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java @@ -249,4 +249,23 @@ class MistralAiChatClientIT { assertThat(content).containsAnyOf("15.0", "15"); } + @Test + void validateCallResponseMetadata() { + String model = MistralAiApi.ChatModel.OPEN_MISTRAL_7B.getModelName(); + // @formatter:off + ChatResponse response = ChatClient.create(chatModel).prompt() + .options(MistralAiChatOptions.builder().withModel(model).build()) + .user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did") + .call() + .chatResponse(); + // @formatter:on + + logger.info(response.toString()); + assertThat(response.getMetadata().getId()).isNotEmpty(); + assertThat(response.getMetadata().getModel()).containsIgnoringCase(model); + assertThat(response.getMetadata().getUsage().getPromptTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getGenerationTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getTotalTokens()).isPositive(); + } + } \ No newline at end of file diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java index cba0070fd..d3073562d 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java @@ -72,6 +72,7 @@ import reactor.core.publisher.Flux; * @author Hyunjoon Choi * @author Mariusz Bernacki * @author luocongqiu + * @author Thomas Vitale * @see ChatModel * @see StreamingChatModel * @see OpenAiApi diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java index 4b7cae9a0..76ab2af67 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java @@ -41,6 +41,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** * @author Christian Tzolov * @author Mariusz Bernacki + * @author Thomas Vitale * @since 0.8.0 */ @JsonInclude(Include.NON_NULL) @@ -66,8 +67,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions { private @JsonProperty("logit_bias") Map logitBias; /** * Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities - * of each output token returned in the 'content' of 'message'. This option is currently not available - * on the 'gpt-4-vision-preview' model. + * of each output token returned in the 'content' of 'message'. */ private @JsonProperty("logprobs") Boolean logprobs; /** diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index a468cde83..591990867 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -48,6 +48,7 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Christian Tzolov * @author Michael Lavelle * @author Mariusz Bernacki + * @author Thomas Vitale */ public class OpenAiApi { @@ -124,7 +125,6 @@ public class OpenAiApi { */ GPT_4_O("gpt-4o"), - /** * GPT-4 Turbo with Vision * The latest GPT-4 Turbo model with vision capabilities. @@ -134,7 +134,7 @@ public class OpenAiApi { GPT_4_TURBO("gpt-4-turbo"), /** - * GPT-4 Turbo with Vision model. Vision requests can now use JSON mode and function calling + * GPT-4 Turbo with Vision model. Vision requests can now use JSON mode and function calling. */ GPT_4_TURBO_2204_04_09("gpt-4-turbo-2024-04-09"), @@ -162,6 +162,7 @@ public class OpenAiApi { * Returns a maximum of 4,096 output tokens * Context window: 128k tokens */ + @Deprecated(since = "1.0.0-M2", forRemoval = true) // Replaced by GPT_4_O GPT_4_VISION_PREVIEW("gpt-4-vision-preview"), /** @@ -178,6 +179,7 @@ public class OpenAiApi { * function calling support. * Context window: 32k tokens */ + @Deprecated(since = "1.0.0-M2", forRemoval = true) // Replaced by GPT_4_O GPT_4_32K("gpt-4-32k"), /** @@ -296,8 +298,7 @@ public class OpenAiApi { * vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 * or 100 should result in a ban or exclusive selection of the relevant token. * @param logprobs Whether to return log probabilities of the output tokens or not. If true, returns the log - * probabilities of each output token returned in the 'content' of 'message'. This option is currently not available - * on the 'gpt-4-vision-preview' model. + * probabilities of each output token returned in the 'content' of 'message'. * @param topLogprobs An integer between 0 and 5 specifying the number of most likely tokens to return at each token * position, each with an associated log probability. 'logprobs' must be set to 'true' if this parameter is used. * @param maxTokens The maximum number of tokens to generate in the chat completion. The total length of input diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/metadata/OpenAiChatResponseMetadata.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/metadata/OpenAiChatResponseMetadata.java index f926496c1..93de30712 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/metadata/OpenAiChatResponseMetadata.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/metadata/OpenAiChatResponseMetadata.java @@ -30,6 +30,7 @@ import java.util.HashMap; * {@link ChatResponseMetadata} implementation for {@literal OpenAI}. * * @author John Blum + * @author Thomas Vitale * @see ChatResponseMetadata * @see RateLimit * @see Usage @@ -37,36 +38,45 @@ import java.util.HashMap; */ public class OpenAiChatResponseMetadata extends HashMap implements ChatResponseMetadata { - protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }"; + protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, model: %3$s, usage: %4$s, rateLimit: %5$s }"; public static OpenAiChatResponseMetadata from(OpenAiApi.ChatCompletion result) { Assert.notNull(result, "OpenAI ChatCompletionResult must not be null"); OpenAiUsage usage = OpenAiUsage.from(result.usage()); - OpenAiChatResponseMetadata chatResponseMetadata = new OpenAiChatResponseMetadata(result.id(), usage); - return chatResponseMetadata; + return new OpenAiChatResponseMetadata(result.id(), result.model(), usage); } private final String id; + private final String model; + @Nullable private RateLimit rateLimit; private final Usage usage; - protected OpenAiChatResponseMetadata(String id, OpenAiUsage usage) { - this(id, usage, null); + protected OpenAiChatResponseMetadata(String id, String model, OpenAiUsage usage) { + this(id, model, usage, null); } - protected OpenAiChatResponseMetadata(String id, OpenAiUsage usage, @Nullable OpenAiRateLimit rateLimit) { + protected OpenAiChatResponseMetadata(String id, String model, OpenAiUsage usage, + @Nullable OpenAiRateLimit rateLimit) { this.id = id; + this.model = model; this.usage = usage; this.rateLimit = rateLimit; } + @Override public String getId() { return this.id; } + @Override + public String getModel() { + return this.model; + } + @Override @Nullable public RateLimit getRateLimit() { @@ -87,7 +97,7 @@ public class OpenAiChatResponseMetadata extends HashMap implemen @Override public String toString() { - return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getUsage(), getRateLimit()); + return AI_METADATA_STRING.formatted(getClass().getName(), getId(), getModel(), getUsage(), getRateLimit()); } } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/api/OpenAiAudioApiIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/api/OpenAiAudioApiIT.java index 7bf9384f0..f774711a6 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/api/OpenAiAudioApiIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/audio/api/OpenAiAudioApiIT.java @@ -78,7 +78,8 @@ public class OpenAiAudioApiIT { StructuredResponse.class) .getBody(); - assertThat(transcriptionDutch.text()).isEqualTo("Hallo, mijn naam is Chris en ik hou van Spring AI."); + assertThat(transcriptionDutch.text()).containsAnyOf("Hallo, mijn naam is Chris en ik hou van Spring AI.", + "Hallo, mijn naam is Chris en ik houd van Spring AI."); } } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java index 716fe491e..39fabed0d 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java @@ -31,6 +31,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Media; import org.springframework.ai.chat.messages.Message; @@ -288,7 +289,7 @@ class OpenAiChatModelIT extends AbstractIT { } @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" }) + @ValueSource(strings = { "gpt-4o" }) void multiModalityEmbeddedImage(String modelName) throws IOException { var imageData = new ClassPathResource("/test.png"); @@ -305,7 +306,7 @@ class OpenAiChatModelIT extends AbstractIT { } @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" }) + @ValueSource(strings = { "gpt-4o" }) void multiModalityImageUrl(String modelName) throws IOException { var userMessage = new UserMessage("Explain what do you see on this picture?", List @@ -328,7 +329,7 @@ class OpenAiChatModelIT extends AbstractIT { new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png")))); Flux response = streamingChatModel.stream(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build())); + OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()).build())); String content = response.collectList() .block() @@ -343,4 +344,23 @@ class OpenAiChatModelIT extends AbstractIT { assertThat(content).containsAnyOf("bowl", "basket"); } + @Test + void validateCallResponseMetadata() { + String model = OpenAiApi.ChatModel.GPT_3_5_TURBO.getModelName(); + // @formatter:off + ChatResponse response = ChatClient.create(chatModel).prompt() + .options(OpenAiChatOptions.builder().withModel(model).build()) + .user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did") + .call() + .chatResponse(); + // @formatter:on + + logger.info(response.toString()); + assertThat(response.getMetadata().getId()).isNotEmpty(); + assertThat(response.getMetadata().getModel()).containsIgnoringCase(model); + assertThat(response.getMetadata().getUsage().getPromptTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getGenerationTokens()).isPositive(); + assertThat(response.getMetadata().getUsage().getTotalTokens()).isPositive(); + } + } \ No newline at end of file diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java index fa4ca8a0c..a5a449909 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java @@ -258,7 +258,7 @@ class OpenAiChatClientIT extends AbstractIT { } @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" }) + @ValueSource(strings = { "gpt-4o" }) void multiModalityEmbeddedImage(String modelName) throws IOException { // @formatter:off @@ -276,7 +276,7 @@ class OpenAiChatClientIT extends AbstractIT { } @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" }) + @ValueSource(strings = { "gpt-4o" }) void multiModalityImageUrl(String modelName) throws IOException { // TODO: add url method that wrapps the checked exception. @@ -304,7 +304,7 @@ class OpenAiChatClientIT extends AbstractIT { // @formatter:off Flux response = ChatClient.create(chatModel).prompt() - .options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()) + .options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()) .build()) .user(u -> u.text("Explain what do you see on this picture?") .media(MimeTypeUtils.IMAGE_PNG, url)) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/metadata/ChatResponseMetadata.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/metadata/ChatResponseMetadata.java index fa62014b0..17fea86cd 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/metadata/ChatResponseMetadata.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/metadata/ChatResponseMetadata.java @@ -24,16 +24,33 @@ import java.util.HashMap; * response. * * @author John Blum + * @author Thomas Vitale * @since 0.7.0 */ public interface ChatResponseMetadata extends ResponseMetadata { - static class DefaultChatResponseMetadata extends HashMap implements ChatResponseMetadata { + class DefaultChatResponseMetadata extends HashMap implements ChatResponseMetadata { } ChatResponseMetadata NULL = new DefaultChatResponseMetadata(); + /** + * A unique identifier for the chat completion operation. + * @return unique operation identifier. + */ + default String getId() { + return ""; + } + + /** + * The model that handled the request. + * @return the model that handled the request. + */ + default String getModel() { + return ""; + } + /** * Returns AI provider specific metadata on rate limits. * @return AI provider specific metadata on rate limits. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc index d5af1e2c2..683ea4c79 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc @@ -127,7 +127,7 @@ ChatResponse response = chatModel.call( new Prompt( "Generate the names of 5 famous pirates.", AzureOpenAiChatOptions.builder() - .withModel("gpt-4-32k") + .withModel("gpt-4-o") .withTemperature(0.4) .build() )); diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc index df16ca67f..0b3f154fa 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc @@ -90,7 +90,7 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur | spring.ai.openai.chat.enabled | Enable OpenAI chat model. | true | spring.ai.openai.chat.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | - | spring.ai.openai.chat.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | - -| spring.ai.openai.chat.options.model | This is the OpenAI Chat model to use. `gpt-4o`, `gpt-4-turbo`, `gpt-4-turbo-2024-04-09`, `gpt-4-0125-preview`, `gpt-4-turbo-preview`, `gpt-4-vision-preview`, `gpt-4-32k`, `gpt-3.5-turbo`, `gpt-3.5-turbo-0125`, `gpt-3.5-turbo-1106`. See the https://platform.openai.com/docs/models[models] page for more information. | `gpt-3.5-turbo` +| spring.ai.openai.chat.options.model | This is the OpenAI Chat model to use. `gpt-4o`, `gpt-4-turbo`, `gpt-4-turbo-2024-04-09`, `gpt-4-0125-preview`, `gpt-4-turbo-preview`, `gpt-3.5-turbo`, `gpt-3.5-turbo-0125`, `gpt-3.5-turbo-1106`. See the https://platform.openai.com/docs/models[models] page for more information. | `gpt-3.5-turbo` | spring.ai.openai.chat.options.temperature | The sampling temperature to use that controls the apparent creativity of generated completions. Higher values will make output more random while lower values will make results more focused and deterministic. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | 0.8 | spring.ai.openai.chat.options.frequencyPenalty | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. | 0.0f | spring.ai.openai.chat.options.logitBias | Modify the likelihood of specified tokens appearing in the completion. | - @@ -129,7 +129,7 @@ ChatResponse response = chatModel.call( new Prompt( "Generate the names of 5 famous pirates.", OpenAiChatOptions.builder() - .withModel("gpt-4-32k") + .withModel("gpt-4-o") .withTemperature(0.4) .build() ));