Add support for QianFan AI models

- Add chat, embeddin and image models
- Tests
- Docs
This commit is contained in:
GR
2024-05-22 18:08:24 +08:00
committed by Mark Pollack
parent db53166779
commit 997e01ca6b
43 changed files with 4352 additions and 2 deletions

View File

@@ -326,6 +326,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Typesense vector store -->
<dependency>
<groupId>org.springframework.ai</groupId>

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* @author Geng Rong
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(QianFanApi.class)
@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class,
QianFanEmbeddingProperties.class, QianFanImageProperties.class })
public class QianFanAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = QianFanChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties,
QianFanChatProperties chatProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler) {
var qianFanApi = qianFanApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties.getApiKey(), chatProperties.getSecretKey(),
commonProperties.getSecretKey(), restClientBuilder, responseErrorHandler);
return new QianFanChatModel(qianFanApi, chatProperties.getOptions(), retryTemplate);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = QianFanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties,
QianFanEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(),
commonProperties.getSecretKey(), restClientBuilder, responseErrorHandler);
return new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(),
embeddingProperties.getOptions(), retryTemplate);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = QianFanImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties,
QianFanImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler) {
String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
: commonProperties.getApiKey();
String secretKey = StringUtils.hasText(imageProperties.getSecretKey()) ? imageProperties.getSecretKey()
: commonProperties.getSecretKey();
String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
: commonProperties.getBaseUrl();
Assert.hasText(apiKey, "QianFan API key must be set. Use the property: spring.ai.qianfan.api-key");
Assert.hasText(secretKey, "QianFan secret key must be set. Use the property: spring.ai.qianfan.secret-key");
Assert.hasText(baseUrl, "QianFan base URL must be set. Use the property: spring.ai.qianfan.base-url");
var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey, restClientBuilder, responseErrorHandler);
return new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate);
}
private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,
String secretKey, String commonSecretKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
Assert.hasText(resolvedBaseUrl, "QianFan base URL must be set");
String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
Assert.hasText(resolvedApiKey, "QianFan API key must be set");
String resolvedSecretKey = StringUtils.hasText(secretKey) ? secretKey : commonSecretKey;
Assert.hasText(resolvedSecretKey, "QianFan Secret key must be set");
return new QianFanApi(resolvedBaseUrl, resolvedApiKey, resolvedSecretKey, restClientBuilder,
responseErrorHandler);
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
FunctionCallbackContext manager = new FunctionCallbackContext();
manager.setApplicationContext(context);
return manager;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* @author Geng Rong
*/
@ConfigurationProperties(QianFanChatProperties.CONFIG_PREFIX)
public class QianFanChatProperties extends QianFanParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.qianfan.chat";
public static final String DEFAULT_CHAT_MODEL = QianFanApi.ChatModel.ERNIE_Speed_8K.value;
private static final Double DEFAULT_TEMPERATURE = 0.7;
/**
* Enable QianFan chat client.
*/
private boolean enabled = true;
@NestedConfigurationProperty
private QianFanChatOptions options = QianFanChatOptions.builder()
.withModel(DEFAULT_CHAT_MODEL)
.withTemperature(DEFAULT_TEMPERATURE.floatValue())
.build();
public QianFanChatOptions getOptions() {
return options;
}
public void setOptions(QianFanChatOptions options) {
this.options = options;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.springframework.ai.qianfan.api.ApiUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(QianFanConnectionProperties.CONFIG_PREFIX)
public class QianFanConnectionProperties extends QianFanParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.qianfan";
public static final String DEFAULT_BASE_URL = ApiUtils.DEFAULT_BASE_URL;
public QianFanConnectionProperties() {
super.setBaseUrl(DEFAULT_BASE_URL);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* @author Geng Rong
*/
@ConfigurationProperties(QianFanEmbeddingProperties.CONFIG_PREFIX)
public class QianFanEmbeddingProperties extends QianFanParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.qianfan.embedding";
/**
* Enable QianFan embedding client.
*/
private boolean enabled = true;
private MetadataMode metadataMode = MetadataMode.EMBED;
@NestedConfigurationProperty
private QianFanEmbeddingOptions options = QianFanEmbeddingOptions.builder()
.withModel(QianFanApi.DEFAULT_EMBEDDING_MODEL)
.build();
public QianFanEmbeddingOptions getOptions() {
return this.options;
}
public void setOptions(QianFanEmbeddingOptions options) {
this.options = options;
}
public MetadataMode getMetadataMode() {
return this.metadataMode;
}
public void setMetadataMode(MetadataMode metadataMode) {
this.metadataMode = metadataMode;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* QianFan Image autoconfiguration properties.
*
* @author Geng Rong
*/
@ConfigurationProperties(QianFanImageProperties.CONFIG_PREFIX)
public class QianFanImageProperties extends QianFanParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.qianfan.image";
public static final String DEFAULT_IMAGE_MODEL = QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue();
/**
* Enable QianFan image model.
*/
private boolean enabled = true;
/**
* Options for QianFan Image API.
*/
@NestedConfigurationProperty
private QianFanImageOptions options = QianFanImageOptions.builder().withModel(DEFAULT_IMAGE_MODEL).build();
public QianFanImageOptions getOptions() {
return options;
}
public void setOptions(QianFanImageOptions options) {
this.options = options;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
/**
* @author Geng Rong
*/
class QianFanParentProperties {
private String apiKey;
private String secretKey;
private String baseUrl;
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}

View File

@@ -39,3 +39,4 @@ org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.typesense.TypesenseVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration
org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
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 org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@EnabledIfEnvironmentVariables(value = { @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(QianFanAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.apiKey=" + System.getenv("QIANFAN_API_KEY"),
"spring.ai.qianfan.secretKey=" + System.getenv("QIANFAN_SECRET_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class));
@Test
void generate() {
contextRunner.run(context -> {
QianFanChatModel client = context.getBean(QianFanChatModel.class);
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
contextRunner.run(context -> {
QianFanChatModel client = context.getBean(QianFanChatModel.class);
Flux<ChatResponse> responseFlux = client.stream(new Prompt(new UserMessage("Hello")));
String response = Objects.requireNonNull(responseFlux.collectList().block())
.stream()
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getContent())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void embedding() {
contextRunner.run(context -> {
QianFanEmbeddingModel embeddingClient = context.getBean(QianFanEmbeddingModel.class);
EmbeddingResponse embeddingResponse = embeddingClient
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
assertThat(embeddingClient.dimensions()).isEqualTo(1024);
});
}
@Test
void generateImage() {
contextRunner.withPropertyValues("spring.ai.qianfan.image.options.size=1024x1024").run(context -> {
QianFanImageModel imageModel = context.getBean(QianFanImageModel.class);
ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest"));
assertThat(imageResponse.getResults()).hasSize(1);
assertThat(imageResponse.getResult().getOutput().getUrl()).isNull();
assertThat(imageResponse.getResult().getOutput().getB64Json()).isNotEmpty();
logger.info("Generated image: " + imageResponse.getResult().getOutput().getB64Json());
});
}
}

View File

@@ -0,0 +1,438 @@
/*
* Copyright 2023 - 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.
* 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.qianfan;
import org.junit.jupiter.api.Test;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests for
* {@link org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties},
* {@link org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties} and
* {@link org.springframework.ai.autoconfigure.qianfan.QianFanEmbeddingProperties}.
*
* @author Geng Rong
*/
public class QianFanPropertiesTests {
@Test
public void chatProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.chat.options.model=MODEL_XYZ",
"spring.ai.qianfan.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(QianFanChatProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(chatProperties.getApiKey()).isNull();
assertThat(chatProperties.getBaseUrl()).isNull();
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
});
}
@Test
public void chatOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.chat.base-url=TEST_BASE_URL2",
"spring.ai.qianfan.chat.api-key=456",
"spring.ai.qianfan.chat.secret-key=def456",
"spring.ai.qianfan.chat.options.model=MODEL_XYZ",
"spring.ai.qianfan.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(QianFanChatProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(chatProperties.getApiKey()).isEqualTo("456");
assertThat(chatProperties.getSecretKey()).isEqualTo("def456");
assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
});
}
@Test
public void embeddingProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.embedding.options.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(embeddingProperties.getApiKey()).isNull();
assertThat(embeddingProperties.getBaseUrl()).isNull();
assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
});
}
@Test
public void embeddingOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.embedding.base-url=TEST_BASE_URL2",
"spring.ai.qianfan.embedding.api-key=456",
"spring.ai.qianfan.embedding.secret-key=def456",
"spring.ai.qianfan.embedding.options.model=MODEL_XYZ")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
assertThat(embeddingProperties.getSecretKey()).isEqualTo("def456");
assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
});
}
@Test
public void chatOptionsTest() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.api-key=API_KEY",
"spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.chat.options.model=MODEL_XYZ",
"spring.ai.qianfan.chat.options.frequencyPenalty=-1.5",
"spring.ai.qianfan.chat.options.logitBias.myTokenId=-5",
"spring.ai.qianfan.chat.options.maxTokens=123",
"spring.ai.qianfan.chat.options.presencePenalty=0",
"spring.ai.qianfan.chat.options.responseFormat.type=json",
"spring.ai.qianfan.chat.options.stop=boza,koza",
"spring.ai.qianfan.chat.options.temperature=0.55",
"spring.ai.qianfan.chat.options.topP=0.56"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(QianFanChatProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("bge_large_zh");
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5f);
assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
assertThat(chatProperties.getOptions().getResponseFormat())
.isEqualTo(new QianFanApi.ChatCompletionRequest.ResponseFormat("json"));
assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56f);
});
}
@Test
public void embeddingOptionsTest() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.api-key=API_KEY",
"spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.embedding.options.model=MODEL_XYZ",
"spring.ai.qianfan.embedding.options.encodingFormat=MyEncodingFormat"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
});
}
@Test
void embeddingActivation() {
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=false")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=true")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty();
});
}
@Test
void chatActivation() {
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=false")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanChatModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=true")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty();
});
}
@Test
public void imageProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.image.options.model=MODEL_XYZ",
"spring.ai.qianfan.image.options.n=3")
// @formatter:on
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var imageProperties = context.getBean(QianFanImageProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(imageProperties.getApiKey()).isNull();
assertThat(imageProperties.getBaseUrl()).isNull();
assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
});
}
@Test
public void imageOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.api-key=abc123",
"spring.ai.qianfan.secret-key=def123",
"spring.ai.qianfan.image.base-url=TEST_BASE_URL2",
"spring.ai.qianfan.image.api-key=456",
"spring.ai.qianfan.image.secret-key=def456",
"spring.ai.qianfan.image.options.model=MODEL_XYZ",
"spring.ai.qianfan.image.options.n=3")
// @formatter:on
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var imageProperties = context.getBean(QianFanImageProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(imageProperties.getApiKey()).isEqualTo("456");
assertThat(imageProperties.getSecretKey()).isEqualTo("def456");
assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
});
}
@Test
public void imageOptionsTest() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.qianfan.api-key=API_KEY",
"spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL",
"spring.ai.qianfan.image.options.n=3",
"spring.ai.qianfan.image.options.model=MODEL_XYZ",
"spring.ai.qianfan.image.options.size=1024x1024",
"spring.ai.qianfan.image.options.width=1024",
"spring.ai.qianfan.image.options.height=1024",
"spring.ai.qianfan.image.options.style=vivid",
"spring.ai.qianfan.image.options.user=userXYZ"
)
// @formatter:on
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
var imageProperties = context.getBean(QianFanImageProperties.class);
var connectionProperties = context.getBean(QianFanConnectionProperties.class);
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024");
assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024);
assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024);
assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid");
assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ");
});
}
@Test
void imageActivation() {
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=false")
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanImageModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL")
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
"spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=true")
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty();
});
}
}

View File

@@ -217,7 +217,7 @@ public class ZhiPuAiPropertiesTests {
"spring.ai.zhipuai.chat.options.topP=0.56",
// "spring.ai.zhipuai.chat.options.toolChoice.functionName=toolChoiceFunctionName",
"spring.ai.zhipuai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ZhiPuAiApi.ChatCompletionRequest.ToolChoiceBuilder.FUNCTION("toolChoiceFunctionName")),
"spring.ai.zhipuai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ZhiPuAiApi.ChatCompletionRequest.ToolChoiceBuilder.function("toolChoiceFunctionName")),
"spring.ai.zhipuai.chat.options.tools[0].function.name=myFunction1",
"spring.ai.zhipuai.chat.options.tools[0].function.description=function description",