Skip to content

Commit 347378a

Browse files
committed
Add promptCacheKey and safetyIdentifier support, document extraBody and reasoningContent
- Add promptCacheKey and safetyIdentifier fields to OpenAiChatOptions - Add integration and unit tests for new fields - Document extraBody parameter for OpenAI-compatible servers - Document reasoningContent field for reasoning models - Update configuration documentation Signed-off-by: SenreySong <25841017+SenreySong@users.noreply.github.com> Signed-off-by: Mark Pollack <mark.pollack@broadcom.com>
1 parent ec717c1 commit 347378a

File tree

8 files changed

+352
-19
lines changed

8 files changed

+352
-19
lines changed

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
237237
*/
238238
private @JsonProperty("service_tier") String serviceTier;
239239

240+
/**
241+
* A cache key used by OpenAI to optimize cache hit rates for similar requests.
242+
* Improves latency and reduces costs. Replaces the deprecated {@code user} field for caching purposes.
243+
* <a href="https://platform.openai.com/docs/guides/prompt-caching">Learn more</a>.
244+
*/
245+
private @JsonProperty("prompt_cache_key") String promptCacheKey;
246+
247+
/**
248+
* A stable identifier to help OpenAI detect users violating usage policies.
249+
* Should be a hashed value (e.g., hashed username or email). Replaces the deprecated {@code user} field for safety tracking.
250+
* <a href="https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers">Learn more</a>.
251+
*/
252+
private @JsonProperty("safety_identifier") String safetyIdentifier;
253+
240254
/**
241255
* Collection of {@link ToolCallback}s to be used for tool calling in the chat completion requests.
242256
*/
@@ -264,6 +278,20 @@ public class OpenAiChatOptions implements ToolCallingChatOptions {
264278
@JsonIgnore
265279
private Map<String, Object> toolContext = new HashMap<>();
266280

281+
/**
282+
* Additional parameters to pass to OpenAI-compatible servers. Accepts any key-value pairs
283+
* that will be included at the top level of the JSON request. Intended for use with
284+
* vLLM, Ollama, and other OpenAI-compatible servers that support parameters beyond the
285+
* standard OpenAI API (e.g., {@code top_k}, {@code repetition_penalty}). The official
286+
* OpenAI API ignores unknown parameters.
287+
* <p>
288+
* Example:
289+
* <pre>{@code
290+
* OpenAiChatOptions.builder()
291+
* .extraBody(Map.of("top_k", 50, "repetition_penalty", 1.1))
292+
* .build()
293+
* }</pre>
294+
*/
267295
private @JsonProperty("extra_body") Map<String, Object> extraBody;
268296

269297
// @formatter:on
@@ -308,6 +336,8 @@ public static OpenAiChatOptions fromOptions(OpenAiChatOptions fromOptions) {
308336
.webSearchOptions(fromOptions.getWebSearchOptions())
309337
.verbosity(fromOptions.getVerbosity())
310338
.serviceTier(fromOptions.getServiceTier())
339+
.promptCacheKey(fromOptions.getPromptCacheKey())
340+
.safetyIdentifier(fromOptions.getSafetyIdentifier())
311341
.extraBody(fromOptions.getExtraBody())
312342
.build();
313343
}
@@ -629,6 +659,22 @@ public void setServiceTier(String serviceTier) {
629659
this.serviceTier = serviceTier;
630660
}
631661

662+
public String getPromptCacheKey() {
663+
return this.promptCacheKey;
664+
}
665+
666+
public void setPromptCacheKey(String promptCacheKey) {
667+
this.promptCacheKey = promptCacheKey;
668+
}
669+
670+
public String getSafetyIdentifier() {
671+
return this.safetyIdentifier;
672+
}
673+
674+
public void setSafetyIdentifier(String safetyIdentifier) {
675+
this.safetyIdentifier = safetyIdentifier;
676+
}
677+
632678
@Override
633679
public OpenAiChatOptions copy() {
634680
return OpenAiChatOptions.fromOptions(this);
@@ -641,8 +687,8 @@ public int hashCode() {
641687
this.streamOptions, this.seed, this.stop, this.temperature, this.topP, this.tools, this.toolChoice,
642688
this.user, this.parallelToolCalls, this.toolCallbacks, this.toolNames, this.httpHeaders,
643689
this.internalToolExecutionEnabled, this.toolContext, this.outputModalities, this.outputAudio,
644-
this.store, this.metadata, this.reasoningEffort, this.webSearchOptions, this.serviceTier,
645-
this.extraBody);
690+
this.store, this.metadata, this.reasoningEffort, this.webSearchOptions, this.verbosity,
691+
this.serviceTier, this.promptCacheKey, this.safetyIdentifier, this.extraBody);
646692
}
647693

648694
@Override
@@ -678,6 +724,8 @@ public boolean equals(Object o) {
678724
&& Objects.equals(this.webSearchOptions, other.webSearchOptions)
679725
&& Objects.equals(this.verbosity, other.verbosity)
680726
&& Objects.equals(this.serviceTier, other.serviceTier)
727+
&& Objects.equals(this.promptCacheKey, other.promptCacheKey)
728+
&& Objects.equals(this.safetyIdentifier, other.safetyIdentifier)
681729
&& Objects.equals(this.extraBody, other.extraBody);
682730
}
683731

@@ -946,6 +994,16 @@ public Builder serviceTier(OpenAiApi.ServiceTier serviceTier) {
946994
return this;
947995
}
948996

997+
public Builder promptCacheKey(String promptCacheKey) {
998+
this.options.promptCacheKey = promptCacheKey;
999+
return this;
1000+
}
1001+
1002+
public Builder safetyIdentifier(String safetyIdentifier) {
1003+
this.options.safetyIdentifier = safetyIdentifier;
1004+
return this;
1005+
}
1006+
9491007
public Builder extraBody(Map<String, Object> extraBody) {
9501008
this.options.extraBody = extraBody;
9511009
return this;

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,8 @@ public record ChatCompletionRequest(// @formatter:off
11311131
@JsonProperty("reasoning_effort") String reasoningEffort,
11321132
@JsonProperty("web_search_options") WebSearchOptions webSearchOptions,
11331133
@JsonProperty("verbosity") String verbosity,
1134+
@JsonProperty("prompt_cache_key") String promptCacheKey,
1135+
@JsonProperty("safety_identifier") String safetyIdentifier,
11341136
Map<String, Object> extraBody) {
11351137

11361138
/**
@@ -1143,7 +1145,7 @@ public record ChatCompletionRequest(// @formatter:off
11431145
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature) {
11441146
this(messages, model, null, null, null, null, null, null, null, null, null, null, null, null, null,
11451147
null, null, null, false, null, temperature, null,
1146-
null, null, null, null, null, null, null, null);
1148+
null, null, null, null, null, null, null, null, null, null);
11471149
}
11481150

11491151
/**
@@ -1157,7 +1159,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
11571159
this(messages, model, null, null, null, null, null, null,
11581160
null, null, null, List.of(OutputModality.AUDIO, OutputModality.TEXT), audio, null, null,
11591161
null, null, null, stream, null, null, null,
1160-
null, null, null, null, null, null, null, null);
1162+
null, null, null, null, null, null, null, null, null, null);
11611163
}
11621164

11631165
/**
@@ -1172,7 +1174,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
11721174
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Double temperature, boolean stream) {
11731175
this(messages, model, null, null, null, null, null, null, null, null, null,
11741176
null, null, null, null, null, null, null, stream, null, temperature, null,
1175-
null, null, null, null, null, null, null, null);
1177+
null, null, null, null, null, null, null, null, null, null);
11761178
}
11771179

11781180
/**
@@ -1188,7 +1190,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
11881190
List<FunctionTool> tools, Object toolChoice) {
11891191
this(messages, model, null, null, null, null, null, null, null, null, null,
11901192
null, null, null, null, null, null, null, false, null, 0.8, null,
1191-
tools, toolChoice, null, null, null, null, null, null);
1193+
tools, toolChoice, null, null, null, null, null, null, null, null);
11921194
}
11931195

11941196
/**
@@ -1201,7 +1203,7 @@ public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
12011203
public ChatCompletionRequest(List<ChatCompletionMessage> messages, Boolean stream) {
12021204
this(messages, null, null, null, null, null, null, null, null, null, null, null, null, null,
12031205
null, null, null, null, stream, null, null, null, null, null, null, null, null, null,
1204-
null, null);
1206+
null, null, null, null);
12051207
}
12061208

12071209
/**
@@ -1214,7 +1216,8 @@ public ChatCompletionRequest streamOptions(StreamOptions streamOptions) {
12141216
return new ChatCompletionRequest(this.messages, this.model, this.store, this.metadata, this.frequencyPenalty, this.logitBias, this.logprobs,
12151217
this.topLogprobs, this.maxTokens, this.maxCompletionTokens, this.n, this.outputModalities, this.audioParameters, this.presencePenalty,
12161218
this.responseFormat, this.seed, this.serviceTier, this.stop, this.stream, streamOptions, this.temperature, this.topP,
1217-
this.tools, this.toolChoice, this.parallelToolCalls, this.user, this.reasoningEffort, this.webSearchOptions, this.verbosity, this.extraBody);
1219+
this.tools, this.toolChoice, this.parallelToolCalls, this.user, this.reasoningEffort, this.webSearchOptions, this.verbosity,
1220+
this.promptCacheKey, this.safetyIdentifier, this.extraBody);
12181221
}
12191222

12201223
/**

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,20 @@ void testBuilderWithAllFields() {
8484
.httpHeaders(Map.of("header1", "value1"))
8585
.toolContext(toolContext)
8686
.serviceTier(ServiceTier.PRIORITY)
87+
.promptCacheKey("test-cache-key")
88+
.safetyIdentifier("test-safety-id")
8789
.build();
8890

8991
assertThat(options)
9092
.extracting("model", "frequencyPenalty", "logitBias", "logprobs", "topLogprobs", "maxTokens",
9193
"maxCompletionTokens", "n", "outputModalities", "outputAudio", "presencePenalty", "responseFormat",
9294
"streamOptions", "seed", "stop", "temperature", "topP", "tools", "toolChoice", "user",
9395
"parallelToolCalls", "store", "metadata", "reasoningEffort", "internalToolExecutionEnabled",
94-
"httpHeaders", "toolContext", "serviceTier")
96+
"httpHeaders", "toolContext", "serviceTier", "promptCacheKey", "safetyIdentifier")
9597
.containsExactly("test-model", 0.5, logitBias, true, 5, null, 50, 2, outputModalities, outputAudio, 0.8,
9698
responseFormat, streamOptions, 12345, stopSequences, 0.7, 0.9, tools, toolChoice, "test-user", true,
9799
false, metadata, "medium", false, Map.of("header1", "value1"), toolContext,
98-
ServiceTier.PRIORITY.getValue());
100+
ServiceTier.PRIORITY.getValue(), "test-cache-key", "test-safety-id");
99101

100102
assertThat(options.getStreamUsage()).isTrue();
101103
assertThat(options.getStreamOptions()).isEqualTo(StreamOptions.INCLUDE_USAGE);
@@ -144,6 +146,8 @@ void testCopy() {
144146
.internalToolExecutionEnabled(true)
145147
.httpHeaders(Map.of("header1", "value1"))
146148
.serviceTier(ServiceTier.DEFAULT)
149+
.promptCacheKey("copy-test-cache")
150+
.safetyIdentifier("copy-test-safety")
147151
.build();
148152

149153
OpenAiChatOptions copiedOptions = originalOptions.copy();

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void testExtraBodySerializationFlattensToTopLevel() throws Exception {
4242
ChatCompletionRequest request = new ChatCompletionRequest(List.of(), // messages
4343
"gpt-4", // model
4444
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false,
45-
null, null, null, null, null, null, null, null, null, null,
45+
null, null, null, null, null, null, null, null, null, null, null, null,
4646
Map.of("top_k", 50, "repetition_penalty", 1.1) // extraBody
4747
);
4848

@@ -71,8 +71,8 @@ void testExtraBodyWithEmptyMap() throws Exception {
7171
ChatCompletionRequest request = new ChatCompletionRequest(List.of(), // messages
7272
"gpt-4", // model
7373
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false,
74-
null, null, null, null, null, null, null, null, null, null, Map.of() // empty
75-
// extraBody
74+
null, null, null, null, null, null, null, null, null, null, null, null, Map.of() // empty
75+
// extraBody
7676
);
7777

7878
// Act
@@ -93,9 +93,9 @@ void testExtraBodyNullSerialization() throws Exception {
9393
ChatCompletionRequest request = new ChatCompletionRequest(List.of(), // messages
9494
"gpt-4", // model
9595
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false,
96-
null, null, null, null, null, null, null, null, null, null, null // extraBody
97-
// =
98-
// null
96+
null, null, null, null, null, null, null, null, null, null, null, null, null // extraBody
97+
// =
98+
// null
9999
);
100100

101101
// Act

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ void validateReasoningTokens() {
8181
ChatCompletionMessage.Role.USER);
8282
ChatCompletionRequest request = new ChatCompletionRequest(List.of(userMessage), "gpt-5", null, null, null, null,
8383
null, null, null, null, null, null, null, null, null, null, null, null, false, null, null, null, null,
84-
null, null, null, "high", null, null, null);
84+
null, null, null, "high", null, null, null, null, null);
8585
ResponseEntity<ChatCompletion> response = this.openAiApi.chatCompletionEntity(request);
8686

8787
assertThat(response).isNotNull();
@@ -185,7 +185,7 @@ void chatCompletionEntityWithNewModelsAndLowVerbosity(OpenAiApi.ChatModel modelN
185185

186186
ChatCompletionRequest request = new ChatCompletionRequest(List.of(chatCompletionMessage), // messages
187187
modelName.getValue(), null, null, null, null, null, null, null, null, null, null, null, null, null,
188-
null, null, null, false, null, 1.0, null, null, null, null, null, null, null, "low", null);
188+
null, null, null, false, null, 1.0, null, null, null, null, null, null, null, "low", null, null, null);
189189

190190
ResponseEntity<ChatCompletion> response = this.openAiApi.chatCompletionEntity(request);
191191

@@ -232,7 +232,7 @@ void chatCompletionEntityWithServiceTier(OpenAiApi.ServiceTier serviceTier) {
232232
ChatCompletionRequest request = new ChatCompletionRequest(List.of(chatCompletionMessage), // messages
233233
OpenAiApi.ChatModel.GPT_4_O.value, null, null, null, null, null, null, null, null, null, null, null,
234234
null, null, null, serviceTier.getValue(), null, false, null, 1.0, null, null, null, null, null, null,
235-
null, null, null);
235+
null, null, null, null, null);
236236

237237
ResponseEntity<ChatCompletion> response = this.openAiApi.chatCompletionEntity(request);
238238

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,19 @@ void testReasoningEffortParameter() {
816816
}
817817
}
818818

819+
@Test
820+
void shouldSendPromptCacheKeyAndSafetyIdentifier() {
821+
OpenAiChatOptions options = OpenAiChatOptions.builder()
822+
.promptCacheKey("test-cache-" + System.currentTimeMillis())
823+
.safetyIdentifier("hashed-user-123")
824+
.build();
825+
826+
ChatResponse response = this.openAiChatModel.call(new Prompt("Tell me a joke about Spring", options));
827+
828+
assertThat(response).isNotNull();
829+
assertThat(response.getResults()).isNotEmpty();
830+
}
831+
819832
record ActorsFilmsRecord(String actor, List<String> movies) {
820833

821834
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,9 @@ Although this is optional for JSON Schema, it's recommended for the structured r
515515
Ollama is OpenAI API-compatible and you can use the xref:api/chat/openai-chat.adoc[Spring AI OpenAI] client to talk to Ollama and use tools.
516516
For this, you need to configure the OpenAI base URL to your Ollama instance: `spring.ai.openai.chat.base-url=http://localhost:11434` and select one of the provided Ollama models: `spring.ai.openai.chat.options.model=mistral`.
517517

518+
TIP: When using the OpenAI client with Ollama, you can pass Ollama-specific parameters (like `top_k`, `repeat_penalty`, `num_predict`) using the xref:api/chat/openai-chat.adoc#openai-compatible-servers[`extraBody` option].
519+
This allows you to leverage Ollama's full capabilities while using the OpenAI client.
520+
518521
image::spring-ai-ollama-over-openai.jpg[Ollama OpenAI API compatibility, 800, 600, align="center"]
519522

520523
Check the link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/proxy/OllamaWithOpenAiChatModelIT.java[OllamaWithOpenAiChatModelIT.java] tests for examples of using Ollama over Spring AI OpenAI.

0 commit comments

Comments
 (0)