Skip to content
Closed
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 @@ -37,6 +37,7 @@
* {@link ZhiPuAiEmbeddingProperties}.
*
* @author Geng Rong
* @author YunKui Lu
*/
public class ZhiPuAiPropertiesTests {

Expand Down Expand Up @@ -243,7 +244,9 @@ public void chatOptionsTest() {
"required": ["location", "lat", "lon", "unit"]
}
""",
"spring.ai.zhipuai.chat.options.user=userXYZ"
"spring.ai.zhipuai.chat.options.user=userXYZ",
"spring.ai.zhipuai.chat.options.response-format.type=json_object",
"spring.ai.zhipuai.chat.options.thinking.type=disabled"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
Expand All @@ -262,6 +265,8 @@ public void chatOptionsTest() {
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
assertThat(chatProperties.getOptions().getRequestId()).isEqualTo("RequestId");
assertThat(chatProperties.getOptions().getDoSample()).isEqualTo(Boolean.TRUE);
assertThat(chatProperties.getOptions().getResponseFormat().type()).isEqualTo("json_object");
assertThat(chatProperties.getOptions().getThinking().type()).isEqualTo("disabled");

JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}",
chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonIgnore;
Expand All @@ -30,9 +31,11 @@
import com.fasterxml.jackson.annotation.JsonProperty;

import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

Expand All @@ -42,6 +45,7 @@
* @author Geng Rong
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @author YunKui Lu
* @since 1.0.0 M1
*/
@JsonInclude(Include.NON_NULL)
Expand Down Expand Up @@ -104,6 +108,16 @@ public class ZhiPuAiChatOptions implements ToolCallingChatOptions {
*/
private @JsonProperty("do_sample") Boolean doSample;

/**
* Control the format of the model output. Set to `json_object` to ensure the message is a valid JSON object.
*/
private @JsonProperty("response_format") ChatCompletionRequest.ResponseFormat responseFormat;

/**
* Control whether to enable the large model's chain of thought. Available options: (default) enabled, disabled.
*/
private @JsonProperty("thinking") ChatCompletionRequest.Thinking thinking;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the default value be null if it's not explicitly set?

Copy link
Contributor Author

@YunKuiLu YunKuiLu Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ilayaperumalg Yes, the thinking in the request body will be null, but for models GLM-4.5 and above, setting thinking=null is equivalent to enabling chain-of-thought reasoning. Here's the English documentation: https://docs.z.ai/api-reference/llm/chat-completion#body-thinking-type

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YunKuiLu Thanks for the clarification!


/**
* Collection of {@link ToolCallback}s to be used for tool calling in the chat completion requests.
*/
Expand Down Expand Up @@ -146,6 +160,8 @@ public static ZhiPuAiChatOptions fromOptions(ZhiPuAiChatOptions fromOptions) {
.toolNames(fromOptions.getToolNames())
.internalToolExecutionEnabled(fromOptions.getInternalToolExecutionEnabled())
.toolContext(fromOptions.getToolContext())
.responseFormat(fromOptions.getResponseFormat())
.thinking(fromOptions.getThinking())
.build();
}

Expand Down Expand Up @@ -244,6 +260,24 @@ public void setDoSample(Boolean doSample) {
this.doSample = doSample;
}

public ChatCompletionRequest.ResponseFormat getResponseFormat() {
return this.responseFormat;
}

public ZhiPuAiChatOptions setResponseFormat(ChatCompletionRequest.ResponseFormat responseFormat) {
this.responseFormat = responseFormat;
return this;
}

public ChatCompletionRequest.Thinking getThinking() {
return this.thinking;
}

public ZhiPuAiChatOptions setThinking(ChatCompletionRequest.Thinking thinking) {
this.thinking = thinking;
return this;
}

@Override
@JsonIgnore
public Double getFrequencyPenalty() {
Expand Down Expand Up @@ -311,138 +345,53 @@ public Map<String, Object> getToolContext() {

@Override
public void setToolContext(Map<String, Object> toolContext) {
Assert.notNull(toolContext, "toolContext cannot be null");
this.toolContext = toolContext;
}

@Override
public final boolean equals(Object o) {
if (!(o instanceof ZhiPuAiChatOptions that)) {
return false;
}

return Objects.equals(this.model, that.model) && Objects.equals(this.maxTokens, that.maxTokens)
&& Objects.equals(this.stop, that.stop) && Objects.equals(this.temperature, that.temperature)
&& Objects.equals(this.topP, that.topP) && Objects.equals(this.tools, that.tools)
&& Objects.equals(this.toolChoice, that.toolChoice) && Objects.equals(this.user, that.user)
&& Objects.equals(this.requestId, that.requestId) && Objects.equals(this.doSample, that.doSample)
&& Objects.equals(this.responseFormat, that.responseFormat)
&& Objects.equals(this.thinking, that.thinking)
&& Objects.equals(this.toolCallbacks, that.toolCallbacks)
&& Objects.equals(this.toolNames, that.toolNames)
&& Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled)
&& Objects.equals(this.toolContext, that.toolContext);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.model == null) ? 0 : this.model.hashCode());
result = prime * result + ((this.maxTokens == null) ? 0 : this.maxTokens.hashCode());
result = prime * result + ((this.stop == null) ? 0 : this.stop.hashCode());
result = prime * result + ((this.temperature == null) ? 0 : this.temperature.hashCode());
result = prime * result + ((this.topP == null) ? 0 : this.topP.hashCode());
result = prime * result + ((this.tools == null) ? 0 : this.tools.hashCode());
result = prime * result + ((this.toolChoice == null) ? 0 : this.toolChoice.hashCode());
result = prime * result + ((this.user == null) ? 0 : this.user.hashCode());
result = prime * result
+ ((this.internalToolExecutionEnabled == null) ? 0 : this.internalToolExecutionEnabled.hashCode());
result = prime * result + ((this.toolCallbacks == null) ? 0 : this.toolCallbacks.hashCode());
result = prime * result + ((this.toolNames == null) ? 0 : this.toolNames.hashCode());
result = prime * result + ((this.toolContext == null) ? 0 : this.toolContext.hashCode());
int result = Objects.hashCode(this.model);
result = 31 * result + Objects.hashCode(this.maxTokens);
result = 31 * result + Objects.hashCode(this.stop);
result = 31 * result + Objects.hashCode(this.temperature);
result = 31 * result + Objects.hashCode(this.topP);
result = 31 * result + Objects.hashCode(this.tools);
result = 31 * result + Objects.hashCode(this.toolChoice);
result = 31 * result + Objects.hashCode(this.user);
result = 31 * result + Objects.hashCode(this.requestId);
result = 31 * result + Objects.hashCode(this.doSample);
result = 31 * result + Objects.hashCode(this.responseFormat);
result = 31 * result + Objects.hashCode(this.thinking);
result = 31 * result + Objects.hashCode(this.toolCallbacks);
result = 31 * result + Objects.hashCode(this.toolNames);
result = 31 * result + Objects.hashCode(this.internalToolExecutionEnabled);
result = 31 * result + Objects.hashCode(this.toolContext);
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ZhiPuAiChatOptions other = (ZhiPuAiChatOptions) obj;
if (this.model == null) {
if (other.model != null) {
return false;
}
}
else if (!this.model.equals(other.model)) {
return false;
}
if (this.maxTokens == null) {
if (other.maxTokens != null) {
return false;
}
}
else if (!this.maxTokens.equals(other.maxTokens)) {
return false;
}
if (this.stop == null) {
if (other.stop != null) {
return false;
}
}
else if (!this.stop.equals(other.stop)) {
return false;
}
if (this.temperature == null) {
if (other.temperature != null) {
return false;
}
}
else if (!this.temperature.equals(other.temperature)) {
return false;
}
if (this.topP == null) {
if (other.topP != null) {
return false;
}
}
else if (!this.topP.equals(other.topP)) {
return false;
}
if (this.tools == null) {
if (other.tools != null) {
return false;
}
}
else if (!this.tools.equals(other.tools)) {
return false;
}
if (this.toolChoice == null) {
if (other.toolChoice != null) {
return false;
}
}
else if (!this.toolChoice.equals(other.toolChoice)) {
return false;
}
if (this.user == null) {
if (other.user != null) {
return false;
}
}
else if (!this.user.equals(other.user)) {
return false;
}
if (this.requestId == null) {
if (other.requestId != null) {
return false;
}
}
else if (!this.requestId.equals(other.requestId)) {
return false;
}
if (this.doSample == null) {
if (other.doSample != null) {
return false;
}
}
else if (!this.doSample.equals(other.doSample)) {
return false;
}
if (this.internalToolExecutionEnabled == null) {
if (other.internalToolExecutionEnabled != null) {
return false;
}
}
else if (!this.internalToolExecutionEnabled.equals(other.internalToolExecutionEnabled)) {
return false;
}
if (this.toolContext == null) {
if (other.toolContext != null) {
return false;
}
}
else if (!this.toolContext.equals(other.toolContext)) {
return false;
}
return true;
public String toString() {
return "ZhiPuAiChatOptions: " + ModelOptionsUtils.toJsonString(this);
}

@Override
Expand Down Expand Up @@ -610,6 +559,16 @@ public Builder toolContext(Map<String, Object> toolContext) {
return this;
}

public Builder responseFormat(ChatCompletionRequest.ResponseFormat responseFormat) {
this.options.responseFormat = responseFormat;
return this;
}

public Builder thinking(ChatCompletionRequest.Thinking thinking) {
this.options.thinking = thinking;
return this;
}

public ZhiPuAiChatOptions build() {
return this.options;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,9 @@ public void setJsonSchema(String jsonSchema) {
* logged and can be used for debugging purposes.
* @param doSample If set, the model will use sampling to generate the next token. If
* not set, the model will use greedy decoding to generate the next token.
* @param responseFormat Control the format of the model output. Set to `json_object`
* to ensure the message is a valid JSON object.
* @param thinking Control whether to enable the large model's chain of thought.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest(// @formatter:off
Expand All @@ -664,9 +667,11 @@ public record ChatCompletionRequest(// @formatter:off
@JsonProperty("top_p") Double topP,
@JsonProperty("tools") List<FunctionTool> tools,
@JsonProperty("tool_choice") Object toolChoice,
@JsonProperty("user") String user,
@JsonProperty("user_id") String user,
@JsonProperty("request_id") String requestId,
@JsonProperty("do_sample") Boolean doSample) { // @formatter:on
@JsonProperty("do_sample") Boolean doSample,
@JsonProperty("response_format") ResponseFormat responseFormat,
@JsonProperty("thinking") Thinking thinking) { // @formatter:on

/**
* Shortcut constructor for a chat completion request with the given messages and
Expand All @@ -676,7 +681,7 @@ public record ChatCompletionRequest(// @formatter:off
* @param temperature What sampling temperature to use, between 0 and 1.
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature) {
this(messages, model, null, null, false, temperature, null, null, null, null, null, null);
this(messages, model, null, null, false, temperature, null, null, null, null, null, null, null, null);
}

/**
Expand All @@ -691,7 +696,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature,
boolean stream) {
this(messages, model, null, null, stream, temperature, null, null, null, null, null, null);
this(messages, model, null, null, stream, temperature, null, null, null, null, null, null, null, null);
}

/**
Expand All @@ -706,7 +711,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, List<FunctionTool> tools,
Object toolChoice) {
this(messages, model, null, null, false, 0.8, null, tools, toolChoice, null, null, null);
this(messages, model, null, null, false, 0.8, null, tools, toolChoice, null, null, null, null, null);
}

/**
Expand All @@ -719,7 +724,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
* terminated by a data: [DONE] message.
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, Boolean stream) {
this(messages, null, null, null, stream, null, null, null, null, null, null, null);
this(messages, null, null, null, stream, null, null, null, null, null, null, null, null, null);
}

/**
Expand Down Expand Up @@ -754,7 +759,32 @@ public static Object function(String functionName) {
*/
@JsonInclude(Include.NON_NULL)
public record ResponseFormat(@JsonProperty("type") String type) {

public static ResponseFormat text() {
return new ResponseFormat("text");
}

public static ResponseFormat jsonObject() {
return new ResponseFormat("json_object");
}
}

/**
* Control whether to enable the large model's chain of thought
*
* @param type Available options: (default) enabled, disabled
*/
@JsonInclude(Include.NON_NULL)
public record Thinking(@JsonProperty("type") String type) {
public static Thinking enabled() {
return new Thinking("enabled");
}

public static Thinking disabled() {
return new Thinking("disabled");
}
}

}

/**
Expand Down
Loading