Skip to content

Commit ad2e1bc

Browse files
rafaelrddcsobychacko
authored andcommitted
GH-4235: Add support for OpenAI service_tier in OpenAiChatOptions
Fixes #4235 Signed-off-by: Rafael Cunha <[email protected]>
1 parent 7bb435a commit ad2e1bc

File tree

6 files changed

+92
-5
lines changed

6 files changed

+92
-5
lines changed

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
233233
*/
234234
private @JsonProperty("web_search_options") WebSearchOptions webSearchOptions;
235235

236+
/**
237+
* Specifies the <a href="https://platform.openai.com/docs/api-reference/responses/create#responses_create-service_tier">processing type</a> used for serving the request.
238+
*/
239+
private @JsonProperty("service_tier") String serviceTier;
240+
236241
/**
237242
* Collection of {@link ToolCallback}s to be used for tool calling in the chat completion requests.
238243
*/
@@ -301,6 +306,7 @@ public static OpenAiChatOptions fromOptions(OpenAiChatOptions fromOptions) {
301306
.reasoningEffort(fromOptions.getReasoningEffort())
302307
.webSearchOptions(fromOptions.getWebSearchOptions())
303308
.verbosity(fromOptions.getVerbosity())
309+
.serviceTier(fromOptions.getServiceTier())
304310
.build();
305311
}
306312

@@ -605,6 +611,14 @@ public void setVerbosity(String verbosity) {
605611
this.verbosity = verbosity;
606612
}
607613

614+
public String getServiceTier() {
615+
return serviceTier;
616+
}
617+
618+
public void setServiceTier(String serviceTier) {
619+
this.serviceTier = serviceTier;
620+
}
621+
608622
@Override
609623
public OpenAiChatOptions copy() {
610624
return OpenAiChatOptions.fromOptions(this);
@@ -617,7 +631,7 @@ public int hashCode() {
617631
this.streamOptions, this.seed, this.stop, this.temperature, this.topP, this.tools, this.toolChoice,
618632
this.user, this.parallelToolCalls, this.toolCallbacks, this.toolNames, this.httpHeaders,
619633
this.internalToolExecutionEnabled, this.toolContext, this.outputModalities, this.outputAudio,
620-
this.store, this.metadata, this.reasoningEffort, this.webSearchOptions);
634+
this.store, this.metadata, this.reasoningEffort, this.webSearchOptions, this.serviceTier);
621635
}
622636

623637
@Override
@@ -651,7 +665,8 @@ public boolean equals(Object o) {
651665
&& Objects.equals(this.metadata, other.metadata)
652666
&& Objects.equals(this.reasoningEffort, other.reasoningEffort)
653667
&& Objects.equals(this.webSearchOptions, other.webSearchOptions)
654-
&& Objects.equals(this.verbosity, other.verbosity);
668+
&& Objects.equals(this.verbosity, other.verbosity)
669+
&& Objects.equals(this.serviceTier, other.serviceTier);
655670
}
656671

657672
@Override
@@ -909,6 +924,16 @@ public Builder verbosity(String verbosity) {
909924
return this;
910925
}
911926

927+
public Builder serviceTier(String serviceTier) {
928+
this.options.serviceTier = serviceTier;
929+
return this;
930+
}
931+
932+
public Builder serviceTier(OpenAiApi.ServiceTier serviceTier) {
933+
this.options.serviceTier = serviceTier.getValue();
934+
return this;
935+
}
936+
912937
public OpenAiChatOptions build() {
913938
return this.options;
914939
}

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,41 @@ public record Approximate(@JsonProperty("city") String city, @JsonProperty("coun
13451345

13461346
} // @formatter:on
13471347

1348+
/**
1349+
* Specifies the processing type used for serving the request.
1350+
*/
1351+
public enum ServiceTier {
1352+
1353+
/**
1354+
* Then the request will be processed with the service tier configured in the
1355+
* Project settings.
1356+
*/
1357+
AUTO("auto"),
1358+
/**
1359+
* Then the request will be processed with the standard pricing.
1360+
*/
1361+
DEFAULT("default"),
1362+
/**
1363+
* Then the request will be processed with the flex pricing.
1364+
*/
1365+
FLEX("flex"),
1366+
/**
1367+
* Then the request will be processed with the priority pricing.
1368+
*/
1369+
PRIORITY("priority");
1370+
1371+
private final String value;
1372+
1373+
private ServiceTier(String value) {
1374+
this.value = value;
1375+
}
1376+
1377+
public String getValue() {
1378+
return value;
1379+
}
1380+
1381+
}
1382+
13481383
/**
13491384
* Message comprising the conversation.
13501385
*

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.ai.openai.api.OpenAiApi;
2727
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.AudioParameters;
2828
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.StreamOptions;
29+
import org.springframework.ai.openai.api.OpenAiApi.ServiceTier;
2930
import org.springframework.ai.openai.api.ResponseFormat;
3031

3132
import static org.assertj.core.api.Assertions.assertThat;
@@ -83,17 +84,19 @@ void testBuilderWithAllFields() {
8384
.internalToolExecutionEnabled(false)
8485
.httpHeaders(Map.of("header1", "value1"))
8586
.toolContext(toolContext)
87+
.serviceTier(ServiceTier.PRIORITY)
8688
.build();
8789

8890
assertThat(options)
8991
.extracting("model", "frequencyPenalty", "logitBias", "logprobs", "topLogprobs", "maxTokens",
9092
"maxCompletionTokens", "n", "outputModalities", "outputAudio", "presencePenalty", "responseFormat",
9193
"streamOptions", "seed", "stop", "temperature", "topP", "tools", "toolChoice", "user",
9294
"parallelToolCalls", "store", "metadata", "reasoningEffort", "internalToolExecutionEnabled",
93-
"httpHeaders", "toolContext")
95+
"httpHeaders", "toolContext", "serviceTier")
9496
.containsExactly("test-model", 0.5, logitBias, true, 5, null, 50, 2, outputModalities, outputAudio, 0.8,
9597
responseFormat, streamOptions, 12345, stopSequences, 0.7, 0.9, tools, toolChoice, "test-user", true,
96-
false, metadata, "medium", false, Map.of("header1", "value1"), toolContext);
98+
false, metadata, "medium", false, Map.of("header1", "value1"), toolContext,
99+
ServiceTier.PRIORITY.getValue());
97100

98101
assertThat(options.getStreamUsage()).isTrue();
99102
assertThat(options.getStreamOptions()).isEqualTo(StreamOptions.INCLUDE_USAGE);
@@ -141,6 +144,7 @@ void testCopy() {
141144
.reasoningEffort("low")
142145
.internalToolExecutionEnabled(true)
143146
.httpHeaders(Map.of("header1", "value1"))
147+
.serviceTier(ServiceTier.DEFAULT)
144148
.build();
145149

146150
OpenAiChatOptions copiedOptions = originalOptions.copy();
@@ -189,6 +193,7 @@ void testSetters() {
189193
options.setReasoningEffort("high");
190194
options.setInternalToolExecutionEnabled(false);
191195
options.setHttpHeaders(Map.of("header2", "value2"));
196+
options.setServiceTier(ServiceTier.DEFAULT.getValue());
192197

193198
assertThat(options.getModel()).isEqualTo("test-model");
194199
assertThat(options.getFrequencyPenalty()).isEqualTo(0.5);
@@ -223,6 +228,7 @@ void testSetters() {
223228
options.setStopSequences(List.of("s1", "s2"));
224229
assertThat(options.getStopSequences()).isEqualTo(List.of("s1", "s2"));
225230
assertThat(options.getStop()).isEqualTo(List.of("s1", "s2"));
231+
assertThat(options.getServiceTier()).isEqualTo("default");
226232
}
227233

228234
@Test
@@ -258,6 +264,7 @@ void testDefaultValues() {
258264
assertThat(options.getToolContext()).isEqualTo(new HashMap<>());
259265
assertThat(options.getStreamUsage()).isFalse();
260266
assertThat(options.getStopSequences()).isNull();
267+
assertThat(options.getServiceTier()).isNull();
261268
}
262269

263270
@Test

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiApiIT.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,22 @@ void chatCompletionEntityWithGpt5ChatAndTemperatureShouldSucceed(OpenAiApi.ChatM
218218
assertThat(response.getBody().model()).containsIgnoringCase(modelName.getValue());
219219
}
220220

221+
@ParameterizedTest(name = "{0} : {displayName}")
222+
@EnumSource(names = { "DEFAULT", "PRIORITY" })
223+
void chatCompletionEntityWithServiceTier(OpenAiApi.ServiceTier serviceTier) {
224+
ChatCompletionMessage chatCompletionMessage = new ChatCompletionMessage(
225+
"What is the answer to the ultimate question of life, the universe, and everything?", Role.USER);
226+
227+
ChatCompletionRequest request = new ChatCompletionRequest(List.of(chatCompletionMessage), // messages
228+
OpenAiApi.ChatModel.GPT_4_O.value, null, null, null, null, null, null, null, null, null, null, null,
229+
null, null, null, serviceTier.getValue(), null, false, null, 1.0, null, null, null, null, null, null,
230+
null, null);
231+
232+
ResponseEntity<ChatCompletion> response = this.openAiApi.chatCompletionEntity(request);
233+
234+
assertThat(response).isNotNull();
235+
assertThat(response.getBody()).isNotNull();
236+
assertThat(response.getBody().serviceTier()).containsIgnoringCase(serviceTier.getValue());
237+
}
238+
221239
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/OpenAiStreamFunctionCallingHelperTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.assertj.core.api.Assertions.assertThat;
2424
import org.junit.jupiter.api.Test;
2525
import org.mockito.Mockito;
26+
import org.springframework.ai.openai.api.OpenAiApi.ServiceTier;
2627

2728
/**
2829
* Unit tests for {@link OpenAiStreamFunctionCallingHelper}
@@ -36,7 +37,7 @@ public class OpenAiStreamFunctionCallingHelperTest {
3637
@Test
3738
public void merge_whenInputIsValid() {
3839
var expectedResult = new OpenAiApi.ChatCompletionChunk("id", Collections.emptyList(),
39-
System.currentTimeMillis(), "model", "serviceTier", "fingerPrint", "object", null);
40+
System.currentTimeMillis(), "model", "default", "fingerPrint", "object", null);
4041
var previous = new OpenAiApi.ChatCompletionChunk(null, null, expectedResult.created(), expectedResult.model(),
4142
expectedResult.serviceTier(), null, null, null);
4243
var current = new OpenAiApi.ChatCompletionChunk(expectedResult.id(), null, null, null, null,

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ The `JSON_SCHEMA` type enables link:https://platform.openai.com/docs/guides/stru
177177
| spring.ai.openai.chat.options.parallel-tool-calls | Whether to enable link:https://platform.openai.com/docs/guides/function-calling/parallel-function-calling[parallel function calling] during tool use. | true
178178
| spring.ai.openai.chat.options.http-headers | Optional HTTP headers to be added to the chat completion request. To override the `api-key` you need to use an `Authorization` header key, and you have to prefix the key value with the `Bearer` prefix. | -
179179
| spring.ai.openai.chat.options.proxy-tool-calls | If true, the Spring AI will not handle the function calls internally, but will proxy them to the client. Then is the client's responsibility to handle the function calls, dispatch them to the appropriate function, and return the results. If false (the default), the Spring AI will handle the function calls internally. Applicable only for chat models with function calling support | false
180+
| spring.ai.openai.chat.options.service-tier | Specifies the link:https://platform.openai.com/docs/api-reference/responses/create#responses_create-service_tier[processing type] used for serving the request. | -
180181
|====
181182

182183
[NOTE]

0 commit comments

Comments
 (0)