From 866a7c5c24b398391bd9c04b9dcd4236c3bf42d5 Mon Sep 17 00:00:00 2001 From: lizhihao688 Date: Fri, 12 Jun 2026 13:16:59 +0800 Subject: [PATCH] fix(model): apply thinkingBudget to OpenAI-compatible API request GenerateOptions.thinkingBudget was stored in the options object but never mapped to OpenAIRequest, causing the thinking_budget parameter to be silently dropped from all OpenAI-compatible API calls. This breaks reasoning/thinking models (e.g. Qwen3, DeepSeek-R1) that use thinking_budget to limit internal reasoning tokens: without the cap, the model exhausts all max_completion_tokens on reasoning_content and returns an empty content field. Changes: - Add thinking_budget field to OpenAIRequest DTO with @JsonProperty - Map GenerateOptions.thinkingBudget in OpenAIChatFormatter.applyOptions() - Add unit tests covering direct options, default fallback, override, and absent cases Fixes #1697 --- .../formatter/openai/OpenAIChatFormatter.java | 7 +++ .../formatter/openai/dto/OpenAIRequest.java | 24 ++++++++ .../openai/OpenAIChatFormatterTest.java | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+) 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 {