Clarify the OpenAi properties

Add dedicated property classes for the Chat and the Embedding models and use a common connection properties for both:
  - OpenAiConnectionProperties (baseUrl, apiKey) with prefix spring.ai.openai.*.
  - OpenAiChatProperties (model, temperature) with prefix spring.ai.openai.chat.
  - OpenAiEmbeddingProperties (model) with prefix spring.ai.openai.embedding.*
 Additionally the OpenAiChatProperties and OpenAiEmbeddingProperties can optionally override the OpenAiConnectionProperties
 so that we can use the Chat model from one Provider and the Embedding model from another.

 Resolves #229
This commit is contained in:
Christian Tzolov
2024-01-13 22:41:09 +01:00
parent 7b38cc3a88
commit 8fa0af553c
11 changed files with 356 additions and 216 deletions

View File

@@ -14,6 +14,15 @@ Let's make your `@Beans` intelligent!
### Breaking Changes
January 13, 2024 Update
The following OpenAi Autoconfiguration chat properties has changed
- from `spring.ai.openai.model` to `spring.ai.openai.chat.model`.
- from `spring.ai.openai.temperature` to `spring.ai.openai.chat.temperature`.
Find updated documentation about the OpenAi properties: https://docs.spring.io/spring-ai/reference/api/clients/openai.html
December 27, 2023 Update
Merge SimplePersistentVectorStore and InMemoryVectorStore into SimpleVectorStore
@@ -214,7 +223,7 @@ Following vector stores are supported:
```xml
<dependency>
<groupId>org.springframework.ai</groupId>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-neo4j-store-spring-boot-starter</artifactId>
<version>0.8.0-SNAPSHOT</version>
</dependency>

View File

@@ -143,22 +143,6 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
// The rest of the chunks with same ID share the same role.
ConcurrentHashMap<String, String> roleMap = new ConcurrentHashMap<>();
// An alternative implementation that returns Flux<Generation> instead of
// Flux<ChatResponse>.
// Flux<Generation> generationFlux = completionChunks.map(chunk -> {
// String chunkId = chunk.id();
// return chunk.choices().stream()
// .map(choice -> {
// if (choice.delta().role() != null) {
// roleMap.putIfAbsent(chunkId, choice.delta().role().name());
// }
// return new Generation(choice.delta().content(),
// Map.of("role", roleMap.get(chunkId)));
// })
// .toList();
// }).flatMapIterable(generations -> generations);
// return generationFlux;
return completionChunks.map(chunk -> {
String chunkId = chunk.id();
List<Generation> generations = chunk.choices().stream().map(choice -> {

View File

@@ -63,6 +63,12 @@ public class ChatController {
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("generation", chatClient.generate(message));
}
@GetMapping("/ai/generateStream")
public Flux<ChatResponse> generateStream(P@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
Prompt prompt = new Prompt(new UserMessage(message));
return chatClient.generateStream(prompt);
}
}
----
@@ -74,9 +80,8 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne
|====
| Property | Description | Default
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com"
| spring.ai.openai.model | The model to use | gpt-3.5-turbo
| spring.ai.openai.api-key | The API Key | -
| spring.ai.openai.base-url | The URL to connect to | https://api.openai.com
| spring.ai.openai.api-key | The API Key | -
|====
The prefix `spring.ai.openai.chat` is the property prefix that lets you configure the `ChatClient` implementation for OpenAI.
@@ -85,19 +90,26 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur
|====
| Property | Description | Default
| spring.ai.azure.openai.chat.model | This is the value of the 'Deployment Name' as presented in the Azure AI Portal | gpt-35-turbo
| spring.ai.azure.openai.chat.temperature | The sampling temperature to use that controls the apparent creativity of generated completions. Higher values will make output more random while lower values will make results more focused and deterministic. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | 0.7
| spring.ai.azure.openai.chat.top-p | An alternative to sampling with temperature called nucleus sampling. This value causes the model to consider the results of tokens with the provided probability mass. As an example, a value of 0.15 will cause only the tokens comprising the top 15% of probability mass to be considered. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | -
| spring.ai.openai.chat.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | -
| spring.ai.openai.chat.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | -
| spring.ai.openai.chat.model | This is the OpenAI Chat model to use | gpt-35-turbo
| spring.ai.openai.chat.temperature | The sampling temperature to use that controls the apparent creativity of generated completions. Higher values will make output more random while lower values will make results more focused and deterministic. It is not recommended to modify temperature and top_p for the same completions request as the interaction of these two settings is difficult to predict. | 0.7
|====
The prefix `spring.ai.openai.embedding` is property prefix that configures the `EmbeddingClient` implementation for OpenAI.
[cols="3,5,3"]
|====
| Property | Description | Default
| spring.ai.openai.embedding.base-url | The URL to connect to | https://api.openai.com"
| spring.ai.openai.embedding.base-url | Optional overrides the spring.ai.openai.base-url to provide chat specific url | -
| spring.ai.openai.embedding.api-key | Optional overrides the spring.ai.openai.api-key to provide chat specific api-key | -
| spring.ai.openai.embedding.model | The model to use | text-embedding-ada-002
| spring.ai.openai.embedding.api-key | The API Key | -
|====
|====
NOTE: You can override the common `spring.ai.openai.base-url` and `spring.ai.openai.api-key` for the `ChatClient` and `EmbeddingClient` implementations.
The `spring.ai.openai.chat.base-url` and `spring.ai.openai.chat.api-key` properties if set take precedence over the common properties.
Similarly, the `spring.ai.openai.embedding.base-url` and `spring.ai.openai.embedding.api-key` properties if set take precedence over the common properties.
This is useful if you want to use different OpenAI accounts for different models and different model endpoints.
Also by default, the `spring.ai.openai.chat.model` is set to `gpt-35-turbo` and the `spring.ai.openai.embedding.model` is set to `text-embedding-ada-002`.

View File

@@ -18,43 +18,65 @@ package org.springframework.ai.autoconfigure.openai;
import org.springframework.ai.autoconfigure.NativeHints;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
@AutoConfiguration
@ConditionalOnClass(OpenAiApi.class)
@EnableConfigurationProperties(OpenAiProperties.class)
@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class,
OpenAiEmbeddingProperties.class })
@ImportRuntimeHints(NativeHints.class)
public class OpenAiAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OpenAiApi openAiApi(OpenAiProperties openAiProperties) {
return new OpenAiApi(openAiProperties.getBaseUrl(), openAiProperties.getApiKey(), RestClient.builder());
}
public OpenAiChatClient openAiChatClient(OpenAiConnectionProperties commonProperties,
OpenAiChatProperties chatProperties) {
String apiKey = StringUtils.hasText(chatProperties.getApiKey()) ? chatProperties.getApiKey()
: commonProperties.getApiKey();
String baseUrl = StringUtils.hasText(chatProperties.getBaseUrl()) ? chatProperties.getBaseUrl()
: commonProperties.getBaseUrl();
Assert.hasText(apiKey, "OpenAI API key must be set");
Assert.hasText(baseUrl, "OpenAI base URL must be set");
var openAiApi = new OpenAiApi(baseUrl, apiKey, RestClient.builder());
@Bean
@ConditionalOnMissingBean
public OpenAiChatClient openAiChatClient(OpenAiApi openAiApi, OpenAiProperties openAiProperties) {
OpenAiChatClient openAiChatClient = new OpenAiChatClient(openAiApi);
openAiChatClient.setTemperature(openAiProperties.getTemperature());
openAiChatClient.setModel(openAiProperties.getModel());
openAiChatClient.setTemperature(chatProperties.getTemperature());
openAiChatClient.setModel(chatProperties.getModel());
return openAiChatClient;
}
@Bean
@ConditionalOnMissingBean
public EmbeddingClient openAiEmbeddingClient(OpenAiApi openAiApi, OpenAiProperties openAiProperties) {
return new OpenAiEmbeddingClient(openAiApi, openAiProperties.getEmbedding().getModel());
public EmbeddingClient openAiEmbeddingClient(OpenAiConnectionProperties commonProperties,
OpenAiEmbeddingProperties embeddingProperties) {
String apiKey = StringUtils.hasText(embeddingProperties.getApiKey()) ? embeddingProperties.getApiKey()
: commonProperties.getApiKey();
String baseUrl = StringUtils.hasText(embeddingProperties.getBaseUrl()) ? embeddingProperties.getBaseUrl()
: commonProperties.getBaseUrl();
Assert.hasText(apiKey, "OpenAI API key must be set");
Assert.hasText(baseUrl, "OpenAI base URL must be set");
var openAiApi = new OpenAiApi(baseUrl, apiKey, RestClient.builder());
return new OpenAiEmbeddingClient(openAiApi, embeddingProperties.getModel());
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(OpenAiChatProperties.CONFIG_PREFIX)
public class OpenAiChatProperties extends OpenAiParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.openai.chat";
public static final String DEFAULT_CHAT_MODEL = "gpt-3.5-turbo";
private Double temperature = 0.7;
private String model = DEFAULT_CHAT_MODEL;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public Double getTemperature() {
return temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(OpenAiConnectionProperties.CONFIG_PREFIX)
public class OpenAiConnectionProperties extends OpenAiParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.openai";
public static final String DEFAULT_BASE_URL = "https://api.openai.com";
public OpenAiConnectionProperties() {
super.setBaseUrl(DEFAULT_BASE_URL);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(OpenAiEmbeddingProperties.CONFIG_PREFIX)
public class OpenAiEmbeddingProperties extends OpenAiParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.openai.embedding";
public static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002";
private String model = DEFAULT_EMBEDDING_MODEL;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai;
/**
* Internal parent properties for the OpenAI properties.
*
* @author Christian Tzolov
* @since 0.8.0
*/
class OpenAiParentProperties {
private String apiKey;
private String baseUrl;
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}

View File

@@ -1,143 +0,0 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.openai;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@ConfigurationProperties(OpenAiProperties.CONFIG_PREFIX)
public class OpenAiProperties {
public static final String CONFIG_PREFIX = "spring.ai.openai";
private Double temperature = 0.7;
private final Embedding embedding = new Embedding(this);
private final Metadata metadata = new Metadata();
private String apiKey;
private String model = "gpt-3.5-turbo";
private String baseUrl = "https://api.openai.com";
public String getApiKey() {
return this.apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public String getBaseUrl() {
return this.baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Double getTemperature() {
return this.temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public Embedding getEmbedding() {
return this.embedding;
}
public Metadata getMetadata() {
return this.metadata;
}
public static class Embedding {
private final OpenAiProperties openAiProperties;
private String apiKey;
private String model = "text-embedding-ada-002";
private String baseUrl;
protected Embedding(OpenAiProperties openAiProperties) {
Assert.notNull(openAiProperties, "OpenAiProperties must not be null");
this.openAiProperties = openAiProperties;
}
public OpenAiProperties getOpenAiProperties() {
return openAiProperties;
}
public String getApiKey() {
return StringUtils.hasText(this.apiKey) ? this.apiKey : getOpenAiProperties().getApiKey();
}
public void setApiKey(String embeddingApiKey) {
this.apiKey = embeddingApiKey;
}
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public String getBaseUrl() {
return StringUtils.hasText(this.baseUrl) ? this.baseUrl : getOpenAiProperties().getBaseUrl();
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}
public static class Metadata {
private Boolean rateLimitMetricsEnabled;
public boolean isRateLimitMetricsEnabled() {
return Boolean.TRUE.equals(getRateLimitMetricsEnabled());
}
public Boolean getRateLimitMetricsEnabled() {
return this.rateLimitMetricsEnabled;
}
public void setRateLimitMetricsEnabled(Boolean rateLimitMetricsEnabled) {
this.rateLimitMetricsEnabled = rateLimitMetricsEnabled;
}
}
}

View File

@@ -17,15 +17,20 @@
package org.springframework.ai.autoconfigure.openai;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.prompt.Prompt;
import org.springframework.ai.prompt.messages.UserMessage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -50,6 +55,21 @@ public class OpenAiAutoConfigurationIT {
});
}
@Test
void generateStreaming() {
contextRunner.run(context -> {
OpenAiChatClient client = context.getBean(OpenAiChatClient.class);
Flux<ChatResponse> responseFlux = client.generateStream(new Prompt(new UserMessage("Hello")));
String response = responseFlux.collectList().block().stream().map(chatResponse -> {
return chatResponse.getGenerations().get(0).getContent();
}).collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void embedding() {
contextRunner.run(context -> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,53 +16,124 @@
package org.springframework.ai.autoconfigure.openai;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for {@link OpenAiProperties}.
* Unit Tests for {@link OpenAiConnectionProperties}, {@link OpenAiChatProperties} and
* {@link OpenAiEmbeddingProperties}.
*
* @author John Blum
* @since 0.7.1
* @author Christian Tzolov
* @since 0.8.0
*/
@SpringBootTest(properties = { "spring.ai.openai.api-key=abc123", "spring.ai.openai.model=claudia-shiffer-5",
"spring.ai.openai.base-url=https://api.openai.spring.io/eieioh", "spring.ai.openai.temperature=0.5",
"spring.ai.openai.duration=30s", "spring.ai.openai.embedding.base-url=https://api.openai.spring.io/embedding" })
@SuppressWarnings("unused")
class OpenAiPropertiesTests {
@Autowired
private OpenAiProperties openAiProperties;
public class OpenAiPropertiesTests {
@Test
void openAiPropertiesAreCorrect() {
public void chatProperties() {
assertThat(this.openAiProperties).isNotNull();
assertThat(this.openAiProperties.getApiKey()).isEqualTo("abc123");
assertThat(this.openAiProperties.getModel()).isEqualTo("claudia-shiffer-5");
assertThat(this.openAiProperties.getBaseUrl()).isEqualTo("https://api.openai.spring.io/eieioh");
assertThat(this.openAiProperties.getTemperature()).isEqualTo(0.5d);
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.openai.base-url=TEST_BASE_URL",
"spring.ai.openai.api-key=abc123",
"spring.ai.openai.chat.model=MODEL_XYZ",
"spring.ai.openai.chat.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(OpenAiChatProperties.class);
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
OpenAiProperties.Embedding embedding = this.openAiProperties.getEmbedding();
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(embedding).isNotNull();
assertThat(embedding.getApiKey()).isEqualTo(this.openAiProperties.getApiKey());
assertThat(embedding.getModel()).isEqualTo("text-embedding-ada-002");
assertThat(embedding.getBaseUrl()).isEqualTo("https://api.openai.spring.io/embedding");
assertThat(chatProperties.getApiKey()).isNull();
assertThat(chatProperties.getBaseUrl()).isNull();
assertThat(chatProperties.getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getTemperature()).isEqualTo(0.55);
});
}
@SpringBootConfiguration
@EnableConfigurationProperties(OpenAiProperties.class)
static class TestConfiguration {
@Test
public void chatOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.openai.base-url=TEST_BASE_URL",
"spring.ai.openai.api-key=abc123",
"spring.ai.openai.chat.base-url=TEST_BASE_URL2",
"spring.ai.openai.chat.api-key=456",
"spring.ai.openai.chat.model=MODEL_XYZ",
"spring.ai.openai.chat.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(OpenAiChatProperties.class);
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(chatProperties.getApiKey()).isEqualTo("456");
assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
assertThat(chatProperties.getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getTemperature()).isEqualTo(0.55);
});
}
@Test
public void embeddingProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.openai.base-url=TEST_BASE_URL",
"spring.ai.openai.api-key=abc123",
"spring.ai.openai.embedding.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(embeddingProperties.getApiKey()).isNull();
assertThat(embeddingProperties.getBaseUrl()).isNull();
assertThat(embeddingProperties.getModel()).isEqualTo("MODEL_XYZ");
});
}
@Test
public void embeddingOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.openai.base-url=TEST_BASE_URL",
"spring.ai.openai.api-key=abc123",
"spring.ai.openai.embedding.base-url=TEST_BASE_URL2",
"spring.ai.openai.embedding.api-key=456",
"spring.ai.openai.embedding.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
assertThat(embeddingProperties.getModel()).isEqualTo("MODEL_XYZ");
});
}
}