Remove Qianfan and Moonshot model modules

Drop support for the Qianfan and Moonshot models by removing their modules from the build.

These integrations are now maintained in the community repositories:
https://github.com/spring-ai-community/qianfan
https://github.com/spring-ai-community/moonshot
This commit is contained in:
Mark Pollack
2025-04-25 14:02:23 -04:00
parent ff52859b2d
commit 98052624b1
62 changed files with 8 additions and 6785 deletions

View File

@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-moonshot</artifactId>
<packaging>jar</packaging>
<name>Spring AI Moonshot AI Auto Configuration</name>
<description>Spring AI Moonshot AI Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<!-- Spring AI dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-moonshot</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,100 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.api.MoonshotApi;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
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;
/**
* {@link AutoConfiguration Auto-configuration} for Moonshot Chat Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@EnableConfigurationProperties({ MoonshotCommonProperties.class, MoonshotChatProperties.class })
@ConditionalOnClass(MoonshotApi.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT,
matchIfMissing = true)
public class MoonshotChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties,
MoonshotChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var moonshotApi = moonshotApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var chatModel = new MoonshotChatModel(moonshotApi, chatProperties.getOptions(), functionCallbackResolver,
toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
private MoonshotApi moonshotApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
var resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
var resoledBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
Assert.hasText(resolvedApiKey, "Moonshot API key must be set");
Assert.hasText(resoledBaseUrl, "Moonshot base URL must be set");
return new MoonshotApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
}
}

View File

@@ -1,53 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.moonshot.api.MoonshotApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* Configuration properties for Moonshot chat client.
*
* @author Geng Rong
* @author Alexandros Pappas
*/
@ConfigurationProperties(MoonshotChatProperties.CONFIG_PREFIX)
public class MoonshotChatProperties extends MoonshotParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.moonshot.chat";
public static final String DEFAULT_CHAT_MODEL = MoonshotApi.ChatModel.MOONSHOT_V1_8K.getValue();
private static final Double DEFAULT_TEMPERATURE = 0.7;
@NestedConfigurationProperty
private MoonshotChatOptions options = MoonshotChatOptions.builder()
.model(DEFAULT_CHAT_MODEL)
.temperature(DEFAULT_TEMPERATURE)
.build();
public MoonshotChatOptions getOptions() {
return this.options;
}
public void setOptions(MoonshotChatOptions options) {
this.options = options;
}
}

View File

@@ -1,37 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Parent properties for Moonshot.
*
* @author Geng Rong
*/
@ConfigurationProperties(MoonshotCommonProperties.CONFIG_PREFIX)
public class MoonshotCommonProperties extends MoonshotParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.moonshot";
public static final String DEFAULT_BASE_URL = "https://api.moonshot.cn";
public MoonshotCommonProperties() {
super.setBaseUrl(DEFAULT_BASE_URL);
}
}

View File

@@ -1,46 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
/**
* Parent properties for Moonshot.
*
* @author Geng Rong
*/
public class MoonshotParentProperties {
private String apiKey;
private String baseUrl;
public String getApiKey() {
return this.apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getBaseUrl() {
return this.baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}

View File

@@ -1,16 +0,0 @@
#
# Copyright 2025-2025 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.
#
org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration

View File

@@ -1,77 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
import java.util.Objects;
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.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class MoonshotChatAutoConfigurationIT {
private static final Log logger = LogFactory.getLog(MoonshotChatAutoConfigurationIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class));
@Test
void generate() {
this.contextRunner.run(context -> {
MoonshotChatModel client = context.getBean(MoonshotChatModel.class);
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
this.contextRunner.run(context -> {
MoonshotChatModel client = context.getBean(MoonshotChatModel.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().getText())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
}

View File

@@ -1,166 +0,0 @@
/*
* 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.model.moonshot.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
public class MoonshotPropertiesTests {
@Test
public void chatProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.moonshot.api-key=abc123",
"spring.ai.moonshot.chat.options.model=MODEL_XYZ",
"spring.ai.moonshot.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.class);
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
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.55);
});
}
@Test
public void chatOverrideConnectionProperties() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.moonshot.api-key=abc123",
"spring.ai.moonshot.chat.base-url=TEST_BASE_URL2",
"spring.ai.moonshot.chat.api-key=456",
"spring.ai.moonshot.chat.options.model=MODEL_XYZ",
"spring.ai.moonshot.chat.options.temperature=0.55")
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.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.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
});
}
@Test
public void chatOptionsTest() {
new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.moonshot.api-key=API_KEY",
"spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.moonshot.chat.options.model=MODEL_XYZ",
"spring.ai.moonshot.chat.options.frequencyPenalty=-1.5",
"spring.ai.moonshot.chat.options.logitBias.myTokenId=-5",
"spring.ai.moonshot.chat.options.maxTokens=123",
"spring.ai.moonshot.chat.options.n=10",
"spring.ai.moonshot.chat.options.presencePenalty=0",
"spring.ai.moonshot.chat.options.responseFormat.type=json",
"spring.ai.moonshot.chat.options.seed=66",
"spring.ai.moonshot.chat.options.stop=boza,koza",
"spring.ai.moonshot.chat.options.temperature=0.55",
"spring.ai.moonshot.chat.options.topP=0.56",
"spring.ai.moonshot.chat.options.user=userXYZ"
)
// @formatter:on
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(MoonshotChatProperties.class);
var connectionProperties = context.getBean(MoonshotCommonProperties.class);
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
assertThat(chatProperties.getOptions().getN()).isEqualTo(10);
assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
});
}
@Test
void chatActivation() {
new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.model.chat=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();
});
new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
"spring.ai.model.chat=moonshot")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();
});
}
}

View File

@@ -1,119 +0,0 @@
/*
* 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.model.moonshot.autoconfigure.tool;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
* @author Alexandros Pappas
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class FunctionCallbackInPromptIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class));
@Test
void functionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
var promptOptions = MoonshotChatOptions.builder()
.functionCallbacks(List.of(FunctionCallback.builder()
.function("CurrentWeatherService", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build();
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
});
}
@Test
void streamingFunctionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
var promptOptions = MoonshotChatOptions.builder()
.functionCallbacks(List.of(FunctionCallback.builder()
.function("CurrentWeatherService", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build()))
.build();
Flux<ChatResponse> response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
});
}
}

View File

@@ -1,177 +0,0 @@
/*
* 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.model.moonshot.autoconfigure.tool;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
* @author Alexandros Pappas
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
class FunctionCallbackWithPlainFunctionBeanIT {
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test
void functionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
// Test weatherFunction
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
MoonshotChatOptions.builder().function("weatherFunction").build()));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
// Test weatherFunctionTwo
response = chatModel.call(new Prompt(List.of(userMessage),
MoonshotChatOptions.builder().function("weatherFunctionTwo").build()));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
});
}
@Test
void functionCallWithPortableFunctionCallingOptions() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
// Test weatherFunction
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
FunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
.function("weatherFunction")
.build();
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
logger.info("Response: {}", response);
});
}
@Test
void streamFunctionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
// Test weatherFunction
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
Flux<ChatResponse> response = chatModel.stream(new Prompt(List.of(userMessage),
MoonshotChatOptions.builder().function("weatherFunction").build()));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
// Test weatherFunctionTwo
response = chatModel.stream(new Prompt(List.of(userMessage),
MoonshotChatOptions.builder().function("weatherFunctionTwo").build()));
content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
});
}
@Configuration
static class Config {
@Bean
@Description("Get the weather in location")
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() {
return new MockWeatherService();
}
// Relies on the Request's JsonClassDescription annotation to provide the
// function description.
@Bean
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunctionTwo() {
MockWeatherService weatherService = new MockWeatherService();
return (weatherService::apply);
}
}
}

View File

@@ -1,95 +0,0 @@
/*
* 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.model.moonshot.autoconfigure.tool;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* Mock 3rd party weather service.
*
* @author Geng Rong
*/
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
@Override
public Response apply(Request request) {
double temperature = 0;
if (request.location().contains("Paris")) {
temperature = 15;
}
else if (request.location().contains("Tokyo")) {
temperature = 10;
}
else if (request.location().contains("San Francisco")) {
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
/**
* Temperature units.
*/
public enum Unit {
/**
* Celsius.
*/
C("metric"),
/**
* Fahrenheit.
*/
F("imperial");
/**
* Human readable unit name.
*/
public final String unitName;
Unit(String text) {
this.unitName = text;
}
}
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@JsonClassDescription("Weather API request")
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}
/**
* Weather Function response.
*/
public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
Unit unit) {
}
}

View File

@@ -1,126 +0,0 @@
/*
* 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.model.moonshot.autoconfigure.tool;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
* @author Alexandros Pappas
*/
@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
public class MoonshotFunctionCallbackIT {
private final Logger logger = LoggerFactory.getLogger(MoonshotFunctionCallbackIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test
void functionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
ChatResponse response = chatModel
.call(new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build()));
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
});
}
@Test
void streamFunctionCallTest() {
this.contextRunner.run(context -> {
MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
Flux<ChatResponse> response = chatModel.stream(
new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build()));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.filter(Objects::nonNull)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
assertThat(content).containsAnyOf("10.0", "10");
assertThat(content).containsAnyOf("15.0", "15");
});
}
@Configuration
static class Config {
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallback.builder()
.function("WeatherInfo", new MockWeatherService())
.description("Get the weather in location")
.inputType(MockWeatherService.Request.class)
.build();
}
}
}

View File

@@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-qianfan</artifactId>
<packaging>jar</packaging>
<name>Spring AI QianFan AI Auto Configuration</name>
<description>Spring AI QianFan AI Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<!-- Spring AI dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,103 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
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;
/**
* Chat {@link AutoConfiguration Auto-configuration} for QianFan Chat Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(QianFanApi.class)
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.QIANFAN,
matchIfMissing = true)
@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class })
public class QianFanChatAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties,
QianFanChatProperties chatProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> observationConvention) {
var qianFanApi = qianFanApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties.getApiKey(), chatProperties.getSecretKey(),
commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
responseErrorHandler);
var chatModel = new QianFanChatModel(qianFanApi, chatProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(chatModel::setObservationConvention);
return chatModel;
}
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 FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
manager.setApplicationContext(context);
return manager;
}
}

View File

@@ -1,52 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
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;
/**
* Configuration properties for QianFan chat model.
*
* @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;
@NestedConfigurationProperty
private QianFanChatOptions options = QianFanChatOptions.builder()
.model(DEFAULT_CHAT_MODEL)
.temperature(DEFAULT_TEMPERATURE)
.build();
public QianFanChatOptions getOptions() {
return this.options;
}
public void setOptions(QianFanChatOptions options) {
this.options = options;
}
}

View File

@@ -1,33 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
import org.springframework.ai.qianfan.api.QianFanConstants;
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 = QianFanConstants.DEFAULT_BASE_URL;
public QianFanConnectionProperties() {
super.setBaseUrl(DEFAULT_BASE_URL);
}
}

View File

@@ -1,93 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
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.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;
/**
* Embedding {@link AutoConfiguration Auto-configuration} for QianFan Embedding Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(QianFanApi.class)
@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.QIANFAN,
matchIfMissing = true)
@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanEmbeddingProperties.class })
public class QianFanEmbeddingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties,
QianFanEmbeddingProperties embeddingProperties,
ObjectProvider<RestClient.Builder> restClientBuilderProvider, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> observationConvention) {
var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(),
commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
responseErrorHandler);
var embeddingModel = new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(),
embeddingProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(embeddingModel::setObservationConvention);
return embeddingModel;
}
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);
}
}

View File

@@ -1,58 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
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;
/**
* Configuration properties for QianFan embedding model.
*
* @author Geng Rong
*/
@ConfigurationProperties(QianFanEmbeddingProperties.CONFIG_PREFIX)
public class QianFanEmbeddingProperties extends QianFanParentProperties {
public static final String CONFIG_PREFIX = "spring.ai.qianfan.embedding";
private MetadataMode metadataMode = MetadataMode.EMBED;
@NestedConfigurationProperty
private QianFanEmbeddingOptions options = QianFanEmbeddingOptions.builder()
.model(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;
}
}

View File

@@ -1,87 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.model.SpringAIModelProperties;
import org.springframework.ai.model.SpringAIModels;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.beans.factory.ObjectProvider;
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.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;
/**
* Image {@link AutoConfiguration Auto-configuration} for QianFan Image Model.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
@ConditionalOnClass(QianFanApi.class)
@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.QIANFAN,
matchIfMissing = true)
@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanImageProperties.class })
public class QianFanImageAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties,
QianFanImageProperties imageProperties, ObjectProvider<RestClient.Builder> restClientBuilderProvider,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ImageModelObservationConvention> observationConvention) {
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,
restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate,
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
observationConvention.ifAvailable(imageModel::setObservationConvention);
return imageModel;
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
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();
/**
* Options for QianFan Image API.
*/
@NestedConfigurationProperty
private QianFanImageOptions options = QianFanImageOptions.builder().model(DEFAULT_IMAGE_MODEL).build();
public QianFanImageOptions getOptions() {
return this.options;
}
public void setOptions(QianFanImageOptions options) {
this.options = options;
}
}

View File

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

View File

@@ -1,18 +0,0 @@
#
# Copyright 2025-2025 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.
#
org.springframework.ai.model.qianfan.autoconfigure.QianFanChatAutoConfiguration
org.springframework.ai.model.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration
org.springframework.ai.model.qianfan.autoconfigure.QianFanImageAutoConfiguration

View File

@@ -1,117 +0,0 @@
/*
* 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.model.qianfan.autoconfigure;
import java.util.List;
import java.util.Objects;
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 org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
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.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@EnabledIfEnvironmentVariables({ @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));
@Test
void generate() {
this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanChatAutoConfiguration.class)).run(context -> {
QianFanChatModel client = context.getBean(QianFanChatModel.class);
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void generateStreaming() {
this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanChatAutoConfiguration.class)).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().getText())
.collect(Collectors.joining());
assertThat(response).isNotEmpty();
logger.info("Response: " + response);
});
}
@Test
void embedding() {
this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanEmbeddingAutoConfiguration.class))
.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() {
this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanImageAutoConfiguration.class))
.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

@@ -1,436 +0,0 @@
/*
* Copyright 2023-2025 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.model.qianfan.autoconfigure;
import org.junit.jupiter.api.Test;
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.retry.autoconfigure.SpringAiRetryAutoConfiguration;
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 QianFanConnectionProperties}, {@link QianFanChatProperties} and
* {@link QianFanEmbeddingProperties}.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
*/
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, QianFanChatAutoConfiguration.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.55);
});
}
@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, QianFanChatAutoConfiguration.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.55);
});
}
@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, QianFanEmbeddingAutoConfiguration.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, QianFanEmbeddingAutoConfiguration.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, QianFanChatAutoConfiguration.class))
.run(context -> {
var chatProperties = context.getBean(QianFanChatProperties.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(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
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.55);
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
});
}
@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, QianFanEmbeddingAutoConfiguration.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.model.embedding=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isEmpty();
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, QianFanEmbeddingAutoConfiguration.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.model.chat=qianfan")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.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.model.chat=none")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanChatProperties.class)).isEmpty();
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, QianFanChatAutoConfiguration.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.model.chat=qianfan")
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.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, QianFanImageAutoConfiguration.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, QianFanImageAutoConfiguration.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, QianFanImageAutoConfiguration.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.model.image=none")
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanImageProperties.class)).isEmpty();
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, QianFanImageAutoConfiguration.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.model.chat=qianfan")
.withConfiguration(
AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class))
.run(context -> {
assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty();
});
}
}

View File

@@ -1,5 +0,0 @@
[QianFan Chat Documentation](https://docs.spring.io/spring-ai/reference/api/chat/qianfan-chat.html)
[QianFan Embedding Documentation](https://docs.spring.io/spring-ai/reference/api/embeddings/qianfan-embeddings.html)
[QianFan Image Documentation](https://docs.spring.io/spring-ai/reference/api/image/qianfan-image.html)

View File

@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-qianfan</artifactId>
<packaging>jar</packaging>
<name>Spring AI QianFan</name>
<description>Baidu QianFan support</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<properties>
</properties>
<dependencies>
<!-- production dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-retry</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- Spring Framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,309 +0,0 @@
/*
* 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.qianfan;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.DefaultUsage;
import org.springframework.ai.chat.metadata.EmptyUsage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.MessageAggregator;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletion;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionChunk;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionMessage;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionMessage.Role;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionRequest;
import org.springframework.ai.qianfan.api.QianFanConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
/**
* {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal QianFan}
* backed by {@link QianFanApi}.
*
* @author Geng Rong
* @see ChatModel
* @see StreamingChatModel
* @see QianFanApi
* @author Alexandros Pappas
* @since 1.0
*/
public class QianFanChatModel implements ChatModel, StreamingChatModel {
private static final Logger logger = LoggerFactory.getLogger(QianFanChatModel.class);
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
/**
* The retry template used to retry the QianFan API calls.
*/
public final RetryTemplate retryTemplate;
/**
* The default options used for the chat completion requests.
*/
private final QianFanChatOptions defaultOptions;
/**
* Low-level access to the QianFan API.
*/
private final QianFanApi qianFanApi;
/**
* Observation registry used for instrumentation.
*/
private final ObservationRegistry observationRegistry;
/**
* Conventions to use for generating observations.
*/
private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
/**
* Creates an instance of the QianFanChatModel.
* @param qianFanApi The QianFanApi instance to be used for interacting with the
* QianFan Chat API.
* @throws IllegalArgumentException if QianFanApi is null
*/
public QianFanChatModel(QianFanApi qianFanApi) {
this(qianFanApi, QianFanChatOptions.builder().model(QianFanApi.DEFAULT_CHAT_MODEL).temperature(0.7).build());
}
/**
* Initializes an instance of the QianFanChatModel.
* @param qianFanApi The QianFanApi instance to be used for interacting with the
* QianFan Chat API.
* @param options The QianFanChatOptions to configure the chat client.
*/
public QianFanChatModel(QianFanApi qianFanApi, QianFanChatOptions options) {
this(qianFanApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
/**
* Initializes a new instance of the QianFanChatModel.
* @param qianFanApi The QianFanApi instance to be used for interacting with the
* QianFan Chat API.
* @param options The QianFanChatOptions to configure the chat client.
* @param retryTemplate The retry template.
*/
public QianFanChatModel(QianFanApi qianFanApi, QianFanChatOptions options, RetryTemplate retryTemplate) {
this(qianFanApi, options, retryTemplate, ObservationRegistry.NOOP);
}
/**
* Initializes a new instance of the QianFanChatModel.
* @param qianFanApi The QianFanApi instance to be used for interacting with the
* QianFan Chat API.
* @param options The QianFanChatOptions to configure the chat client.
* @param retryTemplate The retry template.
* @param observationRegistry The ObservationRegistry used for instrumentation.
*/
public QianFanChatModel(QianFanApi qianFanApi, QianFanChatOptions options, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
Assert.notNull(qianFanApi, "QianFanApi must not be null");
Assert.notNull(options, "Options must not be null");
Assert.notNull(retryTemplate, "RetryTemplate must not be null");
Assert.notNull(observationRegistry, "ObservationRegistry must not be null");
this.qianFanApi = qianFanApi;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public ChatResponse call(Prompt prompt) {
ChatCompletionRequest request = createRequest(prompt, false);
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(QianFanConstants.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.build();
return ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity<ChatCompletion> completionEntity = this.retryTemplate
.execute(ctx -> this.qianFanApi.chatCompletionEntity(request));
var chatCompletion = completionEntity.getBody();
if (chatCompletion == null) {
logger.warn("No chat completion returned for prompt: {}", prompt);
return new ChatResponse(List.of());
}
// @formatter:off
Map<String, Object> metadata = Map.of(
"id", chatCompletion.id(),
"role", Role.ASSISTANT
);
// @formatter:on
var assistantMessage = new AssistantMessage(chatCompletion.result(), metadata);
List<Generation> generations = Collections.singletonList(new Generation(assistantMessage));
ChatResponse chatResponse = new ChatResponse(generations, from(chatCompletion, request.model()));
observationContext.setResponse(chatResponse);
return chatResponse;
});
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return Flux.deferContextual(contextView -> {
ChatCompletionRequest request = createRequest(prompt, true);
var completionChunks = this.qianFanApi.chatCompletionStream(request);
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(QianFanConstants.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.build();
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry);
observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
Flux<ChatResponse> chatResponse = completionChunks.map(this::toChatCompletion)
.switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> {
// @formatter:off
Map<String, Object> metadata = Map.of(
"id", chatCompletion.id(),
"role", Role.ASSISTANT
);
// @formatter:on
var assistantMessage = new AssistantMessage(chatCompletion.result(), metadata);
List<Generation> generations = Collections.singletonList(new Generation(assistantMessage));
return new ChatResponse(generations, from(chatCompletion, request.model()));
}))
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
return new MessageAggregator().aggregate(chatResponse, observationContext::setResponse);
});
}
/**
* Convert the ChatCompletionChunk into a ChatCompletion.
* @param chunk the ChatCompletionChunk to convert
* @return the ChatCompletion
*/
private ChatCompletion toChatCompletion(ChatCompletionChunk chunk) {
return new ChatCompletion(chunk.id(), chunk.object(), chunk.created(), chunk.result(), chunk.finishReason(),
chunk.usage());
}
/**
* Accessible for testing.
*/
public ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
var chatCompletionMessages = prompt.getInstructions()
.stream()
.map(m -> new ChatCompletionMessage(m.getText(),
ChatCompletionMessage.Role.valueOf(m.getMessageType().name())))
.toList();
var systemMessageList = chatCompletionMessages.stream().filter(msg -> msg.role() == Role.SYSTEM).toList();
var userMessageList = chatCompletionMessages.stream().filter(msg -> msg.role() != Role.SYSTEM).toList();
if (systemMessageList.size() > 1) {
throw new IllegalArgumentException("Only one system message is allowed in the prompt");
}
var systemMessage = systemMessageList.isEmpty() ? null : systemMessageList.get(0).content();
var request = new ChatCompletionRequest(userMessageList, systemMessage, stream);
if (this.defaultOptions != null) {
request = ModelOptionsUtils.merge(this.defaultOptions, request, ChatCompletionRequest.class);
}
if (prompt.getOptions() != null) {
var updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
QianFanChatOptions.class);
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class);
}
return request;
}
@Override
public ChatOptions getDefaultOptions() {
return QianFanChatOptions.fromOptions(this.defaultOptions);
}
private ChatOptions buildRequestOptions(QianFanApi.ChatCompletionRequest request) {
return ChatOptions.builder()
.model(request.model())
.frequencyPenalty(request.frequencyPenalty())
.maxTokens(request.maxTokens())
.presencePenalty(request.presencePenalty())
.stopSequences(request.stop())
.temperature(request.temperature())
.topP(request.topP())
.build();
}
private ChatResponseMetadata from(QianFanApi.ChatCompletion result, String model) {
Assert.notNull(result, "QianFan ChatCompletionResult must not be null");
return ChatResponseMetadata.builder()
.id(result.id() != null ? result.id() : "")
.usage(result.usage() != null ? getDefaultUsage(result.usage()) : new EmptyUsage())
.model(model)
.keyValue("created", result.created() != null ? result.created() : 0L)
.build();
}
private DefaultUsage getDefaultUsage(QianFanApi.Usage usage) {
return new DefaultUsage(usage.promptTokens(), usage.completionTokens(), usage.totalTokens(), usage);
}
public void setObservationConvention(ChatModelObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
}

View File

@@ -1,346 +0,0 @@
/*
* 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.qianfan;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
/**
* QianFanChatOptions represents the options for performing chat completion using the
* QianFan API. It provides methods to set and retrieve various options like model,
* frequency penalty, max tokens, etc.
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
* @since 1.0
* @see ChatOptions
*/
@JsonInclude(Include.NON_NULL)
public class QianFanChatOptions implements ChatOptions {
// @formatter:off
/**
* ID of the model to use.
*/
private @JsonProperty("model") String model;
/**
* Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
* frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
*/
private @JsonProperty("frequency_penalty") Double frequencyPenalty;
/**
* The maximum number of tokens to generate in the chat completion. The total length of input
* tokens and generated tokens is limited by the model's context length.
*/
private @JsonProperty("max_output_tokens") Integer maxTokens;
/**
* Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they
* appear in the text so far, increasing the model's likelihood to talk about new topics.
*/
private @JsonProperty("presence_penalty") Double presencePenalty;
/**
* An object specifying the format that the model must output. Setting to { "type":
* "json_object" } enables JSON mode, which guarantees the message the model generates is valid JSON.
*/
private @JsonProperty("response_format") QianFanApi.ChatCompletionRequest.ResponseFormat responseFormat;
/**
* Up to 4 sequences where the API will stop generating further tokens.
*/
private @JsonProperty("stop") List<String> stop;
/**
* What sampling temperature to use, between 0 and 1. Higher values like 0.8 will make the output
* more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend
* altering this or top_p but not both.
*/
private @JsonProperty("temperature") Double temperature;
/**
* An alternative to sampling with temperature, called nucleus sampling, where the model considers the
* results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10%
* probability mass are considered. We generally recommend altering this or temperature but not both.
*/
private @JsonProperty("top_p") Double topP;
// @formatter:on
public static Builder builder() {
return new Builder();
}
public static QianFanChatOptions fromOptions(QianFanChatOptions fromOptions) {
return QianFanChatOptions.builder()
.model(fromOptions.getModel())
.frequencyPenalty(fromOptions.getFrequencyPenalty())
.maxTokens(fromOptions.getMaxTokens())
.presencePenalty(fromOptions.getPresencePenalty())
.responseFormat(fromOptions.getResponseFormat())
.stop(fromOptions.getStop())
.temperature(fromOptions.getTemperature())
.topP(fromOptions.getTopP())
.build();
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
@Override
public Double getFrequencyPenalty() {
return this.frequencyPenalty;
}
public void setFrequencyPenalty(Double frequencyPenalty) {
this.frequencyPenalty = frequencyPenalty;
}
@Override
public Integer getMaxTokens() {
return this.maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
@Override
public Double getPresencePenalty() {
return this.presencePenalty;
}
public void setPresencePenalty(Double presencePenalty) {
this.presencePenalty = presencePenalty;
}
public QianFanApi.ChatCompletionRequest.ResponseFormat getResponseFormat() {
return this.responseFormat;
}
public void setResponseFormat(QianFanApi.ChatCompletionRequest.ResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
@Override
@JsonIgnore
public List<String> getStopSequences() {
return getStop();
}
@JsonIgnore
public void setStopSequences(List<String> stopSequences) {
setStop(stopSequences);
}
public List<String> getStop() {
return this.stop;
}
public void setStop(List<String> stop) {
this.stop = stop;
}
@Override
public Double getTemperature() {
return this.temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
@Override
public Double getTopP() {
return this.topP;
}
public void setTopP(Double topP) {
this.topP = topP;
}
@Override
@JsonIgnore
public Integer getTopK() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.model == null) ? 0 : this.model.hashCode());
result = prime * result + ((this.frequencyPenalty == null) ? 0 : this.frequencyPenalty.hashCode());
result = prime * result + ((this.maxTokens == null) ? 0 : this.maxTokens.hashCode());
result = prime * result + ((this.presencePenalty == null) ? 0 : this.presencePenalty.hashCode());
result = prime * result + ((this.responseFormat == null) ? 0 : this.responseFormat.hashCode());
result = prime * result + ((this.stop == null) ? 0 : this.stop.hashCode());
result = prime * result + ((this.temperature == null) ? 0 : this.temperature.hashCode());
result = prime * result + ((this.topP == null) ? 0 : this.topP.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
QianFanChatOptions other = (QianFanChatOptions) obj;
if (this.model == null) {
if (other.model != null) {
return false;
}
}
else if (!this.model.equals(other.model)) {
return false;
}
if (this.frequencyPenalty == null) {
if (other.frequencyPenalty != null) {
return false;
}
}
else if (!this.frequencyPenalty.equals(other.frequencyPenalty)) {
return false;
}
if (this.maxTokens == null) {
if (other.maxTokens != null) {
return false;
}
}
else if (!this.maxTokens.equals(other.maxTokens)) {
return false;
}
if (this.presencePenalty == null) {
if (other.presencePenalty != null) {
return false;
}
}
else if (!this.presencePenalty.equals(other.presencePenalty)) {
return false;
}
if (this.responseFormat == null) {
if (other.responseFormat != null) {
return false;
}
}
else if (!this.responseFormat.equals(other.responseFormat)) {
return false;
}
if (this.stop == null) {
if (other.stop != null) {
return false;
}
}
else if (!this.stop.equals(other.stop)) {
return false;
}
if (this.temperature == null) {
if (other.temperature != null) {
return false;
}
}
else if (!this.temperature.equals(other.temperature)) {
return false;
}
if (this.topP == null) {
if (other.topP != null) {
return false;
}
}
else if (!this.topP.equals(other.topP)) {
return false;
}
return true;
}
@Override
public QianFanChatOptions copy() {
return fromOptions(this);
}
public static class Builder {
protected QianFanChatOptions options;
public Builder() {
this.options = new QianFanChatOptions();
}
public Builder(QianFanChatOptions options) {
this.options = options;
}
public Builder model(String model) {
this.options.model = model;
return this;
}
public Builder frequencyPenalty(Double frequencyPenalty) {
this.options.frequencyPenalty = frequencyPenalty;
return this;
}
public Builder maxTokens(Integer maxTokens) {
this.options.maxTokens = maxTokens;
return this;
}
public Builder presencePenalty(Double presencePenalty) {
this.options.presencePenalty = presencePenalty;
return this;
}
public Builder responseFormat(QianFanApi.ChatCompletionRequest.ResponseFormat responseFormat) {
this.options.responseFormat = responseFormat;
return this;
}
public Builder stop(List<String> stop) {
this.options.stop = stop;
return this;
}
public Builder temperature(Double temperature) {
this.options.temperature = temperature;
return this;
}
public Builder topP(Double topP) {
this.options.topP = topP;
return this;
}
public QianFanChatOptions build() {
return this.options;
}
}
}

View File

@@ -1,222 +0,0 @@
/*
* 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.qianfan;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.metadata.DefaultUsage;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.AbstractEmbeddingModel;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanApi.EmbeddingList;
import org.springframework.ai.qianfan.api.QianFanConstants;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
/**
* QianFan Embedding Client implementation.
*
* @author Geng Rong
* @author Thomas Vitale
* @since 1.0
*/
public class QianFanEmbeddingModel extends AbstractEmbeddingModel {
private static final Logger logger = LoggerFactory.getLogger(QianFanEmbeddingModel.class);
private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();
private final QianFanEmbeddingOptions defaultOptions;
private final RetryTemplate retryTemplate;
private final QianFanApi qianFanApi;
private final MetadataMode metadataMode;
/**
* Observation registry used for instrumentation.
*/
private final ObservationRegistry observationRegistry;
/**
* Conventions to use for generating observations.
*/
private EmbeddingModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
/**
* Constructor for the QianFanEmbeddingModel class.
* @param qianFanApi The QianFanApi instance to use for making API requests.
*/
public QianFanEmbeddingModel(QianFanApi qianFanApi) {
this(qianFanApi, MetadataMode.EMBED);
}
/**
* Initializes a new instance of the QianFanEmbeddingModel class.
* @param qianFanApi The QianFanApi instance to use for making API requests.
* @param metadataMode The mode for generating metadata.
*/
public QianFanEmbeddingModel(QianFanApi qianFanApi, MetadataMode metadataMode) {
this(qianFanApi, metadataMode,
QianFanEmbeddingOptions.builder().model(QianFanApi.DEFAULT_EMBEDDING_MODEL).build());
}
/**
* Initializes a new instance of the QianFanEmbeddingModel class.
* @param qianFanApi The QianFanApi instance to use for making API requests.
* @param metadataMode The mode for generating metadata.
* @param qianFanEmbeddingOptions The options for QianFan embedding.
*/
public QianFanEmbeddingModel(QianFanApi qianFanApi, MetadataMode metadataMode,
QianFanEmbeddingOptions qianFanEmbeddingOptions) {
this(qianFanApi, metadataMode, qianFanEmbeddingOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
/**
* Initializes a new instance of the QianFanEmbeddingModel class.
* @param qianFanApi The QianFanApi instance to use for making API requests.
* @param metadataMode The mode for generating metadata.
* @param qianFanEmbeddingOptions The options for QianFan embedding.
* @param retryTemplate - The RetryTemplate for retrying failed API requests.
*/
public QianFanEmbeddingModel(QianFanApi qianFanApi, MetadataMode metadataMode,
QianFanEmbeddingOptions qianFanEmbeddingOptions, RetryTemplate retryTemplate) {
this(qianFanApi, metadataMode, qianFanEmbeddingOptions, retryTemplate, ObservationRegistry.NOOP);
}
/**
* Initializes a new instance of the QianFanEmbeddingModel class.
* @param qianFanApi - The QianFanApi instance to use for making API requests.
* @param metadataMode - The mode for generating metadata.
* @param options - The options for QianFan embedding.
* @param retryTemplate - The RetryTemplate for retrying failed API requests.
* @param observationRegistry - The ObservationRegistry used for instrumentation.
*/
public QianFanEmbeddingModel(QianFanApi qianFanApi, MetadataMode metadataMode, QianFanEmbeddingOptions options,
RetryTemplate retryTemplate, ObservationRegistry observationRegistry) {
Assert.notNull(qianFanApi, "QianFanApi must not be null");
Assert.notNull(metadataMode, "metadataMode must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.qianFanApi = qianFanApi;
this.metadataMode = metadataMode;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public float[] embed(Document document) {
Assert.notNull(document, "Document must not be null");
return this.embed(document.getFormattedContent(this.metadataMode));
}
@Override
public EmbeddingResponse call(EmbeddingRequest request) {
QianFanEmbeddingOptions requestOptions = mergeOptions(request.getOptions(), this.defaultOptions);
QianFanApi.EmbeddingRequest apiRequest = new QianFanApi.EmbeddingRequest(request.getInstructions(),
requestOptions.getModel(), requestOptions.getUser());
var observationContext = EmbeddingModelObservationContext.builder()
.embeddingRequest(request)
.provider(QianFanConstants.PROVIDER_NAME)
.requestOptions(requestOptions)
.build();
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
EmbeddingList apiEmbeddingResponse = this.retryTemplate
.execute(ctx -> this.qianFanApi.embeddings(apiRequest).getBody());
if (apiEmbeddingResponse == null) {
logger.warn("No embeddings returned for request: {}", request);
return new EmbeddingResponse(List.of());
}
if (apiEmbeddingResponse.errorNsg() != null) {
logger.error("Error message returned for request: {}", apiEmbeddingResponse.errorNsg());
throw new RuntimeException("Embedding failed: error code:" + apiEmbeddingResponse.errorCode()
+ ", message:" + apiEmbeddingResponse.errorNsg());
}
var metadata = new EmbeddingResponseMetadata(apiRequest.model(),
getDefaultUsage(apiEmbeddingResponse.usage()));
List<Embedding> embeddings = apiEmbeddingResponse.data()
.stream()
.map(e -> new Embedding(e.embedding(), e.index()))
.toList();
EmbeddingResponse embeddingResponse = new EmbeddingResponse(embeddings, metadata);
observationContext.setResponse(embeddingResponse);
return embeddingResponse;
});
}
private DefaultUsage getDefaultUsage(QianFanApi.Usage usage) {
return new DefaultUsage(usage.promptTokens(), usage.completionTokens(), usage.totalTokens(), usage);
}
/**
* Merge runtime and default {@link EmbeddingOptions} to compute the final options to
* use in the request.
*/
private QianFanEmbeddingOptions mergeOptions(@Nullable EmbeddingOptions runtimeOptions,
QianFanEmbeddingOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, EmbeddingOptions.class,
QianFanEmbeddingOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return QianFanEmbeddingOptions.builder()
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.user(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getUser(), defaultOptions.getUser()))
.build();
}
public void setObservationConvention(EmbeddingModelObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
}

View File

@@ -1,101 +0,0 @@
/*
* 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.qianfan;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.embedding.EmbeddingOptions;
/**
* This class represents the options for QianFan embedding.
*
* @author Geng Rong
* @author Thomas Vitale
* @author Ilayaperumal Gopinathan
* @since 1.0
*/
@JsonInclude(Include.NON_NULL)
public class QianFanEmbeddingOptions implements EmbeddingOptions {
// @formatter:off
/**
* ID of the model to use.
*/
private @JsonProperty("model") String model;
/**
* A unique identifier representing your end-user, which can help QianFan to
* monitor and detect abuse.
*/
private @JsonProperty("user_id") String user;
// @formatter:on
public static Builder builder() {
return new Builder();
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
public String getUser() {
return this.user;
}
public void setUser(String user) {
this.user = user;
}
@Override
@JsonIgnore
public Integer getDimensions() {
return null;
}
public static class Builder {
protected QianFanEmbeddingOptions options;
public Builder() {
this.options = new QianFanEmbeddingOptions();
}
public Builder model(String model) {
this.options.setModel(model);
return this;
}
public Builder user(String user) {
this.options.setUser(user);
return this;
}
public QianFanEmbeddingOptions build() {
return this.options;
}
}
}

View File

@@ -1,220 +0,0 @@
/*
* 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.qianfan;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.image.Image;
import org.springframework.ai.image.ImageGeneration;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationContext;
import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.qianfan.api.QianFanConstants;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
/**
* QianFanImageModel is a class that implements the ImageModel interface. It provides a
* client for calling the QianFan image generation API.
*
* @author Geng Rong
* @since 1.0
*/
public class QianFanImageModel implements ImageModel {
private static final Logger logger = LoggerFactory.getLogger(QianFanImageModel.class);
private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention();
/**
* The default options used for the image completion requests.
*/
private final QianFanImageOptions defaultOptions;
/**
* The retry template used to retry the QianFan Image API calls.
*/
private final RetryTemplate retryTemplate;
/**
* Low-level access to the QianFan Image API.
*/
private final QianFanImageApi qianFanImageApi;
/**
* Observation registry used for instrumentation.
*/
private final ObservationRegistry observationRegistry;
/**
* Conventions to use for generating observations.
*/
private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
/**
* Creates an instance of the QianFanImageModel.
* @param qianFanImageApi The QianFanImageApi instance to be used for interacting with
* the QianFan Image API.
* @throws IllegalArgumentException if qianFanImageApi is null
*/
public QianFanImageModel(QianFanImageApi qianFanImageApi) {
this(qianFanImageApi, QianFanImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
/**
* Creates an instance of the QianFanImageModel.
* @param qianFanImageApi The QianFanImageApi instance to be used for interacting with
* the QianFan Image API.
* @param options The QianFanImageOptions to configure the image model.
* @throws IllegalArgumentException if qianFanImageApi is null
*/
public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options) {
this(qianFanImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
/**
* Creates an instance of the QianFanImageModel.
* @param qianFanImageApi The QianFanImageApi instance to be used for interacting with
* the QianFan Image API.
* @param options The QianFanImageOptions to configure the image model.
* @param retryTemplate The retry template.
* @throws IllegalArgumentException if qianFanImageApi is null
*/
public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options,
RetryTemplate retryTemplate) {
this(qianFanImageApi, options, retryTemplate, ObservationRegistry.NOOP);
}
/**
* Initializes a new instance of the QianFanImageModel.
* @param qianFanImageApi The QianFanImageApi instance to be used for interacting with
* the QianFan Image API.
* @param options The QianFanImageOptions to configure the image model.
* @param retryTemplate The retry template.
* @param observationRegistry The ObservationRegistry used for instrumentation.
*/
public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
Assert.notNull(qianFanImageApi, "QianFanImageApi must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.qianFanImageApi = qianFanImageApi;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
QianFanImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions);
QianFanImageApi.QianFanImageRequest imageRequest = createRequest(imagePrompt, requestImageOptions);
var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt)
.provider(QianFanConstants.PROVIDER_NAME)
.requestOptions(requestImageOptions)
.build();
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity<QianFanImageApi.QianFanImageResponse> imageResponseEntity = this.retryTemplate
.execute(ctx -> this.qianFanImageApi.createImage(imageRequest));
ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest);
observationContext.setResponse(imageResponse);
return imageResponse;
});
}
private QianFanImageApi.QianFanImageRequest createRequest(ImagePrompt imagePrompt,
QianFanImageOptions requestImageOptions) {
String instructions = imagePrompt.getInstructions().get(0).getText();
QianFanImageApi.QianFanImageRequest imageRequest = new QianFanImageApi.QianFanImageRequest(instructions,
QianFanImageApi.DEFAULT_IMAGE_MODEL);
return ModelOptionsUtils.merge(requestImageOptions, imageRequest, QianFanImageApi.QianFanImageRequest.class);
}
private ImageResponse convertResponse(ResponseEntity<QianFanImageApi.QianFanImageResponse> imageResponseEntity,
QianFanImageApi.QianFanImageRequest qianFanImageRequest) {
QianFanImageApi.QianFanImageResponse imageApiResponse = imageResponseEntity.getBody();
if (imageApiResponse == null) {
logger.warn("No image response returned for request: {}", qianFanImageRequest);
return new ImageResponse(List.of());
}
List<ImageGeneration> imageGenerationList = imageApiResponse.data()
.stream()
.map(entry -> new ImageGeneration(new Image(null, entry.b64Image())))
.toList();
return new ImageResponse(imageGenerationList);
}
/**
* Convert the {@link ImageOptions} into {@link QianFanImageOptions}.
* @param runtimeImageOptions the image options to use.
* @param defaultOptions the default options.
* @return the converted {@link QianFanImageOptions}.
*/
private QianFanImageOptions mergeOptions(@Nullable ImageOptions runtimeImageOptions,
QianFanImageOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeImageOptions, ImageOptions.class,
QianFanImageOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return QianFanImageOptions.builder()
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.N(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getN(), defaultOptions.getN()))
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.width(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getWidth(), defaultOptions.getWidth()))
.height(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getHeight(), defaultOptions.getHeight()))
.style(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getStyle(), defaultOptions.getStyle()))
.user(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getUser(), defaultOptions.getUser()))
.build();
}
public void setObservationConvention(ImageModelObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
}

View File

@@ -1,236 +0,0 @@
/*
* 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.qianfan;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.image.ImageOptions;
/**
* QianFan Image API options. QianFanImageOptions.java
*
* @author Geng Rong
* @author Ilayaperumal Gopinathan
* @since 1.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class QianFanImageOptions implements ImageOptions {
/**
* The number of images to generate. Must be between 1 and 4.
*/
@JsonProperty("n")
private Integer n;
/**
* The model to use for image generation.
*/
@JsonProperty("model")
private String model;
/**
* The width of the generated images. Must be one of 576, 768, 1024, 1152, 1536 or
* 2048 * for sd_xl.
*/
@JsonProperty("size_width")
private Integer width;
/**
* The height of the generated images. Must be one of 576, 768, 1024, 1152, 1536 or
* 2048 for sd_xl.
*/
@JsonProperty("size_height")
private Integer height;
/**
* The size of the generated images. The default image dimensions are 1024x1024, with
* the following ranges applicable: Suitable for avatars: ["768x768", "1024x1024",
* "1536x1536", "2048x2048"] Suitable for article illustrations: ["1024x768",
* "2048x1536"] Suitable for posters and flyers: ["768x1024", "1536x2048"] Suitable
* for computer wallpapers: ["1024x576", "2048x1152"] Suitable for posters and flyers:
* ["576x1024", "1152x2048"]
*/
@JsonProperty("size")
private String size;
/**
* The style of the generated images. The default style is Base. Must be one of:
* [Base, 3D Model, Abstract, Analog Film, Anime, Cinematic, Comic Book, Craft Clay,
* Digital Art, Enhance, Fantasy Art, Isometric, Line Art, Lowpoly, Neonpunk, Origami,
* Photographic, Pixel Art, Texture]
*/
@JsonProperty("style")
private String style;
/**
* A unique identifier representing your end-user, which can help QianFan to monitor
* and detect abuse.
*/
@JsonProperty("user_id")
private String user;
public static Builder builder() {
return new Builder();
}
@Override
public Integer getN() {
return this.n;
}
public void setN(Integer n) {
this.n = n;
}
@Override
public String getModel() {
return this.model;
}
public void setModel(String model) {
this.model = model;
}
@Override
public Integer getWidth() {
return this.width;
}
public void setWidth(Integer width) {
this.width = width;
this.size = this.width + "x" + this.height;
}
@Override
public Integer getHeight() {
return this.height;
}
public void setHeight(Integer height) {
this.height = height;
this.size = this.width + "x" + this.height;
}
@Override
@JsonIgnore
public String getResponseFormat() {
return null;
}
@Override
public String getStyle() {
return this.style;
}
public void setStyle(String style) {
this.style = style;
}
public String getUser() {
return this.user;
}
public void setUser(String user) {
this.user = user;
}
public String getSize() {
if (this.size != null) {
return this.size;
}
return (this.width != null && this.height != null) ? this.width + "x" + this.height : null;
}
public void setSize(String size) {
this.size = size;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof QianFanImageOptions that)) {
return false;
}
return Objects.equals(this.n, that.n) && Objects.equals(this.model, that.model)
&& Objects.equals(this.width, that.width) && Objects.equals(this.height, that.height)
&& Objects.equals(this.size, that.size) && Objects.equals(this.style, that.style)
&& Objects.equals(this.user, that.user);
}
@Override
public int hashCode() {
return Objects.hash(this.n, this.model, this.width, this.height, this.size, this.style, this.user);
}
@Override
public String toString() {
return "QianFanImageOptions{" + "n=" + this.n + ", model='" + this.model + '\'' + ", width=" + this.width
+ ", height=" + this.height + ", size='" + this.size + '\'' + ", style='" + this.style + '\''
+ ", user='" + this.user + '\'' + '}';
}
public static final class Builder {
private final QianFanImageOptions options;
private Builder() {
this.options = new QianFanImageOptions();
}
public Builder N(Integer n) {
this.options.setN(n);
return this;
}
public Builder model(String model) {
this.options.setModel(model);
return this;
}
public Builder width(Integer width) {
this.options.setWidth(width);
return this;
}
public Builder height(Integer height) {
this.options.setHeight(height);
return this;
}
public Builder style(String style) {
this.options.setStyle(style);
return this;
}
public Builder user(String user) {
this.options.setUser(user);
return this;
}
public QianFanImageOptions build() {
return this.options;
}
}
}

View File

@@ -1,43 +0,0 @@
/*
* 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.qianfan.aot;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage;
/**
* The QianFanRuntimeHints class is responsible for registering runtime hints for QianFan
* API classes.
*
* @author Geng Rong
*/
public class QianFanRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(@NonNull RuntimeHints hints, @Nullable ClassLoader classLoader) {
var mcs = MemberCategory.values();
for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.qianfan")) {
hints.reflection().registerType(tr, mcs);
}
}
}

View File

@@ -1,573 +0,0 @@
/*
* 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.qianfan.api;
import java.util.List;
import java.util.function.Predicate;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.qianfan.api.auth.AuthApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
// @formatter:off
/**
* Single class implementation of the QianFan Chat Completion API and Embedding API.
* <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html">QianFan Docs</a>
*
* @author Geng Rong
* @author Thomas Vitale
* @since 1.0
*/
public class QianFanApi extends AuthApi {
public static final String DEFAULT_CHAT_MODEL = ChatModel.ERNIE_Speed_8K.getValue();
public static final String DEFAULT_EMBEDDING_MODEL = EmbeddingModel.BGE_LARGE_ZH.getValue();
private static final Predicate<ChatCompletionChunk> SSE_DONE_PREDICATE = ChatCompletionChunk::end;
private final RestClient restClient;
private final WebClient webClient;
/**
* Create a new chat completion api with default base URL.
*
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
*/
public QianFanApi(String apiKey, String secretKey) {
this(QianFanConstants.DEFAULT_BASE_URL, apiKey, secretKey);
}
/**
* Create a new chat completion api.
*
* @param baseUrl api base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
*/
public QianFanApi(String baseUrl, String apiKey, String secretKey) {
this(baseUrl, apiKey, secretKey, RestClient.builder());
}
/**
* Create a new chat completion api.
*
* @param baseUrl api base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
* @param restClientBuilder RestClient builder.
*/
public QianFanApi(String baseUrl, String apiKey, String secretKey, RestClient.Builder restClientBuilder) {
this(baseUrl, apiKey, secretKey, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
/**
* Create a new chat completion api.
*
* @param baseUrl api base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
* @param restClientBuilder RestClient builder.
* @param responseErrorHandler Response error handler.
*/
public QianFanApi(String baseUrl, String apiKey, String secretKey, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, secretKey, restClientBuilder, WebClient.builder(), responseErrorHandler);
}
/**
* Create a new chat completion api.
*
* @param baseUrl api base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
* @param restClientBuilder RestClient builder.
* @param webClientBuilder WebClient builder.
* @param responseErrorHandler Response error handler.
*/
public QianFanApi(String baseUrl, String apiKey, String secretKey, RestClient.Builder restClientBuilder,
WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) {
super(apiKey, secretKey);
this.restClient = restClientBuilder
.baseUrl(baseUrl)
.defaultHeaders(QianFanUtils.defaultHeaders())
.defaultStatusHandler(responseErrorHandler)
.build();
this.webClient = webClientBuilder
.baseUrl(baseUrl)
.defaultHeaders(QianFanUtils.defaultHeaders())
.build();
}
/**
* Creates a model response for the given chat conversation.
*
* @param chatRequest The chat completion request.
* @return Entity response with {@link ChatCompletion} as a body and HTTP status code and headers.
*/
public ResponseEntity<ChatCompletion> chatCompletionEntity(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(!chatRequest.stream(), "Request must set the stream property to false.");
return this.restClient.post()
.uri("/v1/wenxinworkshop/chat/{model}?access_token={token}", chatRequest.model, getAccessToken())
.body(chatRequest)
.retrieve()
.toEntity(ChatCompletion.class);
}
/**
* Creates a streaming chat response for the given chat conversation.
* @param chatRequest The chat completion request. Must have the stream property set
* to true.
* @return Returns a {@link Flux} stream from chat completion chunks.
*/
public Flux<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest chatRequest) {
Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(chatRequest.stream(), "Request must set the stream property to true.");
return this.webClient.post()
.uri("/v1/wenxinworkshop/chat/{model}?access_token={token}", chatRequest.model, getAccessToken())
.body(Mono.just(chatRequest), ChatCompletionRequest.class)
.retrieve()
.bodyToFlux(ChatCompletionChunk.class)
.takeUntil(SSE_DONE_PREDICATE);
}
/**
* Creates an embedding vector representing the input text or token array.
* @param embeddingRequest The embedding request.
* @return Returns list of {@link Embedding} wrapped in {@link EmbeddingList}.
*/
public ResponseEntity<EmbeddingList> embeddings(EmbeddingRequest embeddingRequest) {
Assert.notNull(embeddingRequest, "The request body can not be null.");
// Input text to embed, encoded as a string or array of tokens. To embed multiple
// inputs in a single
// request, pass an array of strings or array of token arrays.
Assert.notNull(embeddingRequest.texts(), "The input can not be null.");
// The input must not an empty string, and any array must be 16 dimensions or
// less.
Assert.isTrue(!CollectionUtils.isEmpty(embeddingRequest.texts()), "The input list can not be empty.");
Assert.isTrue(embeddingRequest.texts().size() <= 16, "The list must be 16 dimensions or less");
return this.restClient.post()
.uri("/v1/wenxinworkshop/embeddings/{model}?access_token={token}", embeddingRequest.model, getAccessToken())
.body(embeddingRequest)
.retrieve()
.toEntity(new ParameterizedTypeReference<>() {
});
}
/**
* QianFan Chat Completion Models:
* <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu#%E5%AF%B9%E8%AF%9Dchat">QianFan Model</a>.
*/
public enum ChatModel {
ERNIE_4_0_8K("completions_pro"),
ERNIE_4_0_8K_Preview("ernie-4.0-8k-preview"),
ERNIE_4_0_8K_Preview_0518("completions_adv_pro"),
ERNIE_4_0_8K_0329("ernie-4.0-8k-0329"),
ERNIE_4_0_8K_0104("ernie-4.0-8k-0104"),
ERNIE_3_5_8K("completions"),
ERNIE_3_5_128K("ernie-3.5-128k"),
ERNIE_3_5_8K_Preview("ernie-3.5-8k-preview"),
ERNIE_3_5_8K_0205("ernie-3.5-8k-0205"),
ERNIE_3_5_8K_0329("ernie-3.5-8k-0329"),
ERNIE_3_5_8K_1222("ernie-3.5-8k-1222"),
ERNIE_3_5_4K_0205("ernie-3.5-4k-0205"),
ERNIE_Lite_8K_0922("eb-instant"),
ERNIE_Lite_8K_0308("ernie-lite-8k"),
ERNIE_Speed_8K("ernie_speed"),
ERNIE_Speed_128K("ernie-speed-128k"),
ERNIE_Tiny_8K("ernie-tiny-8k"),
ERNIE_FUNC_8K("ernie-func-8k");
public final String value;
ChatModel(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
/**
* QianFan Embeddings Models:
* <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu#%E5%90%91%E9%87%8Fembeddings">Embeddings</a>.
*/
public enum EmbeddingModel {
/**
* DIMENSION: 384
*/
EMBEDDING_V1("embedding-v1"),
/**
* DIMENSION: 1024
*/
BGE_LARGE_ZH("bge_large_zh"),
/**
* DIMENSION: 1024
*/
BGE_LARGE_EN("bge_large_en"),
/**
* DIMENSION: 1024
*/
TAO_8K("tao_8k");
public final String value;
EmbeddingModel(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
/**
* Creates a model response for the given chat conversation.
*
* @param messages A list of messages comprising the conversation so far.
* @param system The system ID to use.
* @param model ID of the model to use.
* @param frequencyPenalty Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
* frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
* @param maxTokens The maximum number of tokens to generate in the chat completion. The total length of input
* tokens and generated tokens is limited by the model's context length.
* appear in the text so far, increasing the model's likelihood to talk about new topics.
* @param presencePenalty Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
* frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
* @param responseFormat An object specifying the format that the model must output. Setting to { "type":
* "json_object" } enables JSON mode, which guarantees the message the model generates is valid JSON.
* @param stop Up to 4 sequences where the API will stop generating further tokens.
* @param stream If set, partial message deltas will be sent.Tokens will be sent as data-only server-sent events as
* they become available, with the stream terminated by a data: [DONE] message.
* @param temperature What sampling temperature to use, between 0 and 1. Higher values like 0.8 will make the output
* more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend
* altering this or top_p but not both.
* @param topP An alternative to sampling with temperature, called nucleus sampling, where the model considers the
* results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10%
* probability mass are considered. We generally recommend altering this or temperature but not both.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionRequest(
@JsonProperty("messages") List<ChatCompletionMessage> messages,
@JsonProperty("system") String system,
@JsonProperty("model") String model,
@JsonProperty("frequency_penalty") Double frequencyPenalty,
@JsonProperty("max_output_tokens") Integer maxTokens,
@JsonProperty("presence_penalty") Double presencePenalty,
@JsonProperty("response_format") ResponseFormat responseFormat,
@JsonProperty("stop") List<String> stop,
@JsonProperty("stream") Boolean stream,
@JsonProperty("temperature") Double temperature,
@JsonProperty("top_p") Double topP) {
/**
* Shortcut constructor for a chat completion request with the given messages and model.
*
* @param messages A list of messages comprising the conversation so far.
* @param model ID of the model to use.
* @param temperature What sampling temperature to use, between 0 and 1.
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String system, String model, Double temperature) {
this(messages, system, model, null, null,
null, null, null, false, temperature, null);
}
/**
* Shortcut constructor for a chat completion request with the given messages, model and control for streaming.
*
* @param messages A list of messages comprising the conversation so far.
* @param model ID of the model to use.
* @param temperature What sampling temperature to use, between 0 and 1.
* @param stream If set, partial message deltas will be sent.Tokens will be sent as data-only server-sent events
* as they become available, with the stream terminated by a data: [DONE] message.
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String system, String model, Double temperature, boolean stream) {
this(messages, system, model, null, null,
null, null, null, stream, temperature, null);
}
/**
* Shortcut constructor for a chat completion request with the given messages, model, tools and tool choice.
* Streaming is set to false, temperature to 0.8 and all other parameters are null.
*
* @param messages A list of messages comprising the conversation so far.
* @param stream If set, partial message deltas will be sent.Tokens will be sent as data-only server-sent events
* as they become available, with the stream terminated by a data: [DONE] message.
*/
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String system, Boolean stream) {
this(messages, system, DEFAULT_CHAT_MODEL, null, null,
null, null, null, stream, 0.8, null);
}
/**
* An object specifying the format that the model must output.
* @param type Must be one of 'text' or 'json_object'.
*/
@JsonInclude(Include.NON_NULL)
public record ResponseFormat(
@JsonProperty("type") String type) {
}
}
/**
* Message comprising the conversation.
*
* @param rawContent The contents of the message. Can be a {@link String}.
* The response message content is always a {@link String}.
* @param role The role of the messages author. Could be one of the {@link Role} types.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionMessage(
@JsonProperty("content") Object rawContent,
@JsonProperty("role") Role role) {
/**
* Get message content as String.
*/
public String content() {
if (this.rawContent == null) {
return null;
}
if (this.rawContent instanceof String text) {
return text;
}
throw new IllegalStateException("The content is not a string!");
}
/**
* The role of the author of this message.
*/
public enum Role {
/**
* System message.
*/
@JsonProperty("system")
SYSTEM,
/**
* User message.
*/
@JsonProperty("user")
USER,
/**
* Assistant message.
*/
@JsonProperty("assistant")
ASSISTANT
}
}
/**
* Represents a chat completion response returned by model, based on the provided input.
*
* @param id A unique identifier for the chat completion.
* @param result Result of chat completion message.
* @param created The Unix timestamp (in seconds) of when the chat completion was created.
* used in conjunction with the seed request parameter to understand when backend changes have been made that might
* impact determinism.
* @param object The object type, which is always chat.completion.
* @param finishReason The reason the chat completion finished.
* @param usage Usage statistics for the completion request.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletion(
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("result") String result,
@JsonProperty("finish_reason") String finishReason,
@JsonProperty("usage") Usage usage) {
}
/**
* Usage statistics for the completion request.
*
* @param completionTokens Number of tokens in the completion.
* @param promptTokens Number of tokens in the prompt.
* @param totalTokens Total number of tokens used in the request (prompt + completion).
*/
@JsonInclude(Include.NON_NULL)
public record Usage(
@JsonProperty("completion_tokens") Integer completionTokens,
@JsonProperty("prompt_tokens") Integer promptTokens,
@JsonProperty("total_tokens") Integer totalTokens) {
}
/**
* Represents a streamed chunk of a chat completion response returned by model, based on the provided input.
*
* @param id A unique identifier for the chat completion. Each chunk has the same ID.
* @param object The object type, which is always 'chat.completion.chunk'.
* @param created The Unix timestamp (in seconds) of when the chat completion was created. Each chunk has the same
* timestamp.
* @param result Result of chat completion message.
* @param finishReason The reason the chat completion finished.
* @param end If true, the chat completion is finished.
* @param usage Usage statistics for the completion request.
*/
@JsonInclude(Include.NON_NULL)
public record ChatCompletionChunk(
@JsonProperty("id") String id,
@JsonProperty("object") String object,
@JsonProperty("created") Long created,
@JsonProperty("result") String result,
@JsonProperty("finish_reason") String finishReason,
@JsonProperty("is_end") Boolean end,
@JsonProperty("usage") Usage usage
) {
}
/**
* Creates an embedding vector representing the input text.
*
* @param texts Input text to embed, encoded as a string or array of tokens.
* @param model ID of the model to use.
* @param user A unique identifier representing your end-user, which can help QianFan to
* monitor and detect abuse.
*/
@JsonInclude(Include.NON_NULL)
public record EmbeddingRequest(
@JsonProperty("input") List<String> texts,
@JsonProperty("model") String model,
@JsonProperty("user_id") String user
) {
/**
* Create an embedding request with the given input.
* Embedding model is set to 'bge_large_zh'.
* @param text Input text to embed.
*/
public EmbeddingRequest(String text) {
this(List.of(text), DEFAULT_EMBEDDING_MODEL, null);
}
/**
* Create an embedding request with the given input.
* @param text Input text to embed.
* @param model ID of the model to use.
* @param userId A unique identifier representing your end-user, which can help QianFan to
* monitor and detect abuse.
*/
public EmbeddingRequest(String text, String model, String userId) {
this(List.of(text), model, userId);
}
/**
* Create an embedding request with the given input.
* Embedding model is set to 'bge_large_zh'.
* @param texts Input text to embed.
*/
public EmbeddingRequest(List<String> texts) {
this(texts, DEFAULT_EMBEDDING_MODEL, null);
}
/**
* Create an embedding request with the given input.
* @param texts Input text to embed.
* @param model ID of the model to use.
*/
public EmbeddingRequest(List<String> texts, String model) {
this(texts, model, null);
}
}
/**
* Represents an embedding vector returned by embedding endpoint.
*
* @param index The index of the embedding in the list of embeddings.
* @param embedding The embedding vector, which is a list of floats. The length of
* vector depends on the model.
* @param object The object type, which is always 'embedding'.
*/
@JsonInclude(Include.NON_NULL)
public record Embedding(
// @formatter:off
@JsonProperty("index") Integer index,
@JsonProperty("embedding") float[] embedding,
@JsonProperty("object") String object) {
// @formatter:on
/**
* Create an embedding with the given index, embedding and object type set to
* 'embedding'.
* @param index The index of the embedding in the list of embeddings.
* @param embedding The embedding vector, which is a list of floats. The length of
* vector depends on the model.
*/
public Embedding(Integer index, float[] embedding) {
this(index, embedding, "embedding");
}
}
/**
* List of multiple embedding responses.
*
* @param object Must have value "embedding_list".
* @param data List of entities.
* @param model ID of the model to use.
* @param errorCode Error code if any.
* @param errorNsg Error message if any.
* @param usage Usage statistics for the completion request.
*/
@JsonInclude(Include.NON_NULL)
public record EmbeddingList(
// @formatter:off
@JsonProperty("object") String object,
@JsonProperty("data") List<Embedding> data,
@JsonProperty("model") String model,
@JsonProperty("error_code") String errorCode,
@JsonProperty("error_msg") String errorNsg,
@JsonProperty("usage") Usage usage) {
// @formatter:on
}
}
// @formatter:on

View File

@@ -1,38 +0,0 @@
/*
* 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.qianfan.api;
import org.springframework.ai.observation.conventions.AiProvider;
/**
* The ApiUtils class provides utility methods for working with API requests and
* responses.
*
* @author Geng Rong
* @since 1.0
*/
public final class QianFanConstants {
public static final String DEFAULT_BASE_URL = "https://aip.baidubce.com/rpc/2.0/ai_custom";
public static final String PROVIDER_NAME = AiProvider.QIANFAN.value();
private QianFanConstants() {
}
}

View File

@@ -1,146 +0,0 @@
/*
* 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.qianfan.api;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.qianfan.api.auth.AuthApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
/**
* QianFan Image API.
*
* @author Geng Rong
* @since 1.0
*/
public class QianFanImageApi extends AuthApi {
public static final String DEFAULT_IMAGE_MODEL = ImageModel.Stable_Diffusion_XL.getValue();
private final RestClient restClient;
/**
* Create a new QianFan Image api with default base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
*/
public QianFanImageApi(String apiKey, String secretKey) {
this(QianFanConstants.DEFAULT_BASE_URL, apiKey, secretKey, RestClient.builder());
}
/**
* Create a new QianFan Image API with the provided base URL.
* @param baseUrl the base URL for the QianFan API.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
* @param restClientBuilder the rest client builder to use.
*/
public QianFanImageApi(String baseUrl, String apiKey, String secretKey, RestClient.Builder restClientBuilder) {
this(baseUrl, apiKey, secretKey, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
/**
* Create a new QianFan Image API with the provided base URL.
* @param baseUrl the base URL for the QianFan API.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
* @param restClientBuilder the rest client builder to use.
* @param responseErrorHandler the response error handler to use.
*/
public QianFanImageApi(String baseUrl, String apiKey, String secretKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
super(apiKey, secretKey);
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultHeaders(QianFanUtils.defaultHeaders())
.defaultStatusHandler(responseErrorHandler)
.build();
}
public ResponseEntity<QianFanImageResponse> createImage(QianFanImageRequest qianFanImageRequest) {
Assert.notNull(qianFanImageRequest, "Image request cannot be null.");
Assert.hasLength(qianFanImageRequest.prompt(), "Prompt cannot be empty.");
return this.restClient.post()
.uri("/v1/wenxinworkshop/text2image/{model}?access_token={token}", qianFanImageRequest.model(),
getAccessToken())
.body(qianFanImageRequest)
.retrieve()
.toEntity(QianFanImageResponse.class);
}
/**
* QianFan Image API model.
*/
public enum ImageModel {
/**
* Stable Diffusion XL (SDXL) is a powerful text-to-image generation model.
*/
Stable_Diffusion_XL("sd_xl");
private final String value;
ImageModel(String model) {
this.value = model;
}
public String getValue() {
return this.value;
}
}
// @formatter:off
@JsonInclude(JsonInclude.Include.NON_NULL)
public record QianFanImageRequest(
@JsonProperty("model") String model,
@JsonProperty("prompt") String prompt,
@JsonProperty("negative_prompt") String negativePrompt,
@JsonProperty("size") String size,
@JsonProperty("n") Integer n,
@JsonProperty("steps") Integer steps,
@JsonProperty("seed") Integer seed,
@JsonProperty("style") String style,
@JsonProperty("user_id") String user) {
public QianFanImageRequest(String prompt, String model) {
this(model, prompt, null, null, null, null, null, null, null);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public record QianFanImageResponse(
@JsonProperty("id") String id,
@JsonProperty("created") Long created,
@JsonProperty("data") List<Data> data) {
}
// @formatter:onn
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Data(@JsonProperty("index") Integer index, @JsonProperty("b64_image") String b64Image) {
}
}

View File

@@ -1,34 +0,0 @@
/*
* 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.qianfan.api;
import java.util.function.Consumer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
public final class QianFanUtils {
public static Consumer<HttpHeaders> defaultHeaders() {
return headers -> headers.setContentType(MediaType.APPLICATION_JSON);
}
private QianFanUtils() {
}
}

View File

@@ -1,40 +0,0 @@
/*
* 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.qianfan.api.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Represents the response received when requesting an access token.
*
* @param accessToken the access token
* @param refreshToken the refresh token
* @param expiresIn the number of seconds until the token expires
* @param sessionKey the session key
* @param sessionSecret the session secret
* @param error the error code
* @param errorDescription the error description
* @param scope the scope
* @author Geng Rong
* @since 1.0
*/
public record AccessTokenResponse(@JsonProperty("access_token") String accessToken,
@JsonProperty("refresh_token") String refreshToken, @JsonProperty("expires_in") Long expiresIn,
@JsonProperty("session_key") String sessionKey, @JsonProperty("session_secret") String sessionSecret,
@JsonProperty("error") String error, @JsonProperty("error_description") String errorDescription, String scope) {
}

View File

@@ -1,47 +0,0 @@
/*
* 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.qianfan.api.auth;
/**
* QianFan abstract authentication API.
*
* @author Geng Rong
* @since 1.0
*/
public abstract class AuthApi {
private final QianFanAuthenticator authenticator;
private QianFanAccessToken token;
/**
* Create a new chat completion api with default base URL.
* @param apiKey QianFan api key.
* @param secretKey QianFan secret key.
*/
protected AuthApi(String apiKey, String secretKey) {
this.authenticator = QianFanAuthenticator.builder().apiKey(apiKey).secretKey(secretKey).build();
}
protected String getAccessToken() {
if (this.token == null || this.token.needsRefresh()) {
this.token = this.authenticator.requestToken();
}
return this.token.getAccessToken();
}
}

View File

@@ -1,89 +0,0 @@
/*
* 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.qianfan.api.auth;
/**
* Represents an access token for the QianFan API.
*
* @author Geng Rong
* @since 1.0
*/
public class QianFanAccessToken {
private static final Double FRACTION_OF_TIME_TO_LIVE = 0.8D;
private final String accessToken;
private final String refreshToken;
private final Long expiresIn;
private final String sessionKey;
private final String sessionSecret;
private final String scope;
private final Long refreshTime;
public QianFanAccessToken(AccessTokenResponse accessTokenResponse) {
this.accessToken = accessTokenResponse.accessToken();
this.refreshToken = accessTokenResponse.refreshToken();
this.expiresIn = accessTokenResponse.expiresIn();
this.sessionKey = accessTokenResponse.sessionKey();
this.sessionSecret = accessTokenResponse.sessionSecret();
this.scope = accessTokenResponse.scope();
this.refreshTime = getCurrentTimeInSeconds() + (long) ((double) this.expiresIn * FRACTION_OF_TIME_TO_LIVE);
}
public String getAccessToken() {
return this.accessToken;
}
public String getRefreshToken() {
return this.refreshToken;
}
public Long getExpiresIn() {
return this.expiresIn;
}
public String getSessionKey() {
return this.sessionKey;
}
public String getSessionSecret() {
return this.sessionSecret;
}
public Long getRefreshTime() {
return this.refreshTime;
}
public String getScope() {
return this.scope;
}
public synchronized boolean needsRefresh() {
return getCurrentTimeInSeconds() >= this.refreshTime;
}
private long getCurrentTimeInSeconds() {
return System.currentTimeMillis() / 1000L;
}
}

View File

@@ -1,91 +0,0 @@
/*
* 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.qianfan.api.auth;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;
/**
* QianFanAuthenticator is a class that authenticates and requests access token for the
* QianFan API.
*
* @author Geng Rong
* @since 1.0
*/
public class QianFanAuthenticator {
private static final String DEFAULT_AUTH_URL = "https://aip.baidubce.com";
private static final String OPERATION_PATH = "/oauth/2.0/token?client_id={clientId}&client_secret={clientSecret}&grant_type=client_credentials";
private final RestClient restClient;
private final String apiKey;
private final String secretKey;
public QianFanAuthenticator(String authUrl, String apiKey, String secretKey) {
this.apiKey = apiKey;
this.secretKey = secretKey;
this.restClient = RestClient.builder().baseUrl(authUrl).build();
}
public static Builder builder() {
return new Builder();
}
public QianFanAccessToken requestToken() {
ResponseEntity<AccessTokenResponse> tokenResponseEntity = this.restClient.get()
.uri(OPERATION_PATH, this.apiKey, this.secretKey)
.retrieve()
.toEntity(AccessTokenResponse.class);
AccessTokenResponse tokenResponse = tokenResponseEntity.getBody();
if (tokenResponse == null) {
throw new IllegalArgumentException("Failed to get access token, response is null");
}
if (tokenResponse.error() != null) {
throw new IllegalArgumentException("Failed to get access token, error: " + tokenResponse.error()
+ ", error_description: " + tokenResponse.errorDescription());
}
return new QianFanAccessToken(tokenResponse);
}
public static class Builder {
private String apiKey;
private String secretKey;
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public Builder secretKey(String secretKey) {
this.secretKey = secretKey;
return this;
}
public QianFanAuthenticator build() {
return new QianFanAuthenticator(DEFAULT_AUTH_URL, this.apiKey, this.secretKey);
}
}
}

View File

@@ -1,2 +0,0 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.ai.qianfan.aot.QianFanRuntimeHints

View File

@@ -1,55 +0,0 @@
/*
* 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.qianfan;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.qianfan.api.QianFanApi;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
public class ChatCompletionRequestTests {
@Test
public void createRequestWithChatOptions() {
var client = new QianFanChatModel(new QianFanApi("TEST", "TEST"),
QianFanChatOptions.builder().model("DEFAULT_MODEL").temperature(66.6).build());
var request = client.createRequest(new Prompt("Test message content"), false);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isFalse();
assertThat(request.model()).isEqualTo("DEFAULT_MODEL");
assertThat(request.temperature()).isEqualTo(66.6);
request = client.createRequest(new Prompt("Test message content",
QianFanChatOptions.builder().model("PROMPT_MODEL").temperature(99.9).build()), true);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isTrue();
assertThat(request.model()).isEqualTo("PROMPT_MODEL");
assertThat(request.temperature()).isEqualTo(99.9);
}
}

View File

@@ -1,76 +0,0 @@
/*
* 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.qianfan;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
/**
* @author Geng Rong
*/
@SpringBootConfiguration
public class QianFanTestConfiguration {
@Bean
public QianFanApi qianFanApi() {
return new QianFanApi(getApiKey(), getSecretKey());
}
@Bean
public QianFanImageApi qianFanImageApi() {
return new QianFanImageApi(getApiKey(), getSecretKey());
}
private String getApiKey() {
String apiKey = System.getenv("QIANFAN_API_KEY");
if (!StringUtils.hasText(apiKey)) {
throw new IllegalArgumentException(
"You must provide an API key. Put it in an environment variable under the name QIANFAN_API_KEY");
}
return apiKey;
}
private String getSecretKey() {
String apiKey = System.getenv("QIANFAN_SECRET_KEY");
if (!StringUtils.hasText(apiKey)) {
throw new IllegalArgumentException(
"You must provide a secret key. Put it in an environment variable under the name QIANFAN_SECRET_KEY");
}
return apiKey;
}
@Bean
public QianFanChatModel qianFanChatModel(QianFanApi api) {
return new QianFanChatModel(api);
}
@Bean
public EmbeddingModel qianFanEmbeddingModel(QianFanApi api) {
return new QianFanEmbeddingModel(api);
}
@Bean
public ImageModel qianFanImageModel(QianFanImageApi api) {
return new QianFanImageModel(api);
}
}

View File

@@ -1,84 +0,0 @@
/*
* 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.qianfan.api;
import java.util.List;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.stringtemplate.v4.ST;
import reactor.core.publisher.Flux;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletion;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionChunk;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionMessage;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionMessage.Role;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionRequest;
import org.springframework.ai.qianfan.api.QianFanApi.EmbeddingList;
import org.springframework.ai.util.ResourceUtils;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanApiIT {
QianFanApi qianFanApi = new QianFanApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
@Test
void chatCompletionEntity() {
ChatCompletionMessage chatCompletionMessage = new ChatCompletionMessage("Hello world", Role.USER);
ResponseEntity<ChatCompletion> response = this.qianFanApi.chatCompletionEntity(new ChatCompletionRequest(
List.of(chatCompletionMessage), buildSystemMessage(), "ernie_speed", 0.7, false));
assertThat(response).isNotNull();
assertThat(response.getBody()).isNotNull();
}
@Test
void chatCompletionStream() {
ChatCompletionMessage chatCompletionMessage = new ChatCompletionMessage("Hello world", Role.USER);
Flux<ChatCompletionChunk> response = this.qianFanApi.chatCompletionStream(new ChatCompletionRequest(
List.of(chatCompletionMessage), buildSystemMessage(), "ernie_speed", 0.7, true));
assertThat(response).isNotNull();
assertThat(response.collectList().block()).isNotNull();
}
@Test
void embeddings() {
ResponseEntity<EmbeddingList> response = this.qianFanApi
.embeddings(new QianFanApi.EmbeddingRequest("Hello world"));
assertThat(response).isNotNull();
assertThat(Objects.requireNonNull(response.getBody()).data()).hasSize(1);
assertThat(response.getBody().data().get(0).embedding()).hasSize(1024);
}
private String buildSystemMessage() {
String systemMessageTemplate = ResourceUtils.getText("classpath:/prompts/system-message.st");
ST st = new ST(systemMessageTemplate, '{', '}');
return st.add("name", "QianFan").add("voice", "pirate").render();
}
}

View File

@@ -1,219 +0,0 @@
/*
* 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.qianfan.api;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.image.ImageMessage;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletion;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionChunk;
import org.springframework.ai.qianfan.api.QianFanApi.ChatCompletionRequest;
import org.springframework.ai.qianfan.api.QianFanApi.EmbeddingList;
import org.springframework.ai.qianfan.api.QianFanApi.EmbeddingRequest;
import org.springframework.ai.qianfan.api.QianFanApi.Usage;
import org.springframework.ai.qianfan.api.QianFanImageApi.Data;
import org.springframework.ai.qianfan.api.QianFanImageApi.QianFanImageRequest;
import org.springframework.ai.qianfan.api.QianFanImageApi.QianFanImageResponse;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.retry.TransientAiException;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.BDDMockito.given;
/**
* @author Geng Rong
*/
@ExtendWith(MockitoExtension.class)
public class QianFanRetryTests {
private TestRetryListener retryListener;
private @Mock QianFanApi qianFanApi;
private @Mock QianFanImageApi qianFanImageApi;
private QianFanChatModel chatClient;
private QianFanEmbeddingModel embeddingClient;
private QianFanImageModel imageModel;
@BeforeEach
public void beforeEach() {
RetryTemplate retryTemplate = RetryUtils.SHORT_RETRY_TEMPLATE;
this.retryListener = new TestRetryListener();
retryTemplate.registerListener(this.retryListener);
this.chatClient = new QianFanChatModel(this.qianFanApi, QianFanChatOptions.builder().build(), retryTemplate);
this.embeddingClient = new QianFanEmbeddingModel(this.qianFanApi, MetadataMode.EMBED,
QianFanEmbeddingOptions.builder().build(), retryTemplate);
this.imageModel = new QianFanImageModel(this.qianFanImageApi, QianFanImageOptions.builder().build(),
retryTemplate);
}
@Test
public void qianFanChatTransientError() {
ChatCompletion expectedChatCompletion = new ChatCompletion("id", "chat.completion", 666L, "Response", "STOP",
new Usage(10, 10, 10));
given(this.qianFanApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedChatCompletion)));
var result = this.chatClient.call(new Prompt("text"));
assertThat(result).isNotNull();
assertThat(result.getResult().getOutput().getText()).isSameAs("Response");
assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(2);
assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2);
}
@Test
public void qianFanChatNonTransientError() {
given(this.qianFanApi.chatCompletionEntity(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatClient.call(new Prompt("text")));
}
@Test
@Disabled("Currently stream() does not implmement retry")
public void qianFanChatStreamTransientError() {
ChatCompletionChunk expectedChatCompletion = new ChatCompletionChunk("id", "chat.completion", 666L, "Response",
"", true, null);
given(this.qianFanApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(Flux.just(expectedChatCompletion));
var result = this.chatClient.stream(new Prompt("text"));
assertThat(result).isNotNull();
assertThat(Objects.requireNonNull(result.collectList().block()).get(0).getResult().getOutput().getText())
.isSameAs("Response");
assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(2);
assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2);
}
@Test
public void qianFanChatStreamNonTransientError() {
given(this.qianFanApi.chatCompletionStream(isA(ChatCompletionRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.chatClient.stream(new Prompt("text")).collectList().block());
}
@Test
public void qianFanEmbeddingTransientError() {
QianFanApi.Embedding embedding = new QianFanApi.Embedding(1, new float[] { 9.9f, 8.8f });
EmbeddingList expectedEmbeddings = new EmbeddingList("embedding_list", List.of(embedding), "model", null, null,
new Usage(10, 10, 10));
given(this.qianFanApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedEmbeddings)));
var result = this.embeddingClient
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null));
assertThat(result).isNotNull();
assertThat(result.getResult().getOutput()).isEqualTo(new float[] { 9.9f, 8.8f });
assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(2);
assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2);
}
@Test
public void qianFanEmbeddingNonTransientError() {
given(this.qianFanApi.embeddings(isA(EmbeddingRequest.class)))
.willThrow(new RuntimeException("Non Transient Error"));
assertThrows(RuntimeException.class, () -> this.embeddingClient
.call(new org.springframework.ai.embedding.EmbeddingRequest(List.of("text1", "text2"), null)));
}
@Test
public void qianFanImageTransientError() {
var expectedResponse = new QianFanImageResponse("1", 678L, List.of(new Data(1, "b64")));
given(this.qianFanImageApi.createImage(isA(QianFanImageRequest.class)))
.willThrow(new TransientAiException("Transient Error 1"))
.willThrow(new TransientAiException("Transient Error 2"))
.willReturn(ResponseEntity.of(Optional.of(expectedResponse)));
var result = this.imageModel.call(new ImagePrompt(List.of(new ImageMessage("Image Message"))));
assertThat(result).isNotNull();
assertThat(result.getResult().getOutput().getB64Json()).isEqualTo("b64");
assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(2);
assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2);
}
@Test
public void qianFanImageNonTransientError() {
given(this.qianFanImageApi.createImage(isA(QianFanImageRequest.class)))
.willThrow(new RuntimeException("Transient Error 1"));
assertThrows(RuntimeException.class,
() -> this.imageModel.call(new ImagePrompt(List.of(new ImageMessage("Image Message")))));
}
private static class TestRetryListener implements RetryListener {
int onErrorRetryCount = 0;
int onSuccessRetryCount = 0;
@Override
public <T, E extends Throwable> void onSuccess(RetryContext context, RetryCallback<T, E> callback, T result) {
this.onSuccessRetryCount = context.getRetryCount();
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
this.onErrorRetryCount = context.getRetryCount();
}
}
}

View File

@@ -1,96 +0,0 @@
/*
* 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.qianfan.chat;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.qianfan.QianFanTestConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanTestConfiguration.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
class QianFanChatModelIT {
@Autowired
protected ChatModel chatModel;
@Autowired
protected StreamingChatModel streamingChatModel;
@Value("classpath:/prompts/system-message.st")
private Resource systemResource;
@Test
void roleTest() {
UserMessage userMessage = new UserMessage(
"Tell me about three famous pirates from the Golden Age of Piracy in english, focusing on their original nicknames and what they did.");
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
ChatResponse response = this.chatModel.call(prompt);
assertThat(response.getResults()).hasSize(1);
assertThat(response.getResults().get(0).getOutput().getText()).contains("Blackbeard");
}
@Test
void streamRoleTest() {
UserMessage userMessage = new UserMessage(
"Tell me about three famous pirates from the Golden Age of Piracy in english, focusing on their original nicknames and what they did.");
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(this.systemResource);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", "Bob", "voice", "pirate"));
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
Flux<ChatResponse> flux = this.streamingChatModel.stream(prompt);
List<ChatResponse> responses = flux.collectList().block();
assertThat(responses.size()).isGreaterThan(1);
String stitchedResponseContent = responses.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getText)
.collect(Collectors.joining());
assertThat(stitchedResponseContent).contains("Blackbeard");
}
}

View File

@@ -1,179 +0,0 @@
/*
* 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.qianfan.chat;
import java.util.List;
import java.util.stream.Collectors;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.chat.observation.ChatModelObservationDocumentation.HighCardinalityKeyNames;
import static org.springframework.ai.chat.observation.ChatModelObservationDocumentation.LowCardinalityKeyNames;
/**
* Integration tests for observation instrumentation in {@link QianFanChatModel}.
*
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanChatModelObservationIT.Config.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanChatModelObservationIT {
@Autowired
TestObservationRegistry observationRegistry;
@Autowired
QianFanChatModel chatModel;
@BeforeEach
void beforeEach() {
this.observationRegistry.clear();
}
@Test
void observationForChatOperation() {
var options = QianFanChatOptions.builder()
.model(QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.frequencyPenalty(0.0)
.maxTokens(2048)
.presencePenalty(0.0)
.stop(List.of("this-is-the-end"))
.temperature(0.7)
.topP(1.0)
.build();
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
ChatResponse chatResponse = this.chatModel.call(prompt);
assertThat(chatResponse.getResult().getOutput().getText()).isNotEmpty();
ChatResponseMetadata responseMetadata = chatResponse.getMetadata();
assertThat(responseMetadata).isNotNull();
validate(responseMetadata);
}
@Test
void observationForStreamingChatOperation() {
var options = QianFanChatOptions.builder()
.model(QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.frequencyPenalty(0.0)
.maxTokens(2048)
.presencePenalty(0.0)
.stop(List.of("this-is-the-end"))
.temperature(0.7)
.topP(1.0)
.build();
Prompt prompt = new Prompt("Why does a raven look like a desk?", options);
Flux<ChatResponse> chatResponseFlux = this.chatModel.stream(prompt);
List<ChatResponse> responses = chatResponseFlux.collectList().block();
assertThat(responses).isNotEmpty();
String aggregatedResponse = responses.subList(0, responses.size() - 1)
.stream()
.map(r -> r.getResult().getOutput().getText())
.collect(Collectors.joining());
assertThat(aggregatedResponse).isNotEmpty();
ChatResponse lastChatResponse = responses.get(responses.size() - 1);
ChatResponseMetadata responseMetadata = lastChatResponse.getMetadata();
assertThat(responseMetadata).isNotNull();
validate(responseMetadata);
}
private void validate(ChatResponseMetadata responseMetadata) {
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultChatModelObservationConvention.DEFAULT_NAME)
.that()
.hasContextualNameEqualTo("chat " + QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
AiOperationType.CHAT.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.QIANFAN.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel())
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_FREQUENCY_PENALTY.asString(), "0.0")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_MAX_TOKENS.asString(), "2048")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_PRESENCE_PENALTY.asString(), "0.0")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(),
"[\"this-is-the-end\"]")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TEMPERATURE.asString(), "0.7")
.doesNotHaveHighCardinalityKeyValueWithKey(HighCardinalityKeyNames.REQUEST_TOP_K.asString())
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_TOP_P.asString(), "1.0")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.RESPONSE_ID.asString(), responseMetadata.getId())
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(),
String.valueOf(responseMetadata.getUsage().getPromptTokens()))
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_OUTPUT_TOKENS.asString(),
String.valueOf(responseMetadata.getUsage().getCompletionTokens()))
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(),
String.valueOf(responseMetadata.getUsage().getTotalTokens()))
.hasBeenStarted()
.hasBeenStopped();
}
@SpringBootConfiguration
static class Config {
@Bean
public TestObservationRegistry observationRegistry() {
return TestObservationRegistry.create();
}
@Bean
public QianFanApi qianFanApi() {
return new QianFanApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
}
@Bean
public QianFanChatModel qianFanChatModel(QianFanApi qianFanApi, TestObservationRegistry observationRegistry) {
return new QianFanChatModel(qianFanApi, QianFanChatOptions.builder().build(),
RetryTemplate.defaultInstance(), observationRegistry);
}
}
}

View File

@@ -1,76 +0,0 @@
/*
* 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.qianfan.embedding;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.qianfan.QianFanTestConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanTestConfiguration.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
class EmbeddingIT {
@Autowired
private EmbeddingModel embeddingModel;
@Test
void defaultEmbedding() {
Assertions.assertThat(this.embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of("Hello World"));
assertThat(embeddingResponse.getResults()).hasSize(1);
assertThat(embeddingResponse.getResults().get(0)).isNotNull();
assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1024);
Assertions.assertThat(this.embeddingModel.dimensions()).isEqualTo(1024);
}
@Test
void batchEmbedding() {
Assertions.assertThat(this.embeddingModel).isNotNull();
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of("Hello World", "HI"));
assertThat(embeddingResponse.getResults()).hasSize(2);
assertThat(embeddingResponse.getResults().get(0)).isNotNull();
assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1024);
assertThat(embeddingResponse.getResults().get(1)).isNotNull();
assertThat(embeddingResponse.getResults().get(1).getOutput()).hasSize(1024);
Assertions.assertThat(this.embeddingModel.dimensions()).isEqualTo(1024);
}
}

View File

@@ -1,118 +0,0 @@
/*
* 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.qianfan.embedding;
import java.util.List;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObservationConvention;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation.HighCardinalityKeyNames;
import static org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation.LowCardinalityKeyNames;
/**
* Integration tests for observation instrumentation in {@link QianFanEmbeddingModel}.
*
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanEmbeddingModelObservationIT.Config.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanEmbeddingModelObservationIT {
@Autowired
TestObservationRegistry observationRegistry;
@Autowired
QianFanEmbeddingModel embeddingModel;
@Test
void observationForEmbeddingOperation() {
var options = QianFanEmbeddingOptions.builder()
.model(QianFanApi.EmbeddingModel.BGE_LARGE_ZH.getValue())
.build();
EmbeddingRequest embeddingRequest = new EmbeddingRequest(List.of("Here comes the sun"), options);
EmbeddingResponse embeddingResponse = this.embeddingModel.call(embeddingRequest);
assertThat(embeddingResponse.getResults()).isNotEmpty();
EmbeddingResponseMetadata responseMetadata = embeddingResponse.getMetadata();
assertThat(responseMetadata).isNotNull();
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultEmbeddingModelObservationConvention.DEFAULT_NAME)
.that()
.hasContextualNameEqualTo("embedding " + QianFanApi.EmbeddingModel.BGE_LARGE_ZH.getValue())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
AiOperationType.EMBEDDING.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.QIANFAN.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
QianFanApi.EmbeddingModel.BGE_LARGE_ZH.getValue())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.RESPONSE_MODEL.asString(), responseMetadata.getModel())
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_INPUT_TOKENS.asString(),
String.valueOf(responseMetadata.getUsage().getPromptTokens()))
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.USAGE_TOTAL_TOKENS.asString(),
String.valueOf(responseMetadata.getUsage().getTotalTokens()))
.hasBeenStarted()
.hasBeenStopped();
}
@SpringBootConfiguration
static class Config {
@Bean
public TestObservationRegistry observationRegistry() {
return TestObservationRegistry.create();
}
@Bean
public QianFanApi qianFanApi() {
return new QianFanApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
}
@Bean
public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanApi qianFanApi,
TestObservationRegistry observationRegistry) {
return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, QianFanEmbeddingOptions.builder().build(),
RetryTemplate.defaultInstance(), observationRegistry);
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* 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.qianfan.image;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.springframework.ai.image.Image;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptionsBuilder;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.ai.qianfan.QianFanTestConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanTestConfiguration.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanImageModelIT {
@Autowired
protected ImageModel imageModel;
@Test
void imageTest() {
var options = ImageOptionsBuilder.builder().height(1024).width(1024).build();
var instructions = """
A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!".""";
ImagePrompt imagePrompt = new ImagePrompt(instructions, options);
ImageResponse imageResponse = this.imageModel.call(imagePrompt);
assertThat(imageResponse.getResults()).hasSize(1);
ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata();
assertThat(imageResponseMetadata.getCreated()).isPositive();
var generation = imageResponse.getResult();
Image image = generation.getOutput();
assertThat(image.getUrl()).isNull();
assertThat(image.getB64Json()).isNotEmpty();
}
}

View File

@@ -1,113 +0,0 @@
/*
* 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.qianfan.image;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
import org.springframework.ai.observation.conventions.AiOperationType;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.image.observation.ImageModelObservationDocumentation.HighCardinalityKeyNames;
import static org.springframework.ai.image.observation.ImageModelObservationDocumentation.LowCardinalityKeyNames;
/**
* Integration tests for observation instrumentation in {@link QianFanImageModel}.
*
* @author Geng Rong
*/
@SpringBootTest(classes = QianFanImageModelObservationIT.Config.class)
@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
@EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
public class QianFanImageModelObservationIT {
@Autowired
TestObservationRegistry observationRegistry;
@Autowired
QianFanImageModel imageModel;
@Test
void observationForImageOperation() {
var options = QianFanImageOptions.builder()
.model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())
.height(1024)
.width(1024)
.style("Base")
.build();
var instructions = "Here comes the sun";
ImagePrompt imagePrompt = new ImagePrompt(instructions, options);
ImageResponse imageResponse = this.imageModel.call(imagePrompt);
assertThat(imageResponse.getResults()).hasSize(1);
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation()
.hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME)
.that()
.hasContextualNameEqualTo("image " + QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
AiOperationType.IMAGE.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.QIANFAN.value())
.hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue())
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), "1024x1024")
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString(), "Base")
.hasBeenStarted()
.hasBeenStopped();
}
@SpringBootConfiguration
static class Config {
@Bean
public TestObservationRegistry observationRegistry() {
return TestObservationRegistry.create();
}
@Bean
public QianFanImageApi qianFanImageApi() {
return new QianFanImageApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
}
@Bean
public QianFanImageModel qianFanImageModel(QianFanImageApi qianFanImageApi,
TestObservationRegistry observationRegistry) {
return new QianFanImageModel(qianFanImageApi, QianFanImageOptions.builder().build(),
RetryTemplate.defaultInstance(), observationRegistry);
}
}
}

View File

@@ -1,3 +0,0 @@
You are an AI assistant that helps people find information.
Your name is {name}.
You should reply to the user's request with your name and also in the style of a {voice}.

View File

@@ -71,7 +71,6 @@
<module>auto-configurations/models/spring-ai-autoconfigure-model-ollama</module>
<module>auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding</module>
<module>auto-configurations/models/spring-ai-autoconfigure-model-qianfan</module>
<module>auto-configurations/models/spring-ai-autoconfigure-model-stability-ai</module>
<module>auto-configurations/models/spring-ai-autoconfigure-model-transformers</module>
<module>auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai</module>
@@ -168,7 +167,6 @@
<module>models/spring-ai-ollama</module>
<module>models/spring-ai-openai</module>
<module>models/spring-ai-postgresml</module>
<module>models/spring-ai-qianfan</module>
<module>models/spring-ai-stability-ai</module>
<module>models/spring-ai-transformers</module>
<module>models/spring-ai-vertex-ai-embedding</module>
@@ -190,7 +188,6 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-ollama</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-openai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-postgresml-embedding</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-qianfan</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-stability-ai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-transformers</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-embedding</module>

View File

@@ -554,11 +554,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-qianfan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
@@ -908,11 +903,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-qianfan</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>

View File

@@ -71,15 +71,7 @@ public enum AiProvider {
*/
MINIMAX("minimax"),
/**
* AI system provided by Moonshot.
*/
MOONSHOT("moonshot"),
/**
* AI system provided by Qianfan.
*/
QIANFAN("qianfan"),
/**
* AI system provided by Zhipuai.

View File

@@ -1,273 +1,5 @@
= QianFan Chat
Spring AI supports the various AI language models from QianFan. You can interact with QianFan language models and create a multilingual conversational assistant based on QianFan models.
This functionality has been moved to the Spring AI Community repository.
== Prerequisites
You will need to create an API with QianFan to access QianFan language models.
Create an account at https://login.bce.baidu.com/new-reg[QianFan registration page] and generate the token on the https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
The Spring AI project defines a configuration property named `spring.ai.qianfan.api-key` and `spring.ai.qianfan.secret-key`.
you should set to the value of the `API Key` and `Secret Key` obtained from https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
Exporting an environment variable is one way to set that configuration property:
[source,shell]
----
export SPRING_AI_QIANFAN_API_KEY=<INSERT API KEY HERE>
export SPRING_AI_QIANFAN_SECRET_KEY=<INSERT SECRET KEY HERE>
----
=== Add Repositories and BOM
Spring AI artifacts are published in Maven Central and Spring Snapshot repositories.
Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system.
To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system.
== Auto-configuration
[NOTE]
====
There has been a significant change in the Spring AI auto-configuration, starter modules' artifact names.
Please refer to the https://docs.spring.io/spring-ai/reference/upgrade-notes.html[upgrade notes] for more information.
====
Spring AI provides Spring Boot auto-configuration for the QianFan Chat Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-qianfan</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-qianfan'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== Chat Properties
==== Retry Properties
The prefix `spring.ai.retry` is used as the property prefix that lets you configure the retry mechanism for the QianFan Chat client.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.retry.max-attempts | Maximum number of retry attempts. | 10
| spring.ai.retry.backoff.initial-interval | Initial sleep duration for the exponential backoff policy. | 2 sec.
| spring.ai.retry.backoff.multiplier | Backoff interval multiplier. | 5
| spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min.
| spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false
| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty
| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty
|====
==== Connection Properties
The prefix `spring.ai.qianfan` is used as the property prefix that lets you connect to QianFan.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.qianfan.base-url | The URL to connect to | https://api.qianfan.chat
| spring.ai.qianfan.api-key | The API Key | -
| spring.ai.qianfan.secret-key | The Secret Key | -
|====
==== Configuration Properties
[NOTE]
====
Enabling and disabling of the chat auto-configurations are now configured via top level properties with the prefix `spring.ai.model.chat`.
To enable, spring.ai.model.chat=qianfan (It is enabled by default)
To disable, spring.ai.model.chat=none (or any value which doesn't match qianfan)
This change is done to allow configuration of multiple models.
====
The prefix `spring.ai.qianfan.chat` is the property prefix that lets you configure the chat client implementation for QianFan.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.qianfan.chat.enabled (Removed and no longer valid) | Enable QianFan chat client. | true
| spring.ai.model.chat | Enable QianFan chat client. | qianfan
| spring.ai.qianfan.chat.base-url | Optional overrides the spring.ai.qianfan.base-url to provide chat specific url | https://api.qianfan.chat
| spring.ai.qianfan.chat.api-key | Optional overrides the spring.ai.qianfan.api-key to provide chat specific api-key | -
| spring.ai.qianfan.chat.secret-key | Optional overrides the spring.ai.qianfan.secret-key to provide chat specific secret-key | -
| spring.ai.qianfan.chat.options.model | This is the QianFan Chat model to use | `abab5.5-chat` (the `abab5.5s-chat`, `abab5.5-chat`, and `abab6-chat` point to the latest model versions)
| spring.ai.qianfan.chat.options.maxTokens | The maximum number of tokens to generate in the chat completion. The total length of input tokens and generated tokens is limited by the model's context length. | -
| spring.ai.qianfan.chat.options.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.qianfan.chat.options.topP | An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. | 1.0
| spring.ai.qianfan.chat.options.presencePenalty | Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics. | 0.0f
| spring.ai.qianfan.chat.options.frequencyPenalty | Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim. | 0.0f
| spring.ai.qianfan.chat.options.stop | The model will stop generating characters specified by stop, and currently only supports a single stop word in the format of ["stop_word1"] | -
|====
NOTE: You can override the common `spring.ai.qianfan.base-url`, `spring.ai.qianfan.api-key` and `spring.ai.qianfan.secret-key` for the `ChatClient` implementations.
The `spring.ai.qianfan.chat.base-url`, `spring.ai.qianfan.chat.api-key` and `spring.ai.qianfan.chat.secret-key` properties if set take precedence over the common properties.
This is useful if you want to use different QianFan accounts for different models and different model endpoints.
TIP: All properties prefixed with `spring.ai.qianfan.chat.options` can be overridden at runtime by adding a request specific <<chat-options>> to the `Prompt` call.
== Runtime Options [[chat-options]]
The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java[QianFanChatOptions.java] provides model configurations, such as the model to use, the temperature, the frequency penalty, etc.
On start-up, the default options can be configured with the `QianFanChatModel(api, options)` constructor or the `spring.ai.qianfan.chat.options.*` properties.
At run-time you can override the default options by adding new, request specific, options to the `Prompt` call.
For example to override the default model and temperature for a specific request:
[source,java]
----
ChatResponse response = chatClient.call(
new Prompt(
"Generate the names of 5 famous pirates.",
QianFanChatOptions.builder()
.model(QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.temperature(0.5)
.build()
));
----
TIP: In addition to the model specific link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java[QianFanChatOptions] you can use a portable https://github.com/spring-projects/spring-ai/blob/main/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/ChatOptions.java[ChatOptions] instance, created with the https://github.com/spring-projects/spring-ai/blob/main/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/ChatOptionsBuilder.java[ChatOptionsBuilder#builder()].
== Sample Controller
https://start.spring.io/[Create] a new Spring Boot project and add the `spring-ai-starter-model-qianfan` to your pom (or gradle) dependencies.
Add a `application.properties` file, under the `src/main/resources` directory, to enable and configure the QianFan Chat client:
[source,application.properties]
----
spring.ai.qianfan.api-key=YOUR_API_KEY
spring.ai.qianfan.secret-key=YOUR_SECRET_KEY
spring.ai.qianfan.chat.options.model=ernie_speed
spring.ai.qianfan.chat.options.temperature=0.7
----
TIP: replace the `api-key` and `secret-key` with your QianFan credentials.
This will create a `QianFanChatModel` implementation that you can inject into your class.
Here is an example of a simple `@Controller` class that uses the chat client for text generations.
[source,java]
----
@RestController
public class ChatController {
private final QianFanChatModel chatClient;
@Autowired
public ChatController(QianFanChatModel chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/ai/generate")
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("generation", this.chatClient.call(message));
}
@GetMapping("/ai/generateStream")
public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
var prompt = new Prompt(new UserMessage(message));
return this.chatClient.stream(prompt);
}
}
----
== Manual Configuration
The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatModel.java[QianFanChatModel] implements the `ChatClient` and `StreamingChatClient` and uses the <<low-level-api>> to connect to the QianFan service.
Add the `spring-ai-qianfan` dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-qianfan'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
Next, create a `QianFanChatModel` and use it for text generations:
[source,java]
----
var qianFanApi = new QianFanApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
var chatClient = new QianFanChatModel(this.qianFanApi, QianFanChatOptions.builder()
.model(QianFanApi.ChatModel.ERNIE_Speed_8K.getValue())
.temperature(0.4)
.maxTokens(200)
.build());
ChatResponse response = this.chatClient.call(
new Prompt("Generate the names of 5 famous pirates."));
// Or with streaming responses
Flux<ChatResponse> streamResponse = this.chatClient.stream(
new Prompt("Generate the names of 5 famous pirates."));
----
The `QianFanChatOptions` provides the configuration information for the chat requests.
The `QianFanChatOptions.Builder` is fluent options builder.
=== Low-level QianFanApi Client [[low-level-api]]
The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/api/QianFanApi.java[QianFanApi] provides is lightweight Java client for link:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2[QianFan API].
Here is a simple snippet how to use the api programmatically:
[source,java]
----
String systemMessage = "Your name is QianWen";
QianFanApi qianFanApi =
new QianFanApi(System.getenv("QIANFAN_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
ChatCompletionMessage chatCompletionMessage =
new ChatCompletionMessage("Hello world", Role.USER);
// Sync request
ResponseEntity<ChatCompletion> response = this.qianFanApi.chatCompletionEntity(
new ChatCompletionRequest(List.of(this.chatCompletionMessage), this.systemMessage, QianFanApi.ChatModel.ERNIE_Speed_8K.getValue(), 0.7, false));
// Streaming request
Flux<ChatCompletionChunk> streamResponse = this.qianFanApi.chatCompletionStream(
new ChatCompletionRequest(List.of(this.chatCompletionMessage), this.systemMessage, QianFanApi.ChatModel.ERNIE_Speed_8K.getValue(), 0.7, true));
----
Follow the https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/api/QianFanApi.java[QianFanApi.java]'s JavaDoc for further information.
==== QianFanApi Samples
* The link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/test/java/org/springframework/ai/qianfan/api/QianFanApiIT.java[QianFanApiIT.java] test provides some general examples how to use the lightweight library.
Please visit https://github.com/spring-ai-community/qianfan for the latest version.

View File

@@ -1,220 +1,5 @@
= QianFan Chat
Spring AI supports the various AI language models from QianFan. You can interact with QianFan language models and create a multilingual conversational assistant based on QianFan models.
== Prerequisites
You will need to create an API with QianFan to access QianFan language models.
Create an account at https://login.bce.baidu.com/new-reg[QianFan registration page] and generate the token on the https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
The Spring AI project defines a configuration property named `spring.ai.qianfan.api-key` and `spring.ai.qianfan.secret-key`.
you should set to the value of the `API Key` and `Secret Key` obtained from https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
Exporting an environment variable is one way to set that configuration property:
[source,shell]
----
export SPRING_AI_QIANFAN_API_KEY=<INSERT API KEY HERE>
export SPRING_AI_QIANFAN_SECRET_KEY=<INSERT SECRET KEY HERE>
----
=== Add Repositories and BOM
Spring AI artifacts are published in Maven Central and Spring Snapshot repositories.
Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system.
To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system.
== Auto-configuration
[NOTE]
====
There has been a significant change in the Spring AI auto-configuration, starter modules' artifact names.
Please refer to the https://docs.spring.io/spring-ai/reference/upgrade-notes.html[upgrade notes] for more information.
====
Spring AI provides Spring Boot auto-configuration for the Azure QianFan Embedding Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-qianfan</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-qianfan'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== Embedding Properties
==== Retry Properties
The prefix `spring.ai.retry` is used as the property prefix that lets you configure the retry mechanism for the QianFan Embedding client.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.retry.max-attempts | Maximum number of retry attempts. | 10
| spring.ai.retry.backoff.initial-interval | Initial sleep duration for the exponential backoff policy. | 2 sec.
| spring.ai.retry.backoff.multiplier | Backoff interval multiplier. | 5
| spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min.
| spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false
| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty
| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty
|====
==== Connection Properties
The prefix `spring.ai.qianfan` is used as the property prefix that lets you connect to QianFan.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.qianfan.base-url | The URL to connect to | https://aip.baidubce.com/rpc/2.0/ai_custom
| spring.ai.qianfan.api-key | The API Key | -
| spring.ai.qianfan.secret-key | The Secret Key | -
|====
==== Configuration Properties
[NOTE]
====
Enabling and disabling of the embedding auto-configurations are now configured via top level properties with the prefix `spring.ai.model.embedding`.
To enable, spring.ai.model.embedding=qianfan (It is enabled by default)
To disable, spring.ai.model.embedding=none (or any value which doesn't match qianfan)
This change is done to allow configuration of multiple models.
====
The prefix `spring.ai.qianfan.embedding` is property prefix that configures the `EmbeddingClient` implementation for QianFan.
[cols="3,5,1", stripes=even]
|====
| Property | Description | Default
| spring.ai.qianfan.embedding.enabled (Removed and no longer valid) | Enable QianFan embedding client. | true
| spring.ai.model.embedding | Enable QianFan embedding client. | qianfan
| spring.ai.qianfan.embedding.base-url | Optional overrides the spring.ai.qianfan.base-url to provide embedding specific url | -
| spring.ai.qianfan.embedding.api-key | Optional overrides the spring.ai.qianfan.api-key to provide embedding specific api-key | -
| spring.ai.qianfan.embedding.secret-key | Optional overrides the spring.ai.qianfan.secret-key to provide embedding specific secret-key | -
| spring.ai.qianfan.embedding.options.model | The model to use | bge_large_zh
|====
NOTE: You can override the common `spring.ai.qianfan.base-url`, `spring.ai.qianfan.api-key` and `spring.ai.qianfan.secret-key` for the `ChatModel` and `EmbeddingModel` implementations.
The `spring.ai.qianfan.chat.base-url`, `spring.ai.qianfan.chat.api-key` and `spring.ai.qianfan.chat.secret-key` properties if set take precedence over the common properties.
Similarly, the `spring.ai.qianfan.chat.base-url`, `spring.ai.qianfan.chat.api-key` and `spring.ai.qianfan.chat.secret-key` properties if set take precedence over the common properties.
This is useful if you want to use different QianFan accounts for different models and different model endpoints.
TIP: All properties prefixed with `spring.ai.qianfan.embedding.options` can be overridden at runtime by adding a request specific <<embedding-options>> to the `EmbeddingRequest` call.
== Runtime Options [[embedding-options]]
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanEmbeddingOptions.java[QianFanEmbeddingOptions.java] provides the QianFan configurations, such as the model to use and etc.
The default options can be configured using the `spring.ai.qianfan.embedding.options` properties as well.
At start-time use the `QianFanEmbeddingModel` constructor to set the default options used for all embedding requests.
At run-time you can override the default options, using a `QianFanEmbeddingOptions` instance as part of your `EmbeddingRequest`.
For example to override the default model name for a specific request:
[source,java]
----
EmbeddingResponse embeddingResponse = embeddingClient.call(
new EmbeddingRequest(List.of("Hello World", "World is big and salvation is near"),
QianFanEmbeddingOptions.builder()
.model("Different-Embedding-Model-Deployment-Name")
.build()));
----
== Sample Controller
This will create a `EmbeddingClient` implementation that you can inject into your class.
Here is an example of a simple `@Controller` class that uses the `EmbeddingClient` implementation.
[source,application.properties]
----
spring.ai.qianfan.api-key=YOUR_API_KEY
spring.ai.qianfan.secret-key=YOUR_SECRET_KEY
spring.ai.qianfan.embedding.options.model=tao_8k
----
[source,java]
----
@RestController
public class EmbeddingController {
private final EmbeddingClient embeddingClient;
@Autowired
public EmbeddingController(EmbeddingClient embeddingClient) {
this.embeddingClient = embeddingClient;
}
@GetMapping("/ai/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
EmbeddingResponse embeddingResponse = this.embeddingClient.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
}
----
== Manual Configuration
If you are not using Spring Boot, you can manually configure the QianFan Embedding Client.
For this add the `spring-ai-qianfan` dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-qianfan'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
NOTE: The `spring-ai-qianfan` dependency provides access also to the `QianFanChatModel`.
For more information about the `QianFanChatModel` refer to the link:../chat/qianfan-chat.html[QianFan Chat Client] section.
Next, create an `QianFanEmbeddingModel` instance and use it to compute the similarity between two input texts:
[source,java]
----
var qianFanApi = new QianFanApi(System.getenv("MINIMAX_API_KEY"), System.getenv("QIANFAN_SECRET_KEY"));
var embeddingClient = new QianFanEmbeddingModel(api, MetadataMode.EMBED, QianFanEmbeddingOptions.builder()
.model("bge_large_en")
.build());
EmbeddingResponse embeddingResponse = this.embeddingClient
.embedForResponse(List.of("Hello World", "World is big and salvation is near"));
----
The `QianFanEmbeddingOptions` provides the configuration information for the embedding requests.
The options class offers a `builder()` for easy options creation.
= QianFan Embeddings
This functionality has been moved to the Spring AI Community repository.
Please visit https://github.com/spring-ai-community/qianfan for the latest version.

View File

@@ -1,138 +1,5 @@
= QianFan Image Generation
= QianFan Image
This functionality has been moved to the Spring AI Community repository.
Spring AI supports CogView, the Image generation model from QianFan.
== Prerequisites
You will need to create an API with QianFan to access QianFan language models.
Create an account at https://login.bce.baidu.com/new-reg[QianFan registration page] and generate the token on the https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
The Spring AI project defines a configuration property named `spring.ai.qianfan.api-key` and `spring.ai.qianfan.secret-key`.
you should set to the value of the `API Key` and `Secret Key` obtained from https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application[API Keys page].
Exporting an environment variable is one way to set that configuration property:
[source,shell]
----
export SPRING_AI_QIANFAN_API_KEY=<INSERT API KEY HERE>
export SPRING_AI_QIANFAN_SECRET_KEY=<INSERT SECRET KEY HERE>
----
=== Add Repositories and BOM
Spring AI artifacts are published in Maven Central and Spring Snapshot repositories.
Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system.
To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system.
== Auto-configuration
[NOTE]
====
There has been a significant change in the Spring AI auto-configuration, starter modules' artifact names.
Please refer to the https://docs.spring.io/spring-ai/reference/upgrade-notes.html[upgrade notes] for more information.
====
Spring AI provides Spring Boot auto-configuration for the QianFan Chat Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-qianfan</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-qianfan'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== Image Generation Properties
[NOTE]
====
Enabling and disabling of the image auto-configurations are now configured via top level properties with the prefix `spring.ai.model.image`.
To enable, spring.ai.model.image=qianfan (It is enabled by default)
To disable, spring.ai.model.image=none (or any value which doesn't match qianfan)
This change is done to allow configuration of multiple models.
====
The prefix `spring.ai.qianfan.image` is the property prefix that lets you configure the `ImageModel` implementation for QianFan.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.qianfan.image.enabled (Removed and no longer valid) | Enable QianFan image model. | true
| spring.ai.model.image | Enable QianFan image model. | qianfan
| spring.ai.qianfan.image.base-url | Optional overrides the spring.ai.qianfan.base-url to provide chat specific url | -
| spring.ai.qianfan.image.api-key | Optional overrides the spring.ai.qianfan.api-key to provide chat specific api-key | -
| spring.ai.qianfan.image.secret-key | Optional overrides the spring.ai.qianfan.secret-key to provide chat specific secret-key | -
| spring.ai.qianfan.image.options.model | The model to use for image generation. | sd_xl
| spring.ai.qianfan.image.options.user | A unique identifier representing your end-user, which can help QianFan to monitor and detect abuse. | -
|====
==== Connection Properties
The prefix `spring.ai.qianfan` is used as the property prefix that lets you connect to QianFan.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.qianfan.base-url | The URL to connect to | https://aip.baidubce.com/rpc/2.0/ai_custom
| spring.ai.qianfan.api-key | The API Key | -
| spring.ai.qianfan.secret-key | The Secret Key | -
|====
==== Configuration Properties
==== Retry Properties
The prefix `spring.ai.retry` is used as the property prefix that lets you configure the retry mechanism for the QianFan Image client.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.retry.max-attempts | Maximum number of retry attempts. | 10
| spring.ai.retry.backoff.initial-interval | Initial sleep duration for the exponential backoff policy. | 2 sec.
| spring.ai.retry.backoff.multiplier | Backoff interval multiplier. | 5
| spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min.
| spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false
| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty
| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty
|====
== Runtime Options [[image-options]]
The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageOptions.java[QianFanImageOptions.java] provides model configurations, such as the model to use, the quality, the size, etc.
On start-up, the default options can be configured with the `QianFanImageModel(QianFanImageApi qianFanImageApi)` constructor and the `withDefaultOptions(QianFanImageOptions defaultOptions)` method. Alternatively, use the `spring.ai.qianfan.image.options.*` properties described previously.
At runtime you can override the default options by adding new, request specific, options to the `ImagePrompt` call.
For example to override the QianFan specific options such as quality and the number of images to create, use the following code example:
[source,java]
----
ImageResponse response = qianFanImageModel.call(
new ImagePrompt("A light cream colored mini golden doodle",
QianFanImageOptions.builder()
.N(4)
.height(1024)
.width(1024).build())
);
----
TIP: In addition to the model specific https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageOptions.java[QianFanImageOptions] you can use a portable https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/image/ImageOptions.java[ImageOptions] instance, created with the https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/image/ImageOptionsBuilder.java[ImageOptionsBuilder#builder()].
Please visit https://github.com/spring-ai-community/qianfan for the latest version.

View File

@@ -38,8 +38,6 @@ public final class SpringAIModels {
public static final String MISTRAL = "mistral";
public static final String MOONSHOT = "moonshot";
public static final String OCI_GENAI = "oci-genai";
public static final String OLLAMA = "ollama";
@@ -48,8 +46,6 @@ public final class SpringAIModels {
public static final String POSTGRESML = "postgresml";
public static final String QIANFAN = "qianfan";
public static final String STABILITY_AI = "stabilityai";
public static final String TRANSFORMERS = "transformers";

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-starter-model-moonshot</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starter - Moonshot</name>
<description>Spring AI Moonshot Spring Boot Starter</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-moonshot</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-moonshot</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-starter-model-qianfan</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starter - QianFan</name>
<description>Spring AI QianFan Spring Boot Starter</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-qianfan</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>