Skip to content

Add support for max_completion_tokens in AzureOpenAI chat options request and update the test and document. #3305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
@@ -1,17 +1,14 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -712,6 +709,9 @@ private ChatCompletionsOptions merge(ChatCompletionsOptions fromAzureOptions,
mergedAzureOptions.setMaxTokens((fromAzureOptions.getMaxTokens() != null) ? fromAzureOptions.getMaxTokens()
: toSpringAiOptions.getMaxTokens());

mergedAzureOptions.setMaxCompletionTokens((fromAzureOptions.getMaxCompletionTokens() != null)
? fromAzureOptions.getMaxCompletionTokens() : toSpringAiOptions.getMaxCompletionTokens());

mergedAzureOptions.setLogitBias(fromAzureOptions.getLogitBias() != null ? fromAzureOptions.getLogitBias()
: toSpringAiOptions.getLogitBias());

Expand Down Expand Up @@ -795,6 +795,10 @@ private ChatCompletionsOptions merge(AzureOpenAiChatOptions fromSpringAiOptions,
mergedAzureOptions.setMaxTokens(fromSpringAiOptions.getMaxTokens());
}

if (fromSpringAiOptions.getMaxCompletionTokens() != null) {
mergedAzureOptions.setMaxCompletionTokens(fromSpringAiOptions.getMaxCompletionTokens());
}

if (fromSpringAiOptions.getLogitBias() != null) {
mergedAzureOptions.setLogitBias(fromSpringAiOptions.getLogitBias());
}
Expand Down Expand Up @@ -886,6 +890,9 @@ private ChatCompletionsOptions copy(ChatCompletionsOptions fromOptions) {
if (fromOptions.getMaxTokens() != null) {
copyOptions.setMaxTokens(fromOptions.getMaxTokens());
}
if (fromOptions.getMaxCompletionTokens() != null) {
copyOptions.setMaxCompletionTokens(fromOptions.getMaxCompletionTokens());
}
if (fromOptions.getLogitBias() != null) {
copyOptions.setLogitBias(fromOptions.getLogitBias());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -54,6 +51,9 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {

/**
* The maximum number of tokens to generate.
*
* This value is now deprecated in favor of `max_completion_tokens`, and is not
* compatible with o1 series models.
*/
@JsonProperty("max_tokens")
private Integer maxTokens;
Expand Down Expand Up @@ -138,6 +138,7 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {

/**
* The response format expected from the Azure OpenAI model
*
* @see org.springframework.ai.azure.openai.AzureOpenAiResponseFormat for supported
* formats
*/
Expand Down Expand Up @@ -167,6 +168,13 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
@JsonProperty("top_log_probs")
private Integer topLogProbs;

/*
* An upper bound for the number of tokens that can be generated for a completion,
* including visible output tokens and reasoning tokens.
*/
@JsonProperty("max_completion_tokens")
private Integer maxCompletionTokens;

/*
* If provided, the configuration options for available Azure OpenAI chat
* enhancements.
Expand Down Expand Up @@ -266,6 +274,7 @@ public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOpti
.frequencyPenalty(fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty() : null)
.logitBias(fromOptions.getLogitBias())
.maxTokens(fromOptions.getMaxTokens())
.maxCompletionTokens(fromOptions.getMaxCompletionTokens())
.N(fromOptions.getN())
.presencePenalty(fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty() : null)
.stop(fromOptions.getStop() != null ? new ArrayList<>(fromOptions.getStop()) : null)
Expand Down Expand Up @@ -300,6 +309,14 @@ public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}

public Integer getMaxCompletionTokens() {
return this.maxCompletionTokens;
}

public void setMaxCompletionTokens(Integer maxCompletionTokens) {
this.maxCompletionTokens = maxCompletionTokens;
}

public Map<String, Integer> getLogitBias() {
return this.logitBias;
}
Expand Down Expand Up @@ -510,6 +527,7 @@ public boolean equals(Object o) {
&& Objects.equals(this.enableStreamUsage, that.enableStreamUsage)
&& Objects.equals(this.reasoningEffort, that.reasoningEffort)
&& Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.maxTokens, that.maxTokens)
&& Objects.equals(this.maxCompletionTokens, that.maxCompletionTokens)
&& Objects.equals(this.frequencyPenalty, that.frequencyPenalty)
&& Objects.equals(this.presencePenalty, that.presencePenalty)
&& Objects.equals(this.temperature, that.temperature) && Objects.equals(this.topP, that.topP);
Expand All @@ -520,8 +538,8 @@ public int hashCode() {
return Objects.hash(this.logitBias, this.user, this.n, this.stop, this.deploymentName, this.responseFormat,
this.toolCallbacks, this.toolNames, this.internalToolExecutionEnabled, this.seed, this.logprobs,
this.topLogProbs, this.enhancements, this.streamOptions, this.reasoningEffort, this.enableStreamUsage,
this.toolContext, this.maxTokens, this.frequencyPenalty, this.presencePenalty, this.temperature,
this.topP);
this.toolContext, this.maxTokens, this.maxCompletionTokens, this.frequencyPenalty, this.presencePenalty,
this.temperature, this.topP);
}

public static class Builder {
Expand Down Expand Up @@ -556,6 +574,11 @@ public Builder maxTokens(Integer maxTokens) {
return this;
}

public Builder maxCompletionTokens(Integer maxCompletionTokens) {
this.options.maxCompletionTokens = maxCompletionTokens;
return this;
}

public Builder N(Integer n) {
this.options.n = n;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -60,7 +57,7 @@ public void createRequestWithChatOptions() {
.frequencyPenalty(696.9)
.presencePenalty(969.6)
.logitBias(Map.of("foo", 1))
.maxTokens(969)
.maxCompletionTokens(969)
.N(69)
.stop(List.of("foo", "bar"))
.topP(0.69)
Expand All @@ -86,7 +83,7 @@ public void createRequestWithChatOptions() {
assertThat(requestOptions.getFrequencyPenalty()).isEqualTo(696.9);
assertThat(requestOptions.getPresencePenalty()).isEqualTo(969.6);
assertThat(requestOptions.getLogitBias()).isEqualTo(Map.of("foo", 1));
assertThat(requestOptions.getMaxTokens()).isEqualTo(969);
assertThat(requestOptions.getMaxCompletionTokens()).isEqualTo(969);
assertThat(requestOptions.getN()).isEqualTo(69);
assertThat(requestOptions.getStop()).isEqualTo(List.of("foo", "bar"));
assertThat(requestOptions.getTopP()).isEqualTo(0.69);
Expand All @@ -106,7 +103,7 @@ public void createRequestWithChatOptions() {
.frequencyPenalty(100.0)
.presencePenalty(100.0)
.logitBias(Map.of("foo", 2))
.maxTokens(100)
.maxCompletionTokens(100)
.N(100)
.stop(List.of("foo", "bar"))
.topP(0.111)
Expand All @@ -127,7 +124,7 @@ public void createRequestWithChatOptions() {
assertThat(requestOptions.getFrequencyPenalty()).isEqualTo(100.0);
assertThat(requestOptions.getPresencePenalty()).isEqualTo(100.0);
assertThat(requestOptions.getLogitBias()).isEqualTo(Map.of("foo", 2));
assertThat(requestOptions.getMaxTokens()).isEqualTo(100);
assertThat(requestOptions.getMaxCompletionTokens()).isEqualTo(100);
assertThat(requestOptions.getN()).isEqualTo(100);
assertThat(requestOptions.getStop()).isEqualTo(List.of("foo", "bar"));
assertThat(requestOptions.getTopP()).isEqualTo(0.111);
Expand All @@ -144,7 +141,7 @@ public void createRequestWithChatOptions() {
public void createChatOptionsWithPresencePenaltyAndFrequencyPenalty(Double presencePenalty,
Double frequencyPenalty) {
var options = AzureOpenAiChatOptions.builder()
.maxTokens(800)
.maxCompletionTokens(800)
.temperature(0.7)
.topP(0.95)
.presencePenalty(presencePenalty)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -164,7 +161,8 @@ public OpenAIClientBuilder openAIClient() {
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder) {
return AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxTokens(1000).build())
.defaultOptions(
AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxCompletionTokens(1000).build())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -306,7 +303,8 @@ public OpenAIClientBuilder openAIClientBuilder() {
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder) {
return AzureOpenAiChatModel.builder()
.openAIClientBuilder(openAIClientBuilder)
.defaultOptions(AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxTokens(1000).build())
.defaultOptions(
AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxCompletionTokens(1000).build())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package org.springframework.ai.azure.openai;
Expand Down Expand Up @@ -51,6 +48,7 @@ void testBuilderWithAllFields() {
.frequencyPenalty(0.5)
.logitBias(Map.of("token1", 1, "token2", -1))
.maxTokens(200)
.maxCompletionTokens(400)
.N(2)
.presencePenalty(0.8)
.stop(List.of("stop1", "stop2"))
Expand All @@ -68,10 +66,10 @@ void testBuilderWithAllFields() {
.build();

assertThat(options)
.extracting("deploymentName", "frequencyPenalty", "logitBias", "maxTokens", "n", "presencePenalty", "stop",
"temperature", "topP", "user", "responseFormat", "streamUsage", "reasoningEffort", "seed",
"logprobs", "topLogProbs", "enhancements", "streamOptions")
.containsExactly("test-deployment", 0.5, Map.of("token1", 1, "token2", -1), 200, 2, 0.8,
.extracting("deploymentName", "frequencyPenalty", "logitBias", "maxTokens", "maxCompletionTokens", "n",
"presencePenalty", "stop", "temperature", "topP", "user", "responseFormat", "streamUsage",
"reasoningEffort", "seed", "logprobs", "topLogProbs", "enhancements", "streamOptions")
.containsExactly("test-deployment", 0.5, Map.of("token1", 1, "token2", -1), 200, 400, 2, 0.8,
List.of("stop1", "stop2"), 0.7, 0.9, "test-user", responseFormat, true, "low", 12345L, true, 5,
enhancements, streamOptions);
}
Expand All @@ -93,6 +91,7 @@ void testCopy() {
.frequencyPenalty(0.5)
.logitBias(Map.of("token1", 1, "token2", -1))
.maxTokens(200)
.maxCompletionTokens(400)
.N(2)
.presencePenalty(0.8)
.stop(List.of("stop1", "stop2"))
Expand Down Expand Up @@ -131,6 +130,7 @@ void testSetters() {
options.setFrequencyPenalty(0.5);
options.setLogitBias(Map.of("token1", 1, "token2", -1));
options.setMaxTokens(200);
options.setMaxCompletionTokens(400);
options.setN(2);
options.setPresencePenalty(0.8);
options.setStop(List.of("stop1", "stop2"));
Expand All @@ -153,6 +153,7 @@ void testSetters() {
assertThat(options.getFrequencyPenalty()).isEqualTo(0.5);
assertThat(options.getLogitBias()).isEqualTo(Map.of("token1", 1, "token2", -1));
assertThat(options.getMaxTokens()).isEqualTo(200);
assertThat(options.getMaxCompletionTokens()).isEqualTo(400);
assertThat(options.getN()).isEqualTo(2);
assertThat(options.getPresencePenalty()).isEqualTo(0.8);
assertThat(options.getStop()).isEqualTo(List.of("stop1", "stop2"));
Expand All @@ -178,6 +179,7 @@ void testDefaultValues() {
assertThat(options.getFrequencyPenalty()).isNull();
assertThat(options.getLogitBias()).isNull();
assertThat(options.getMaxTokens()).isNull();
assertThat(options.getMaxCompletionTokens()).isNull();
assertThat(options.getN()).isNull();
assertThat(options.getPresencePenalty()).isNull();
assertThat(options.getStop()).isNull();
Expand Down
Loading