From 3dcde38d423526fc5b51e971e6f648bc3ba682aa Mon Sep 17 00:00:00 2001 From: Janemia Date: Tue, 26 May 2026 12:12:08 +0800 Subject: [PATCH 1/4] feat: allow per-request enableThinking override via GenerateOptions Add enableThinking field to GenerateOptions so that hooks can dynamically control enable_thinking on a per-turn basis, not just thinking_budget. DashScopeChatModel.applyThinkingMode() now prioritizes the options-level value over the model-level fixed value, with full backward compatibility. --- .../core/model/DashScopeChatModel.java | 16 ++++++---- .../core/model/GenerateOptions.java | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index cbdd860ad5..280e7e6857 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -321,22 +321,26 @@ private Flux streamWithHttpClient( * Apply thinking mode configuration to request if enabled. */ private void applyThinkingMode(DashScopeRequest request, GenerateOptions options) { + Boolean effectiveEnableThinking = options.getEnableThinking() != null + ? options.getEnableThinking() + : this.enableThinking; + // Validate thinking configuration - if (options.getThinkingBudget() != null && !Boolean.TRUE.equals(enableThinking)) { + if (options.getThinkingBudget() != null && !Boolean.TRUE.equals(effectiveEnableThinking)) { throw new IllegalStateException( "thinkingBudget is set but enableThinking is not enabled. To use thinking mode" + " with budget control, you must explicitly enable thinking by calling" - + " .enableThinking(true) on the model builder. Example:" + + " .enableThinking(true) on the model builder or setting" + + " enableThinking(true) in GenerateOptions. Example:" + " DashScopeChatModel.builder().enableThinking(true)" + ".defaultOptions(GenerateOptions.builder().thinkingBudget(1000).build())"); } - if (enableThinking != null) { - // Explicitly assign value for thinking mode - request.getParameters().setEnableThinking(enableThinking); + if (effectiveEnableThinking != null) { + request.getParameters().setEnableThinking(effectiveEnableThinking); } - if (Boolean.TRUE.equals(enableThinking) && options.getThinkingBudget() != null) { + if (Boolean.TRUE.equals(effectiveEnableThinking) && options.getThinkingBudget() != null) { request.getParameters().setThinkingBudget(options.getThinkingBudget()); } diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java index d71f8808e3..c6e7e0bfb2 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java @@ -43,6 +43,7 @@ public class GenerateOptions { private final Integer maxCompletionTokens; private final Double frequencyPenalty; private final Double presencePenalty; + private final Boolean enableThinking; private final Integer thinkingBudget; private final String reasoningEffort; private final ExecutionConfig executionConfig; @@ -72,6 +73,7 @@ private GenerateOptions(Builder builder) { this.maxCompletionTokens = builder.maxCompletionTokens; this.frequencyPenalty = builder.frequencyPenalty; this.presencePenalty = builder.presencePenalty; + this.enableThinking = builder.enableThinking; this.thinkingBudget = builder.thinkingBudget; this.reasoningEffort = builder.reasoningEffort; this.executionConfig = builder.executionConfig; @@ -229,6 +231,19 @@ public Double getPresencePenalty() { return presencePenalty; } + /** + * Gets whether thinking mode is enabled for this request. + * + *

This parameter allows per-request control over thinking mode. When set, it overrides + * the model-level {@code enableThinking} configuration. When null, the model-level setting + * is used. + * + * @return true to enable thinking, false to disable, or null to use model default + */ + public Boolean getEnableThinking() { + return enableThinking; + } + /** * Gets the maximum number of tokens for reasoning/thinking content. * @@ -451,6 +466,8 @@ public static GenerateOptions mergeOptions(GenerateOptions primary, GenerateOpti primary.presencePenalty != null ? primary.presencePenalty : fallback.presencePenalty); + builder.enableThinking( + primary.enableThinking != null ? primary.enableThinking : fallback.enableThinking); builder.thinkingBudget( primary.thinkingBudget != null ? primary.thinkingBudget : fallback.thinkingBudget); builder.reasoningEffort( @@ -512,6 +529,7 @@ public static class Builder { private Integer maxCompletionTokens; private Double frequencyPenalty; private Double presencePenalty; + private Boolean enableThinking; private Integer thinkingBudget; private String reasoningEffort; private ExecutionConfig executionConfig; @@ -666,6 +684,20 @@ public Builder presencePenalty(Double presencePenalty) { return this; } + /** + * Sets whether thinking mode is enabled for this request. + * + *

When set, this overrides the model-level {@code enableThinking} configuration, + * allowing per-request control over thinking mode via hooks. + * + * @param enableThinking true to enable thinking, false to disable, null to use model default + * @return this builder + */ + public Builder enableThinking(Boolean enableThinking) { + this.enableThinking = enableThinking; + return this; + } + /** * Sets the thinking budget (maximum tokens for reasoning/thinking content). * From dd5904c7b704225f5eb80b6ad17d1b11917e920e Mon Sep 17 00:00:00 2001 From: Janemia Date: Tue, 26 May 2026 12:22:14 +0800 Subject: [PATCH 2/4] style: fix spotless formatting for ternary expression --- .../java/io/agentscope/core/model/DashScopeChatModel.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index 280e7e6857..06104d2f6e 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -321,9 +321,10 @@ private Flux streamWithHttpClient( * Apply thinking mode configuration to request if enabled. */ private void applyThinkingMode(DashScopeRequest request, GenerateOptions options) { - Boolean effectiveEnableThinking = options.getEnableThinking() != null - ? options.getEnableThinking() - : this.enableThinking; + Boolean effectiveEnableThinking = + options.getEnableThinking() != null + ? options.getEnableThinking() + : this.enableThinking; // Validate thinking configuration if (options.getThinkingBudget() != null && !Boolean.TRUE.equals(effectiveEnableThinking)) { From 3d752e66113db928cfae9408b7f228de2f564591 Mon Sep 17 00:00:00 2001 From: Janemia Date: Thu, 4 Jun 2026 16:17:50 +0800 Subject: [PATCH 3/4] fix: address review feedback for per-request enableThinking - [major] Add stream validation: throw when per-request enableThinking=true but model stream=false - [minor] Only throw on thinkingBudget when enableThinking is null (not configured); silently ignore when explicitly false - [minor] Remove 'via hooks' qualifier from Javadoc - [nit] Add unit tests for per-request override, stream validation, explicitly-disabled-with-budget, and mergeOptions enableThinking --- .../core/model/DashScopeChatModel.java | 20 ++++-- .../core/model/GenerateOptions.java | 6 +- .../core/model/DashScopeChatModelTest.java | 70 ++++++++++++++++++- .../core/model/GenerateOptionsTest.java | 29 ++++++++ 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java index 06104d2f6e..6758fe4bb0 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/DashScopeChatModel.java @@ -326,12 +326,22 @@ private void applyThinkingMode(DashScopeRequest request, GenerateOptions options ? options.getEnableThinking() : this.enableThinking; - // Validate thinking configuration - if (options.getThinkingBudget() != null && !Boolean.TRUE.equals(effectiveEnableThinking)) { + // Validate: thinking mode requires streaming + if (Boolean.TRUE.equals(effectiveEnableThinking) && !this.stream) { throw new IllegalStateException( - "thinkingBudget is set but enableThinking is not enabled. To use thinking mode" - + " with budget control, you must explicitly enable thinking by calling" - + " .enableThinking(true) on the model builder or setting" + "enableThinking is set to true but the model was built with stream=false." + + " Thinking mode requires streaming. Either build the model with" + + " stream=true (or enableThinking=true which forces streaming)," + + " or do not enable thinking per-request on a non-streaming model."); + } + + // Validate thinking budget: only throw when enableThinking is null (not configured), + // silently ignore thinkingBudget when enableThinking is explicitly false (user intent) + if (options.getThinkingBudget() != null && effectiveEnableThinking == null) { + throw new IllegalStateException( + "thinkingBudget is set but enableThinking is not configured. To use thinking" + + " mode with budget control, you must explicitly enable thinking by" + + " calling .enableThinking(true) on the model builder or setting" + " enableThinking(true) in GenerateOptions. Example:" + " DashScopeChatModel.builder().enableThinking(true)" + ".defaultOptions(GenerateOptions.builder().thinkingBudget(1000).build())"); diff --git a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java index c6e7e0bfb2..2eb679081a 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java +++ b/agentscope-core/src/main/java/io/agentscope/core/model/GenerateOptions.java @@ -235,8 +235,8 @@ public Double getPresencePenalty() { * Gets whether thinking mode is enabled for this request. * *

This parameter allows per-request control over thinking mode. When set, it overrides - * the model-level {@code enableThinking} configuration. When null, the model-level setting - * is used. + * the model-level {@code enableThinking} configuration. When null, the model-level + * setting is used. * * @return true to enable thinking, false to disable, or null to use model default */ @@ -688,7 +688,7 @@ public Builder presencePenalty(Double presencePenalty) { * Sets whether thinking mode is enabled for this request. * *

When set, this overrides the model-level {@code enableThinking} configuration, - * allowing per-request control over thinking mode via hooks. + * allowing per-request control over thinking mode. * * @param enableThinking true to enable thinking, false to disable, null to use model default * @return this builder diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java index f5fd48a12d..e63ee6dc9a 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java @@ -698,15 +698,31 @@ void testApplyThinkingModeWithNull() { @Test @DisplayName( - "Should throw an IllegalStateException when setting thinkingBudget while thinking mode" - + " is disabled") + "Should throw when thinkingBudget is set but enableThinking is not configured (null)") void testApplyThinkingModeValidation() { + DashScopeChatModel chatModel = + DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").build(); + + DashScopeRequest request = + DashScopeRequest.builder() + .parameters(DashScopeParameters.builder().build()) + .build(); + + GenerateOptions options = GenerateOptions.builder().thinkingBudget(100).build(); + + assertThrows( + IllegalStateException.class, + () -> invokeApplyThinkingMode(chatModel, request, options)); + } + + @Test + @DisplayName("Should silently ignore thinkingBudget when enableThinking is explicitly false") + void testApplyThinkingModeExplicitlyDisabledWithBudget() { DashScopeChatModel chatModel = DashScopeChatModel.builder() .apiKey(mockApiKey) .modelName("qwen-plus") .enableThinking(false) - .enableSearch(false) .build(); DashScopeRequest request = @@ -716,6 +732,54 @@ void testApplyThinkingModeValidation() { GenerateOptions options = GenerateOptions.builder().thinkingBudget(100).build(); + assertDoesNotThrow(() -> invokeApplyThinkingMode(chatModel, request, options)); + assertFalse(request.getParameters().getEnableThinking()); + assertNull(request.getParameters().getThinkingBudget()); + } + + @Test + @DisplayName("Should allow per-request enableThinking override from options") + void testApplyThinkingModePerRequestOverride() { + // Model built with enableThinking=true, but per-request disables it + DashScopeChatModel chatModel = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen-plus") + .enableThinking(true) + .build(); + + DashScopeRequest request = + DashScopeRequest.builder() + .parameters(DashScopeParameters.builder().build()) + .build(); + + GenerateOptions options = + GenerateOptions.builder().enableThinking(false).build(); + + assertDoesNotThrow(() -> invokeApplyThinkingMode(chatModel, request, options)); + assertFalse(request.getParameters().getEnableThinking()); + } + + @Test + @DisplayName( + "Should throw when per-request enableThinking=true but model stream=false") + void testApplyThinkingModePerRequestRequiresStreaming() { + // Model built with stream=false and no model-level thinking + DashScopeChatModel chatModel = + DashScopeChatModel.builder() + .apiKey(mockApiKey) + .modelName("qwen-plus") + .stream(false) + .build(); + + DashScopeRequest request = + DashScopeRequest.builder() + .parameters(DashScopeParameters.builder().build()) + .build(); + + GenerateOptions options = + GenerateOptions.builder().enableThinking(true).build(); + assertThrows( IllegalStateException.class, () -> invokeApplyThinkingMode(chatModel, request, options)); diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/GenerateOptionsTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/GenerateOptionsTest.java index 9db349d158..de47d82272 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/GenerateOptionsTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/GenerateOptionsTest.java @@ -474,4 +474,33 @@ void testExplicitNullEndpointPath() { assertNotNull(options); assertNull(options.getEndpointPath()); } + + @Test + @DisplayName("Should merge enableThinking with primary taking precedence") + void testMergeOptionsEnableThinkingPrimaryOverridesFallback() { + GenerateOptions primary = GenerateOptions.builder().enableThinking(false).build(); + + GenerateOptions fallback = + GenerateOptions.builder().enableThinking(true).thinkingBudget(500).build(); + + GenerateOptions merged = GenerateOptions.mergeOptions(primary, fallback); + + assertNotNull(merged); + assertEquals(false, merged.getEnableThinking()); + assertEquals(500, merged.getThinkingBudget()); + } + + @Test + @DisplayName("Should use fallback enableThinking when primary is null") + void testMergeOptionsEnableThinkingFallback() { + GenerateOptions primary = GenerateOptions.builder().temperature(0.8).build(); + + GenerateOptions fallback = GenerateOptions.builder().enableThinking(true).build(); + + GenerateOptions merged = GenerateOptions.mergeOptions(primary, fallback); + + assertNotNull(merged); + assertEquals(true, merged.getEnableThinking()); + assertEquals(0.8, merged.getTemperature()); + } } From f84fb581cf2b782caa487241e2fa2564a14b68be Mon Sep 17 00:00:00 2001 From: Janemia Date: Wed, 10 Jun 2026 15:02:38 +0800 Subject: [PATCH 4/4] style: apply spotless formatting to test code --- .../core/model/DashScopeChatModelTest.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java index e63ee6dc9a..3a70912b12 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/model/DashScopeChatModelTest.java @@ -753,23 +753,18 @@ void testApplyThinkingModePerRequestOverride() { .parameters(DashScopeParameters.builder().build()) .build(); - GenerateOptions options = - GenerateOptions.builder().enableThinking(false).build(); + GenerateOptions options = GenerateOptions.builder().enableThinking(false).build(); assertDoesNotThrow(() -> invokeApplyThinkingMode(chatModel, request, options)); assertFalse(request.getParameters().getEnableThinking()); } @Test - @DisplayName( - "Should throw when per-request enableThinking=true but model stream=false") + @DisplayName("Should throw when per-request enableThinking=true but model stream=false") void testApplyThinkingModePerRequestRequiresStreaming() { // Model built with stream=false and no model-level thinking DashScopeChatModel chatModel = - DashScopeChatModel.builder() - .apiKey(mockApiKey) - .modelName("qwen-plus") - .stream(false) + DashScopeChatModel.builder().apiKey(mockApiKey).modelName("qwen-plus").stream(false) .build(); DashScopeRequest request = @@ -777,8 +772,7 @@ void testApplyThinkingModePerRequestRequiresStreaming() { .parameters(DashScopeParameters.builder().build()) .build(); - GenerateOptions options = - GenerateOptions.builder().enableThinking(true).build(); + GenerateOptions options = GenerateOptions.builder().enableThinking(true).build(); assertThrows( IllegalStateException.class,