Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -321,22 +321,37 @@ private Flux<ChatResponse> streamWithHttpClient(
* Apply thinking mode configuration to request if enabled.
*/
private void applyThinkingMode(DashScopeRequest request, GenerateOptions options) {
// Validate thinking configuration
if (options.getThinkingBudget() != null && !Boolean.TRUE.equals(enableThinking)) {
Boolean effectiveEnableThinking =
options.getEnableThinking() != null
? options.getEnableThinking()
: this.enableThinking;

// Validate: thinking mode requires streaming
if (Boolean.TRUE.equals(effectiveEnableThinking) && !this.stream) {
throw new IllegalStateException(
"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 enabled. To use thinking mode"
+ " with budget control, you must explicitly enable thinking by calling"
+ " .enableThinking(true) on the model builder. Example:"
"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())");
}

if (enableThinking != null) {
// Explicitly assign value for thinking mode
request.getParameters().setEnableThinking(enableThinking);
if (effectiveEnableThinking != null) {
request.getParameters().setEnableThinking(effectiveEnableThinking);
Comment thread
LearningGp marked this conversation as resolved.
}

if (Boolean.TRUE.equals(enableThinking) && options.getThinkingBudget() != null) {
if (Boolean.TRUE.equals(effectiveEnableThinking) && options.getThinkingBudget() != null) {
request.getParameters().setThinkingBudget(options.getThinkingBudget());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -229,6 +231,19 @@ public Double getPresencePenalty() {
return presencePenalty;
}

/**
* Gets whether thinking mode is enabled for this request.
*
* <p>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.
*
Expand Down Expand Up @@ -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);
Comment thread
LearningGp marked this conversation as resolved.
builder.thinkingBudget(
primary.thinkingBudget != null ? primary.thinkingBudget : fallback.thinkingBudget);
builder.reasoningEffort(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -666,6 +684,20 @@ public Builder presencePenalty(Double presencePenalty) {
return this;
}

/**
* Sets whether thinking mode is enabled for this request.
*
* <p>When set, this overrides the model-level {@code enableThinking} configuration,
* allowing per-request control over thinking mode.
*
* @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;
Comment thread
LearningGp marked this conversation as resolved.
return this;
}

/**
* Sets the thinking budget (maximum tokens for reasoning/thinking content).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -716,6 +732,48 @@ 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Loading