diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java index 77ee1fd195..4cd51b869d 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIChatFormatter.java @@ -81,6 +81,13 @@ public void applyOptions( request.setReasoningEffort(reasoningEffort); } + // Apply thinking budget + Integer thinkingBudget = + getOptionOrDefault(options, defaultOptions, GenerateOptions::getThinkingBudget); + if (thinkingBudget != null) { + request.setThinkingBudget(thinkingBudget); + } + // Apply top_p Double topP = getOptionOrDefault(options, defaultOptions, GenerateOptions::getTopP); if (topP != null) { diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIRequest.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIRequest.java index d3f58d0322..f63fd339be 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIRequest.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/dto/OpenAIRequest.java @@ -128,6 +128,17 @@ public class OpenAIRequest { @JsonProperty("reasoning_effort") private String reasoningEffort; + /** + * Maximum tokens allocated for the model's internal thinking/reasoning process. + * Supported by OpenAI-compatible providers that expose a thinking budget parameter + * (e.g., Alibaba Cloud Bailian/DashScope Qwen3 series via {@code thinking_budget}). + * When set, the model is prevented from spending more than this many tokens on + * reasoning, leaving the remainder of {@code max_completion_tokens} for the + * visible response. + */ + @JsonProperty("thinking_budget") + private Integer thinkingBudget; + /** * Controls whether to allow parallel tool calls. * Set to false to disable parallel tool calling. @@ -345,6 +356,14 @@ public void setReasoningEffort(String reasoningEffort) { this.reasoningEffort = reasoningEffort; } + public Integer getThinkingBudget() { + return thinkingBudget; + } + + public void setThinkingBudget(Integer thinkingBudget) { + this.thinkingBudget = thinkingBudget; + } + public Boolean getParallelToolCalls() { return parallelToolCalls; } @@ -554,6 +573,11 @@ public Builder reasoningEffort(String reasoningEffort) { return this; } + public Builder thinkingBudget(Integer thinkingBudget) { + request.setThinkingBudget(thinkingBudget); + return this; + } + public Builder parallelToolCalls(Boolean parallelToolCalls) { request.setParallelToolCalls(parallelToolCalls); return this; diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIChatFormatterTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIChatFormatterTest.java index 2bcbafa215..699a62699f 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIChatFormatterTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIChatFormatterTest.java @@ -379,6 +379,64 @@ void testApplyToolsWithNullStrict() { } } + @Nested + @DisplayName("thinkingBudget Tests") + class ThinkingBudgetTests { + + @Test + @DisplayName("Should apply thinkingBudget from GenerateOptions") + void testApplyThinkingBudget() { + OpenAIRequest request = + OpenAIRequest.builder().model("qwen3.7-max").messages(List.of()).build(); + + GenerateOptions options = GenerateOptions.builder().thinkingBudget(4096).build(); + + formatter.applyOptions(request, options, null); + + assertEquals(4096, request.getThinkingBudget()); + } + + @Test + @DisplayName("Should apply thinkingBudget from defaultOptions when options is null") + void testApplyThinkingBudgetFromDefault() { + OpenAIRequest request = + OpenAIRequest.builder().model("qwen3.7-max").messages(List.of()).build(); + + GenerateOptions defaultOptions = GenerateOptions.builder().thinkingBudget(2048).build(); + + formatter.applyOptions(request, null, defaultOptions); + + assertEquals(2048, request.getThinkingBudget()); + } + + @Test + @DisplayName("Options thinkingBudget should override defaultOptions") + void testThinkingBudgetOptionsOverridesDefault() { + OpenAIRequest request = + OpenAIRequest.builder().model("qwen3.7-max").messages(List.of()).build(); + + GenerateOptions defaultOptions = GenerateOptions.builder().thinkingBudget(1000).build(); + GenerateOptions options = GenerateOptions.builder().thinkingBudget(8000).build(); + + formatter.applyOptions(request, options, defaultOptions); + + assertEquals(8000, request.getThinkingBudget()); + } + + @Test + @DisplayName("Should not set thinkingBudget when not specified") + void testThinkingBudgetNotSetWhenAbsent() { + OpenAIRequest request = + OpenAIRequest.builder().model("gpt-4o").messages(List.of()).build(); + + GenerateOptions options = GenerateOptions.builder().temperature(0.7).build(); + + formatter.applyOptions(request, options, null); + + assertNull(request.getThinkingBudget()); + } + } + @Nested @DisplayName("applyAdditionalBodyParams Tests") class ApplyAdditionalBodyParamsTests {