Skip to content

Commit ee41e48

Browse files
committed
Support extraBody parameter for OpenAiEmbeddingModel
Signed-off-by: guanxu <1510424541@qq.com>
1 parent 0df8a45 commit ee41e48

3 files changed

Lines changed: 92 additions & 5 deletions

File tree

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -903,11 +903,10 @@ else if (json.equals("required")) {
903903
// Add extraBody parameters as additional body properties for OpenAI-compatible
904904
// providers
905905
if (requestOptions.getExtraBody() != null && !requestOptions.getExtraBody().isEmpty()) {
906-
Map<String, com.openai.core.JsonValue> extraParams = requestOptions.getExtraBody()
906+
Map<String, JsonValue> extraParams = requestOptions.getExtraBody()
907907
.entrySet()
908908
.stream()
909-
.collect(java.util.stream.Collectors.toMap(Map.Entry::getKey,
910-
entry -> com.openai.core.JsonValue.from(entry.getValue())));
909+
.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));
911910
builder.additionalBodyProperties(extraParams);
912911
}
913912

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
import java.util.List;
2323
import java.util.Locale;
2424
import java.util.Map;
25+
import java.util.stream.Collectors;
2526

2627
import com.openai.azure.AzureOpenAIServiceVersion;
28+
import com.openai.core.JsonValue;
2729
import com.openai.credential.Credential;
2830
import com.openai.models.embeddings.EmbeddingCreateParams;
2931
import com.openai.models.embeddings.EmbeddingModel;
@@ -39,6 +41,7 @@
3941
* @author Christian Tzolov
4042
* @author Ilayaperumal Gopinathan
4143
* @author Sebastien Deleuze
44+
* @author guan xu
4245
*/
4346
public class OpenAiEmbeddingOptions extends AbstractOpenAiOptions implements EmbeddingOptions {
4447

@@ -61,18 +64,28 @@ public class OpenAiEmbeddingOptions extends AbstractOpenAiOptions implements Emb
6164
*/
6265
private final @Nullable Integer dimensions;
6366

67+
/**
68+
* Extra parameters that are not part of the standard OpenAI API. These parameters are
69+
* passed as additional body properties to support OpenAI-compatible providers like
70+
* vLLM, Ollama, Groq, etc. that support custom parameters such as top_k,
71+
* repetition_penalty, etc.
72+
*/
73+
private final @Nullable Map<String, Object> extraBody;
74+
6475
protected OpenAiEmbeddingOptions(@Nullable String baseUrl, @Nullable String apiKey, @Nullable Credential credential,
6576
@Nullable String model, @Nullable String microsoftDeploymentName,
6677
@Nullable AzureOpenAIServiceVersion microsoftFoundryServiceVersion, @Nullable String organizationId,
6778
@Nullable Boolean isMicrosoftFoundry, @Nullable Boolean isGitHubModels, @Nullable Duration timeout,
6879
@Nullable Integer maxRetries, @Nullable Proxy proxy, @Nullable Map<String, String> customHeaders,
69-
@Nullable String user, @Nullable EncodingFormat encodingFormat, @Nullable Integer dimensions) {
80+
@Nullable String user, @Nullable EncodingFormat encodingFormat, @Nullable Integer dimensions,
81+
@Nullable Map<String, Object> extraBody) {
7082
super(baseUrl, apiKey, credential, model != null ? model : DEFAULT_EMBEDDING_MODEL, microsoftDeploymentName,
7183
microsoftFoundryServiceVersion, organizationId, isMicrosoftFoundry, isGitHubModels, timeout, maxRetries,
7284
proxy, customHeaders);
7385
this.user = user;
7486
this.encodingFormat = encodingFormat;
7587
this.dimensions = dimensions;
88+
this.extraBody = (extraBody != null ? Map.copyOf(extraBody) : null);
7689
}
7790

7891
public static Builder builder() {
@@ -92,6 +105,10 @@ public static Builder builder() {
92105
return this.dimensions;
93106
}
94107

108+
public @Nullable Map<String, Object> getExtraBody() {
109+
return this.extraBody;
110+
}
111+
95112
public EmbeddingCreateParams toOpenAiCreateParams(List<String> instructions) {
96113

97114
EmbeddingCreateParams.Builder builder = EmbeddingCreateParams.builder();
@@ -117,6 +134,17 @@ else if (this.getModel() != null) {
117134
if (this.getDimensions() != null) {
118135
builder.dimensions(this.getDimensions());
119136
}
137+
138+
// Add extraBody parameters as additional body properties for OpenAI-compatible
139+
// providers
140+
if (this.getExtraBody() != null && !this.getExtraBody().isEmpty()) {
141+
Map<String, JsonValue> extraParams = this.getExtraBody()
142+
.entrySet()
143+
.stream()
144+
.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));
145+
builder.additionalBodyProperties(extraParams);
146+
}
147+
120148
return builder.build();
121149
}
122150

@@ -147,6 +175,8 @@ public static final class Builder extends AbstractBuilder<OpenAiEmbeddingOptions
147175

148176
private @Nullable Integer dimensions;
149177

178+
private @Nullable Map<String, Object> extraBody;
179+
150180
public Builder from(OpenAiEmbeddingOptions fromOptions) {
151181
// Parent class fields
152182
this.baseUrl = fromOptions.getBaseUrl();
@@ -166,6 +196,7 @@ public Builder from(OpenAiEmbeddingOptions fromOptions) {
166196
this.user = fromOptions.getUser();
167197
this.encodingFormat = fromOptions.getEncodingFormat();
168198
this.dimensions = fromOptions.getDimensions();
199+
this.extraBody = fromOptions.getExtraBody();
169200
return this;
170201
}
171202

@@ -223,6 +254,16 @@ public Builder merge(@Nullable EmbeddingOptions from) {
223254
if (castFrom.getEncodingFormat() != null) {
224255
this.encodingFormat = castFrom.getEncodingFormat();
225256
}
257+
if (castFrom.getExtraBody() != null) {
258+
if (this.extraBody == null) {
259+
this.extraBody = new HashMap<>(castFrom.getExtraBody());
260+
}
261+
else {
262+
Map<String, Object> merged = new HashMap<>(this.extraBody);
263+
merged.putAll(castFrom.getExtraBody());
264+
this.extraBody = merged;
265+
}
266+
}
226267
}
227268
return this;
228269
}
@@ -257,12 +298,17 @@ public Builder dimensions(@Nullable Integer dimensions) {
257298
return this;
258299
}
259300

301+
public Builder extraBody(@Nullable Map<String, Object> extraBody) {
302+
this.extraBody = extraBody;
303+
return this;
304+
}
305+
260306
@Override
261307
public OpenAiEmbeddingOptions build() {
262308
return new OpenAiEmbeddingOptions(this.baseUrl, this.apiKey, this.credential, this.model,
263309
this.microsoftDeploymentName, this.microsoftFoundryServiceVersion, this.organizationId,
264310
this.isMicrosoftFoundry, this.isGitHubModels, this.timeout, this.maxRetries, this.proxy,
265-
this.customHeaders, this.user, this.encodingFormat, this.dimensions);
311+
this.customHeaders, this.user, this.encodingFormat, this.dimensions, this.extraBody);
266312
}
267313

268314
}

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,46 @@ void testOptionsBuilderMergeCustomHeaders() {
9999
.containsEntry("merged-header2", "merged-value2");
100100
}
101101

102+
@Test
103+
void extraBodyIsPassedAsAdditionalBodyProperties() {
104+
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder()
105+
.model("test-model")
106+
.extraBody(Map.of("top_k", 40, "repetition_penalty", 1.2))
107+
.build();
108+
109+
EmbeddingCreateParams createParams = options.toOpenAiCreateParams(List.of("test input"));
110+
111+
assertThat(options.getExtraBody()).containsEntry("top_k", 40).containsEntry("repetition_penalty", 1.2);
112+
assertThat(createParams._additionalBodyProperties()).containsKeys("top_k", "repetition_penalty");
113+
assertThat(createParams._additionalBodyProperties().get("top_k").asNumber().get()).isEqualTo(40);
114+
assertThat(createParams._additionalBodyProperties().get("repetition_penalty").asNumber().get()).isEqualTo(1.2);
115+
}
116+
117+
@Test
118+
void extraBodyIsCopiedAndMerged() {
119+
OpenAiEmbeddingOptions source = OpenAiEmbeddingOptions.builder()
120+
.model("test-model")
121+
.extraBody(Map.of("top_k", 40))
122+
.build();
123+
124+
OpenAiEmbeddingOptions copied = OpenAiEmbeddingOptions.builder().from(source).build();
125+
OpenAiEmbeddingOptions merged = OpenAiEmbeddingOptions.builder()
126+
.extraBody(Map.of("repetition_penalty", 1.2))
127+
.merge(source)
128+
.build();
129+
130+
assertThat(copied.getExtraBody()).containsEntry("top_k", 40);
131+
assertThat(merged.getExtraBody()).containsEntry("top_k", 40).containsEntry("repetition_penalty", 1.2);
132+
}
133+
134+
@Test
135+
void emptyExtraBodyIsNotAddedToCreateParams() {
136+
OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder().model("test-model").build();
137+
138+
EmbeddingCreateParams createParams = options.toOpenAiCreateParams(List.of("test input"));
139+
140+
assertThat(options.getExtraBody()).isNull();
141+
assertThat(createParams._additionalBodyProperties()).isEmpty();
142+
}
143+
102144
}

0 commit comments

Comments
 (0)