Skip to content

Commit 96d6686

Browse files
committed
Add support for max_completion_tokens in AzureOpenAI chat options request
An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens. Replaces max_tokens field which is now deprecated.
1 parent 3919204 commit 96d6686

File tree

7 files changed

+100
-77
lines changed

7 files changed

+100
-77
lines changed

models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2023-2025 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

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

712+
mergedAzureOptions.setMaxCompletionTokens((fromAzureOptions.getMaxCompletionTokens() != null)
713+
? fromAzureOptions.getMaxCompletionTokens() : toSpringAiOptions.getMaxCompletionTokens());
714+
715715
mergedAzureOptions.setLogitBias(fromAzureOptions.getLogitBias() != null ? fromAzureOptions.getLogitBias()
716716
: toSpringAiOptions.getLogitBias());
717717

@@ -795,6 +795,10 @@ private ChatCompletionsOptions merge(AzureOpenAiChatOptions fromSpringAiOptions,
795795
mergedAzureOptions.setMaxTokens(fromSpringAiOptions.getMaxTokens());
796796
}
797797

798+
if (fromSpringAiOptions.getMaxCompletionTokens() != null) {
799+
mergedAzureOptions.setMaxCompletionTokens(fromSpringAiOptions.getMaxCompletionTokens());
800+
}
801+
798802
if (fromSpringAiOptions.getLogitBias() != null) {
799803
mergedAzureOptions.setLogitBias(fromSpringAiOptions.getLogitBias());
800804
}
@@ -886,6 +890,9 @@ private ChatCompletionsOptions copy(ChatCompletionsOptions fromOptions) {
886890
if (fromOptions.getMaxTokens() != null) {
887891
copyOptions.setMaxTokens(fromOptions.getMaxTokens());
888892
}
893+
if (fromOptions.getMaxCompletionTokens() != null) {
894+
copyOptions.setMaxCompletionTokens(fromOptions.getMaxCompletionTokens());
895+
}
889896
if (fromOptions.getLogitBias() != null) {
890897
copyOptions.setLogitBias(fromOptions.getLogitBias());
891898
}

models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2023-2024 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

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

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

139139
/**
140140
* The response format expected from the Azure OpenAI model
141+
*
141142
* @see org.springframework.ai.azure.openai.AzureOpenAiResponseFormat for supported
142143
* formats
143144
*/
@@ -167,6 +168,13 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions {
167168
@JsonProperty("top_log_probs")
168169
private Integer topLogProbs;
169170

171+
/*
172+
* An upper bound for the number of tokens that can be generated for a completion,
173+
* including visible output tokens and reasoning tokens.
174+
*/
175+
@JsonProperty("max_completion_tokens")
176+
private Integer maxCompletionTokens;
177+
170178
/*
171179
* If provided, the configuration options for available Azure OpenAI chat
172180
* enhancements.
@@ -266,6 +274,7 @@ public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOpti
266274
.frequencyPenalty(fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty() : null)
267275
.logitBias(fromOptions.getLogitBias())
268276
.maxTokens(fromOptions.getMaxTokens())
277+
.maxCompletionTokens(fromOptions.getMaxCompletionTokens())
269278
.N(fromOptions.getN())
270279
.presencePenalty(fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty() : null)
271280
.stop(fromOptions.getStop() != null ? new ArrayList<>(fromOptions.getStop()) : null)
@@ -300,6 +309,14 @@ public void setMaxTokens(Integer maxTokens) {
300309
this.maxTokens = maxTokens;
301310
}
302311

312+
public Integer getMaxCompletionTokens() {
313+
return this.maxCompletionTokens;
314+
}
315+
316+
public void setMaxCompletionTokens(Integer maxCompletionTokens) {
317+
this.maxCompletionTokens = maxCompletionTokens;
318+
}
319+
303320
public Map<String, Integer> getLogitBias() {
304321
return this.logitBias;
305322
}
@@ -510,6 +527,7 @@ public boolean equals(Object o) {
510527
&& Objects.equals(this.enableStreamUsage, that.enableStreamUsage)
511528
&& Objects.equals(this.reasoningEffort, that.reasoningEffort)
512529
&& Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.maxTokens, that.maxTokens)
530+
&& Objects.equals(this.maxCompletionTokens, that.maxCompletionTokens)
513531
&& Objects.equals(this.frequencyPenalty, that.frequencyPenalty)
514532
&& Objects.equals(this.presencePenalty, that.presencePenalty)
515533
&& Objects.equals(this.temperature, that.temperature) && Objects.equals(this.topP, that.topP);
@@ -520,8 +538,8 @@ public int hashCode() {
520538
return Objects.hash(this.logitBias, this.user, this.n, this.stop, this.deploymentName, this.responseFormat,
521539
this.toolCallbacks, this.toolNames, this.internalToolExecutionEnabled, this.seed, this.logprobs,
522540
this.topLogProbs, this.enhancements, this.streamOptions, this.reasoningEffort, this.enableStreamUsage,
523-
this.toolContext, this.maxTokens, this.frequencyPenalty, this.presencePenalty, this.temperature,
524-
this.topP);
541+
this.toolContext, this.maxTokens, this.maxCompletionTokens, this.frequencyPenalty, this.presencePenalty,
542+
this.temperature, this.topP);
525543
}
526544

527545
public static class Builder {
@@ -556,6 +574,11 @@ public Builder maxTokens(Integer maxTokens) {
556574
return this;
557575
}
558576

577+
public Builder maxCompletionTokens(Integer maxCompletionTokens) {
578+
this.options.maxCompletionTokens = maxCompletionTokens;
579+
return this;
580+
}
581+
559582
public Builder N(Integer n) {
560583
this.options.n = n;
561584
return this;

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2023-2024 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

1714
package org.springframework.ai.azure.openai;
@@ -60,7 +57,7 @@ public void createRequestWithChatOptions() {
6057
.frequencyPenalty(696.9)
6158
.presencePenalty(969.6)
6259
.logitBias(Map.of("foo", 1))
63-
.maxTokens(969)
60+
.maxCompletionTokens(969)
6461
.N(69)
6562
.stop(List.of("foo", "bar"))
6663
.topP(0.69)
@@ -86,7 +83,7 @@ public void createRequestWithChatOptions() {
8683
assertThat(requestOptions.getFrequencyPenalty()).isEqualTo(696.9);
8784
assertThat(requestOptions.getPresencePenalty()).isEqualTo(969.6);
8885
assertThat(requestOptions.getLogitBias()).isEqualTo(Map.of("foo", 1));
89-
assertThat(requestOptions.getMaxTokens()).isEqualTo(969);
86+
assertThat(requestOptions.getMaxCompletionTokens()).isEqualTo(969);
9087
assertThat(requestOptions.getN()).isEqualTo(69);
9188
assertThat(requestOptions.getStop()).isEqualTo(List.of("foo", "bar"));
9289
assertThat(requestOptions.getTopP()).isEqualTo(0.69);
@@ -106,7 +103,7 @@ public void createRequestWithChatOptions() {
106103
.frequencyPenalty(100.0)
107104
.presencePenalty(100.0)
108105
.logitBias(Map.of("foo", 2))
109-
.maxTokens(100)
106+
.maxCompletionTokens(100)
110107
.N(100)
111108
.stop(List.of("foo", "bar"))
112109
.topP(0.111)
@@ -127,7 +124,7 @@ public void createRequestWithChatOptions() {
127124
assertThat(requestOptions.getFrequencyPenalty()).isEqualTo(100.0);
128125
assertThat(requestOptions.getPresencePenalty()).isEqualTo(100.0);
129126
assertThat(requestOptions.getLogitBias()).isEqualTo(Map.of("foo", 2));
130-
assertThat(requestOptions.getMaxTokens()).isEqualTo(100);
127+
assertThat(requestOptions.getMaxCompletionTokens()).isEqualTo(100);
131128
assertThat(requestOptions.getN()).isEqualTo(100);
132129
assertThat(requestOptions.getStop()).isEqualTo(List.of("foo", "bar"));
133130
assertThat(requestOptions.getTopP()).isEqualTo(0.111);
@@ -144,7 +141,7 @@ public void createRequestWithChatOptions() {
144141
public void createChatOptionsWithPresencePenaltyAndFrequencyPenalty(Double presencePenalty,
145142
Double frequencyPenalty) {
146143
var options = AzureOpenAiChatOptions.builder()
147-
.maxTokens(800)
144+
.maxCompletionTokens(800)
148145
.temperature(0.7)
149146
.topP(0.95)
150147
.presencePenalty(presencePenalty)

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatClientIT.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2023-2024 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

1714
package org.springframework.ai.azure.openai;
@@ -164,7 +161,8 @@ public OpenAIClientBuilder openAIClient() {
164161
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder) {
165162
return AzureOpenAiChatModel.builder()
166163
.openAIClientBuilder(openAIClientBuilder)
167-
.defaultOptions(AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxTokens(1000).build())
164+
.defaultOptions(
165+
AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxCompletionTokens(1000).build())
168166
.build();
169167
}
170168

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelIT.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2023-2025 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

1714
package org.springframework.ai.azure.openai;
@@ -306,7 +303,8 @@ public OpenAIClientBuilder openAIClientBuilder() {
306303
public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder) {
307304
return AzureOpenAiChatModel.builder()
308305
.openAIClientBuilder(openAIClientBuilder)
309-
.defaultOptions(AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxTokens(1000).build())
306+
.defaultOptions(
307+
AzureOpenAiChatOptions.builder().deploymentName("gpt-4o").maxCompletionTokens(1000).build())
310308
.build();
311309
}
312310

models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptionsTests.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
/*
22
* Copyright 2025-2025 the original author or authors.
33
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
76
*
8-
* https://www.apache.org/licenses/LICENSE-2.0
7+
* https://www.apache.org/licenses/LICENSE-2.0
98
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
1512
*/
1613

1714
package org.springframework.ai.azure.openai;
@@ -51,6 +48,7 @@ void testBuilderWithAllFields() {
5148
.frequencyPenalty(0.5)
5249
.logitBias(Map.of("token1", 1, "token2", -1))
5350
.maxTokens(200)
51+
.maxCompletionTokens(400)
5452
.N(2)
5553
.presencePenalty(0.8)
5654
.stop(List.of("stop1", "stop2"))
@@ -68,10 +66,10 @@ void testBuilderWithAllFields() {
6866
.build();
6967

7068
assertThat(options)
71-
.extracting("deploymentName", "frequencyPenalty", "logitBias", "maxTokens", "n", "presencePenalty", "stop",
72-
"temperature", "topP", "user", "responseFormat", "streamUsage", "reasoningEffort", "seed",
73-
"logprobs", "topLogProbs", "enhancements", "streamOptions")
74-
.containsExactly("test-deployment", 0.5, Map.of("token1", 1, "token2", -1), 200, 2, 0.8,
69+
.extracting("deploymentName", "frequencyPenalty", "logitBias", "maxTokens", "maxCompletionTokens", "n",
70+
"presencePenalty", "stop", "temperature", "topP", "user", "responseFormat", "streamUsage",
71+
"reasoningEffort", "seed", "logprobs", "topLogProbs", "enhancements", "streamOptions")
72+
.containsExactly("test-deployment", 0.5, Map.of("token1", 1, "token2", -1), 200, 400, 2, 0.8,
7573
List.of("stop1", "stop2"), 0.7, 0.9, "test-user", responseFormat, true, "low", 12345L, true, 5,
7674
enhancements, streamOptions);
7775
}
@@ -93,6 +91,7 @@ void testCopy() {
9391
.frequencyPenalty(0.5)
9492
.logitBias(Map.of("token1", 1, "token2", -1))
9593
.maxTokens(200)
94+
.maxCompletionTokens(400)
9695
.N(2)
9796
.presencePenalty(0.8)
9897
.stop(List.of("stop1", "stop2"))
@@ -131,6 +130,7 @@ void testSetters() {
131130
options.setFrequencyPenalty(0.5);
132131
options.setLogitBias(Map.of("token1", 1, "token2", -1));
133132
options.setMaxTokens(200);
133+
options.setMaxCompletionTokens(400);
134134
options.setN(2);
135135
options.setPresencePenalty(0.8);
136136
options.setStop(List.of("stop1", "stop2"));
@@ -153,6 +153,7 @@ void testSetters() {
153153
assertThat(options.getFrequencyPenalty()).isEqualTo(0.5);
154154
assertThat(options.getLogitBias()).isEqualTo(Map.of("token1", 1, "token2", -1));
155155
assertThat(options.getMaxTokens()).isEqualTo(200);
156+
assertThat(options.getMaxCompletionTokens()).isEqualTo(400);
156157
assertThat(options.getN()).isEqualTo(2);
157158
assertThat(options.getPresencePenalty()).isEqualTo(0.8);
158159
assertThat(options.getStop()).isEqualTo(List.of("stop1", "stop2"));
@@ -178,6 +179,7 @@ void testDefaultValues() {
178179
assertThat(options.getFrequencyPenalty()).isNull();
179180
assertThat(options.getLogitBias()).isNull();
180181
assertThat(options.getMaxTokens()).isNull();
182+
assertThat(options.getMaxCompletionTokens()).isNull();
181183
assertThat(options.getN()).isNull();
182184
assertThat(options.getPresencePenalty()).isNull();
183185
assertThat(options.getStop()).isNull();

0 commit comments

Comments
 (0)