Skip to content
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
Expand Up @@ -16,6 +16,8 @@

package org.springframework.ai.model.openai.autoconfigure;

import java.util.Map;

import org.jspecify.annotations.Nullable;

import org.springframework.ai.document.MetadataMode;
Expand All @@ -24,6 +26,11 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;

/**
* OpenAI SDK Embedding autoconfiguration properties.
*
* @author guan xu
*/
@ConfigurationProperties(OpenAiEmbeddingProperties.CONFIG_PREFIX)
public class OpenAiEmbeddingProperties extends AbstractOpenAiProperties {

Expand All @@ -39,6 +46,8 @@ public class OpenAiEmbeddingProperties extends AbstractOpenAiProperties {

private @Nullable Integer dimensions;

private @Nullable Map<String, Object> extraBody;

public MetadataMode getMetadataMode() {
return this.metadataMode;
}
Expand Down Expand Up @@ -79,12 +88,21 @@ public void setDimensions(@Nullable Integer dimensions) {
this.dimensions = dimensions;
}

public @Nullable Map<String, Object> getExtraBody() {
return this.extraBody;
}

public void setExtraBody(@Nullable Map<String, Object> extraBody) {
this.extraBody = extraBody;
}

public OpenAiEmbeddingOptions toOptions() {
return OpenAiEmbeddingOptions.builder()
.model(this.model)
.user(this.user)
.encodingFormat(this.encodingFormat)
.dimensions(this.dimensions)
.extraBody(this.extraBody)
.build();
}

Expand Down Expand Up @@ -142,6 +160,16 @@ public void setDimensions(@Nullable Integer dimensions) {
OpenAiEmbeddingProperties.this.setDimensions(dimensions);
}

@DeprecatedConfigurationProperty(replacement = "spring.ai.openai.embedding.extra-body")
@Deprecated(since = "2.0.0", forRemoval = true)
public @Nullable Map<String, Object> getExtraBody() {
return OpenAiEmbeddingProperties.this.getExtraBody();
}

public void setExtraBody(@Nullable Map<String, Object> extraBody) {
OpenAiEmbeddingProperties.this.setExtraBody(extraBody);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration tests for {@link OpenAiEmbeddingAutoConfiguration}.
*
* @author guan xu
*/
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiChatAutoConfigurationIT {

Expand Down Expand Up @@ -127,4 +132,34 @@ void chatActivation() {

}

@Test
public void chatExtraBodyTest() {
this.contextRunner
.withPropertyValues(// @formatter:off
"spring.ai.openai.api-key=API_KEY",
"spring.ai.openai.base-url=http://TEST.BASE.URL",

"spring.ai.openai.chat.extra-body.key1=value1",
"spring.ai.openai.chat.extra-body.key2=123",
"spring.ai.openai.chat.extra-body.nested.key3=true"
)
// @formatter:on
.withConfiguration(
AutoConfigurations.of(OpenAiChatAutoConfiguration.class, ToolCallingAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(OpenAiChatProperties.class);

assertThat(chatProperties.getExtraBody()).isNotNull();
assertThat(chatProperties.getExtraBody()).containsEntry("key1", "value1");
assertThat(chatProperties.getExtraBody()).containsEntry("key2", "123");
assertThat(chatProperties.getExtraBody()).containsKey("nested");

var options = chatProperties.toOptions();
assertThat(options.getExtraBody()).isNotNull();
assertThat(options.getExtraBody()).containsEntry("key1", "value1");
assertThat(options.getExtraBody()).containsEntry("key2", "123");
assertThat(options.getExtraBody()).containsKey("nested");
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,35 @@ public void chatToolChoiceStringValuesTest() {
}
}

@Test
public void chatExtraBodyTest() {

this.contextRunner
.withPropertyValues(// @formatter:off
"spring.ai.openai.api-key=API_KEY",
"spring.ai.openai.base-url=http://TEST.BASE.URL",

"spring.ai.openai.chat.extra-body.key1=value1",
"spring.ai.openai.chat.extra-body.key2=123",
"spring.ai.openai.chat.extra-body.nested.key3=true"
)
// @formatter:on
.withConfiguration(
AutoConfigurations.of(OpenAiChatAutoConfiguration.class, ToolCallingAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(OpenAiChatProperties.class);

assertThat(chatProperties.getExtraBody()).isNotNull();
assertThat(chatProperties.getExtraBody()).containsEntry("key1", "value1");
assertThat(chatProperties.getExtraBody()).containsEntry("key2", "123");
assertThat(chatProperties.getExtraBody()).containsKey("nested");

var options = chatProperties.toOptions();
assertThat(options.getExtraBody()).isNotNull();
assertThat(options.getExtraBody()).containsEntry("key1", "value1");
assertThat(options.getExtraBody()).containsEntry("key2", "123");
assertThat(options.getExtraBody()).containsKey("nested");
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration tests for {@link OpenAiEmbeddingAutoConfiguration}.
*
* @author Sebastien Deleuze
* @author guan xu
*/
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiEmbeddingAutoConfigurationIT {
Expand Down Expand Up @@ -114,4 +117,34 @@ public void embeddingOptionsTest() {
});
}

@Test
void embeddingExtraBodyTest() {

this.contextRunner
.withPropertyValues(// @formatter:off
"spring.ai.openai.api-key=API_KEY",
"spring.ai.openai.base-url=http://TEST.BASE.URL",

"spring.ai.openai.embedding.extra-body.key1=value1",
"spring.ai.openai.embedding.extra-body.key2=123",
"spring.ai.openai.embedding.extra-body.nested.key3=true"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);

assertThat(embeddingProperties.getExtraBody()).isNotNull();
assertThat(embeddingProperties.getExtraBody()).containsEntry("key1", "value1");
assertThat(embeddingProperties.getExtraBody()).containsEntry("key2", "123");
assertThat(embeddingProperties.getExtraBody()).containsKey("nested");

var options = embeddingProperties.toOptions();
assertThat(options.getExtraBody()).isNotNull();
assertThat(options.getExtraBody()).containsEntry("key1", "value1");
assertThat(options.getExtraBody()).containsEntry("key2", "123");
assertThat(options.getExtraBody()).containsKey("nested");
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*
* @author Christian Tzolov
* @author Sebastien Deleuze
* @author guan xu
*/
public class OpenAiEmbeddingPropertiesTests {

Expand Down Expand Up @@ -86,4 +87,34 @@ public void embeddingOptionsTest() {
});
}

@Test
public void embeddingExtraBodyTest() {

this.contextRunner
.withPropertyValues(// @formatter:off
"spring.ai.openai.api-key=API_KEY",
"spring.ai.openai.base-url=http://TEST.BASE.URL",

"spring.ai.openai.embedding.extra-body.key1=value1",
"spring.ai.openai.embedding.extra-body.key2=123",
"spring.ai.openai.embedding.extra-body.nested.key3=true"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);

assertThat(embeddingProperties.getExtraBody()).isNotNull();
assertThat(embeddingProperties.getExtraBody()).containsEntry("key1", "value1");
assertThat(embeddingProperties.getExtraBody()).containsEntry("key2", "123");
assertThat(embeddingProperties.getExtraBody()).containsKey("nested");

var options = embeddingProperties.toOptions();
assertThat(options.getExtraBody()).isNotNull();
assertThat(options.getExtraBody()).containsEntry("key1", "value1");
assertThat(options.getExtraBody()).containsEntry("key2", "123");
assertThat(options.getExtraBody()).containsKey("nested");
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -905,11 +905,10 @@ else if (json.equals("required")) {
// Add extraBody parameters as additional body properties for OpenAI-compatible
// providers
if (requestOptions.getExtraBody() != null && !requestOptions.getExtraBody().isEmpty()) {
Map<String, com.openai.core.JsonValue> extraParams = requestOptions.getExtraBody()
Map<String, JsonValue> extraParams = requestOptions.getExtraBody()
.entrySet()
.stream()
.collect(java.util.stream.Collectors.toMap(Map.Entry::getKey,
entry -> com.openai.core.JsonValue.from(entry.getValue())));
.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));
builder.additionalBodyProperties(extraParams);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

import com.openai.azure.AzureOpenAIServiceVersion;
import com.openai.core.JsonValue;
import com.openai.credential.Credential;
import com.openai.models.embeddings.EmbeddingCreateParams;
import com.openai.models.embeddings.EmbeddingModel;
Expand All @@ -39,6 +41,7 @@
* @author Christian Tzolov
* @author Ilayaperumal Gopinathan
* @author Sebastien Deleuze
* @author guan xu
*/
public class OpenAiEmbeddingOptions extends AbstractOpenAiOptions implements EmbeddingOptions {

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

/**
* Extra parameters that are not part of the standard OpenAI API. These parameters are
* passed as additional body properties to support OpenAI-compatible providers like
* vLLM, Ollama, Groq, etc. that support custom parameters such as top_k,
* repetition_penalty, etc.
*/
private final @Nullable Map<String, Object> extraBody;

protected OpenAiEmbeddingOptions(@Nullable String baseUrl, @Nullable String apiKey, @Nullable Credential credential,
@Nullable String model, @Nullable String microsoftDeploymentName,
@Nullable AzureOpenAIServiceVersion microsoftFoundryServiceVersion, @Nullable String organizationId,
@Nullable Boolean isMicrosoftFoundry, @Nullable Boolean isGitHubModels, @Nullable Duration timeout,
@Nullable Integer maxRetries, @Nullable Proxy proxy, @Nullable Map<String, String> customHeaders,
@Nullable String user, @Nullable EncodingFormat encodingFormat, @Nullable Integer dimensions) {
@Nullable String user, @Nullable EncodingFormat encodingFormat, @Nullable Integer dimensions,
@Nullable Map<String, Object> extraBody) {
super(baseUrl, apiKey, credential, model != null ? model : DEFAULT_EMBEDDING_MODEL, microsoftDeploymentName,
microsoftFoundryServiceVersion, organizationId, isMicrosoftFoundry, isGitHubModels, timeout, maxRetries,
proxy, customHeaders);
this.user = user;
this.encodingFormat = encodingFormat;
this.dimensions = dimensions;
this.extraBody = (extraBody != null ? Map.copyOf(extraBody) : null);
}

public static Builder builder() {
Expand All @@ -92,6 +105,10 @@ public static Builder builder() {
return this.dimensions;
}

public @Nullable Map<String, Object> getExtraBody() {
return this.extraBody;
}

public EmbeddingCreateParams toOpenAiCreateParams(List<String> instructions) {

EmbeddingCreateParams.Builder builder = EmbeddingCreateParams.builder();
Expand All @@ -117,6 +134,17 @@ else if (this.getModel() != null) {
if (this.getDimensions() != null) {
builder.dimensions(this.getDimensions());
}

// Add extraBody parameters as additional body properties for OpenAI-compatible
// providers
if (this.getExtraBody() != null && !this.getExtraBody().isEmpty()) {
Map<String, JsonValue> extraParams = this.getExtraBody()
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonValue.from(entry.getValue())));
builder.additionalBodyProperties(extraParams);
}

return builder.build();
}

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

private @Nullable Integer dimensions;

private @Nullable Map<String, Object> extraBody;

public Builder from(OpenAiEmbeddingOptions fromOptions) {
// Parent class fields
this.baseUrl = fromOptions.getBaseUrl();
Expand All @@ -166,6 +196,7 @@ public Builder from(OpenAiEmbeddingOptions fromOptions) {
this.user = fromOptions.getUser();
this.encodingFormat = fromOptions.getEncodingFormat();
this.dimensions = fromOptions.getDimensions();
this.extraBody = fromOptions.getExtraBody();
return this;
}

Expand Down Expand Up @@ -223,6 +254,16 @@ public Builder merge(@Nullable EmbeddingOptions from) {
if (castFrom.getEncodingFormat() != null) {
this.encodingFormat = castFrom.getEncodingFormat();
}
if (castFrom.getExtraBody() != null) {
if (this.extraBody == null) {
this.extraBody = new HashMap<>(castFrom.getExtraBody());
}
else {
Map<String, Object> merged = new HashMap<>(this.extraBody);
merged.putAll(castFrom.getExtraBody());
this.extraBody = merged;
}
}
}
return this;
}
Expand Down Expand Up @@ -257,12 +298,17 @@ public Builder dimensions(@Nullable Integer dimensions) {
return this;
}

public Builder extraBody(@Nullable Map<String, Object> extraBody) {
this.extraBody = extraBody;
return this;
}

@Override
public OpenAiEmbeddingOptions build() {
return new OpenAiEmbeddingOptions(this.baseUrl, this.apiKey, this.credential, this.model,
this.microsoftDeploymentName, this.microsoftFoundryServiceVersion, this.organizationId,
this.isMicrosoftFoundry, this.isGitHubModels, this.timeout, this.maxRetries, this.proxy,
this.customHeaders, this.user, this.encodingFormat, this.dimensions);
this.customHeaders, this.user, this.encodingFormat, this.dimensions, this.extraBody);
}

}
Expand Down
Loading
Loading