From 35101e75ae3fe0fd14efd0909ffa8d1e7ca7459e Mon Sep 17 00:00:00 2001 From: Alexandros Pappas Date: Mon, 3 Feb 2025 19:56:40 +0100 Subject: [PATCH] feat: Add reasoningEffort parameter to OpenAI API and Chat Options This commit introduces the `reasoningEffort` parameter to the OpenAI API integration, allowing control over the reasoning effort used by models like `o1-mini`. Changes: - Adds `reasoningEffort` field to `OpenAiApi.ChatCompletionRequest`. - Adds `reasoningEffort` field and builder method to `OpenAiChatOptions`. Signed-off-by: Alexandros Pappas Signed-off-by: Alexandros Pappas --- .../ai/openai/OpenAiChatOptions.java | 28 +++++++++++++++++-- .../ai/openai/api/OpenAiApi.java | 16 ++++++----- .../ai/openai/api/OpenAiApiIT.java | 22 +++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) 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 831aedf22..6c3e1246c 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 @@ -182,6 +182,15 @@ public class OpenAiChatOptions implements FunctionCallingOptions { * Developer-defined tags and values used for filtering completions in the dashboard. */ private @JsonProperty("metadata") Map metadata; + + /** + * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. + * Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + * Optional. Defaults to medium. + * Only for 'o1' models. + */ + private @JsonProperty("reasoning_effort") String reasoningEffort; + /** * OpenAI Tool Function Callbacks to register with the ChatModel. * For Prompt Options the functionCallbacks are automatically enabled for the duration of the prompt execution. @@ -256,6 +265,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions { .toolContext(fromOptions.getToolContext()) .store(fromOptions.getStore()) .metadata(fromOptions.getMetadata()) + .reasoningEffort(fromOptions.getReasoningEffort()) .build(); } @@ -520,6 +530,14 @@ public class OpenAiChatOptions implements FunctionCallingOptions { this.metadata = metadata; } + public String getReasoningEffort() { + return this.reasoningEffort; + } + + public void setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } + @Override public OpenAiChatOptions copy() { return OpenAiChatOptions.fromOptions(this); @@ -532,7 +550,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions { this.streamOptions, this.seed, this.stop, this.temperature, this.topP, this.tools, this.toolChoice, this.user, this.parallelToolCalls, this.functionCallbacks, this.functions, this.httpHeaders, this.proxyToolCalls, this.toolContext, this.outputModalities, this.outputAudio, this.store, - this.metadata); + this.metadata, this.reasoningEffort); } @Override @@ -563,7 +581,8 @@ public class OpenAiChatOptions implements FunctionCallingOptions { && Objects.equals(this.proxyToolCalls, other.proxyToolCalls) && Objects.equals(this.outputModalities, other.outputModalities) && Objects.equals(this.outputAudio, other.outputAudio) && Objects.equals(this.store, other.store) - && Objects.equals(this.metadata, other.metadata); + && Objects.equals(this.metadata, other.metadata) + && Objects.equals(this.reasoningEffort, other.reasoningEffort); } @Override @@ -740,6 +759,11 @@ public class OpenAiChatOptions implements FunctionCallingOptions { return this; } + public Builder reasoningEffort(String reasoningEffort) { + this.options.reasoningEffort = reasoningEffort; + return this; + } + public OpenAiChatOptions build() { return this.options; } 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 6758964f8..52e809805 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 @@ -58,6 +58,7 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Mariusz Bernacki * @author Thomas Vitale * @author David Frizelle + * @author Alexandros Pappas */ public class OpenAiApi { @@ -913,7 +914,8 @@ public class OpenAiApi { @JsonProperty("tools") List tools, @JsonProperty("tool_choice") Object toolChoice, @JsonProperty("parallel_tool_calls") Boolean parallelToolCalls, - @JsonProperty("user") String user) { + @JsonProperty("user") String user, + @JsonProperty("reasoning_effort") String reasoningEffort) { /** * Shortcut constructor for a chat completion request with the given messages, model and temperature. @@ -925,7 +927,7 @@ public class OpenAiApi { public ChatCompletionRequest(List messages, String model, Double temperature) { this(messages, model, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, null, temperature, null, - null, null, null, null); + null, null, null, null, null); } /** @@ -939,7 +941,7 @@ public class OpenAiApi { this(messages, model, null, null, null, null, null, null, null, null, null, List.of(OutputModality.AUDIO, OutputModality.TEXT), audio, null, null, null, null, null, stream, null, null, null, - null, null, null, null); + null, null, null, null, null); } /** @@ -954,7 +956,7 @@ public class OpenAiApi { public ChatCompletionRequest(List messages, String model, Double temperature, boolean stream) { this(messages, model, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, stream, null, temperature, null, - null, null, null, null); + null, null, null, null, null); } /** @@ -970,7 +972,7 @@ public class OpenAiApi { List tools, Object toolChoice) { this(messages, model, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, null, 0.8, null, - tools, toolChoice, null, null); + tools, toolChoice, null, null, null); } /** @@ -983,7 +985,7 @@ public class OpenAiApi { public ChatCompletionRequest(List messages, Boolean stream) { this(messages, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, stream, null, null, null, - null, null, null, null); + null, null, null, null, null); } /** @@ -996,7 +998,7 @@ public class OpenAiApi { return new ChatCompletionRequest(this.messages, this.model, this.store, this.metadata, this.frequencyPenalty, this.logitBias, this.logprobs, this.topLogprobs, this.maxTokens, this.maxCompletionTokens, this.n, this.outputModalities, this.audioParameters, this.presencePenalty, this.responseFormat, this.seed, this.serviceTier, this.stop, this.stream, streamOptions, this.temperature, this.topP, - this.tools, this.toolChoice, this.parallelToolCalls, this.user); + this.tools, this.toolChoice, this.parallelToolCalls, this.user, this.reasoningEffort); } /** diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java index a3c098f66..71a3e7497 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Base64; import java.util.List; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import reactor.core.publisher.Flux; @@ -40,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Christian Tzolov * @author Thomas Vitale + * @author Alexandros Pappas */ @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") public class OpenAiApiIT { @@ -66,6 +68,26 @@ public class OpenAiApiIT { assertThat(response.collectList().block()).isNotNull(); } + @Test + @Disabled("The reasoning_effort option is only available in o1 models.") + void validateReasoningTokens() { + ChatCompletionMessage userMessage = new ChatCompletionMessage( + "If a train travels 100 miles in 2 hours, what is its average speed?", ChatCompletionMessage.Role.USER); + ChatCompletionRequest request = new ChatCompletionRequest(List.of(userMessage), "o1", null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, false, null, null, null, null, + null, null, null, "low"); + ResponseEntity response = this.openAiApi.chatCompletionEntity(request); + + assertThat(response).isNotNull(); + assertThat(response.getBody()).isNotNull(); + + OpenAiApi.Usage.CompletionTokenDetails completionTokenDetails = response.getBody() + .usage() + .completionTokenDetails(); + assertThat(completionTokenDetails).isNotNull(); + assertThat(completionTokenDetails.reasoningTokens()).isPositive(); + } + @Test void embeddings() { ResponseEntity> response = this.openAiApi