Add OpenAiImageClient auto-configuration
Fixes gh-289 Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
committed by
Christian Tzolov
parent
6cf2f86412
commit
41a459a10b
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -20,7 +20,9 @@ import org.springframework.ai.autoconfigure.NativeHints;
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingClient;
|
||||
import org.springframework.ai.openai.OpenAiImageClient;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -34,7 +36,7 @@ import org.springframework.web.client.RestClient;
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(OpenAiApi.class)
|
||||
@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class,
|
||||
OpenAiEmbeddingProperties.class })
|
||||
OpenAiEmbeddingProperties.class, OpenAiImageProperties.class })
|
||||
@ImportRuntimeHints(NativeHints.class)
|
||||
public class OpenAiAutoConfiguration {
|
||||
|
||||
@@ -78,4 +80,22 @@ public class OpenAiAutoConfiguration {
|
||||
return new OpenAiEmbeddingClient(openAiApi).withDefaultOptions(embeddingProperties.getOptions());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OpenAiImageClient openAiImageClient(OpenAiConnectionProperties commonProperties,
|
||||
OpenAiImageProperties imageProperties) {
|
||||
String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
|
||||
: commonProperties.getApiKey();
|
||||
|
||||
String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
|
||||
: commonProperties.getBaseUrl();
|
||||
|
||||
Assert.hasText(apiKey, "OpenAI API key must be set");
|
||||
Assert.hasText(baseUrl, "OpenAI base URL must be set");
|
||||
|
||||
var openAiImageApi = new OpenAiImageApi(baseUrl, apiKey, RestClient.builder());
|
||||
|
||||
return new OpenAiImageClient(openAiImageApi).withDefaultOptions(imageProperties.getOptions());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.autoconfigure.openai;
|
||||
|
||||
import org.springframework.ai.openai.OpenAiImageOptions;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
|
||||
/**
|
||||
* OpenAI Image autoconfiguration properties.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @since 0.8.0
|
||||
*/
|
||||
@ConfigurationProperties(OpenAiImageProperties.CONFIG_PREFIX)
|
||||
public class OpenAiImageProperties extends OpenAiParentProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.openai.image";
|
||||
|
||||
/**
|
||||
* Options for OpenAI Image API.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private OpenAiImageOptions options = OpenAiImageOptions.builder().build();
|
||||
|
||||
public OpenAiImageOptions getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public void setOptions(OpenAiImageOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,6 +25,9 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.image.ImagePrompt;
|
||||
import org.springframework.ai.image.ImageResponse;
|
||||
import org.springframework.ai.openai.OpenAiImageClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
@@ -86,4 +89,15 @@ public class OpenAiAutoConfigurationIT {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateImage() {
|
||||
contextRunner.withPropertyValues("spring.ai.openai.image.options.size=256x256").run(context -> {
|
||||
OpenAiImageClient client = context.getBean(OpenAiImageClient.class);
|
||||
ImageResponse imageResponse = client.call(new ImagePrompt("forest"));
|
||||
assertThat(imageResponse.getResults()).hasSize(1);
|
||||
assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty();
|
||||
logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* {@link OpenAiEmbeddingProperties}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public class OpenAiPropertiesTests {
|
||||
@@ -141,6 +142,58 @@ public class OpenAiPropertiesTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void imageProperties() {
|
||||
new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.openai.base-url=TEST_BASE_URL",
|
||||
"spring.ai.openai.api-key=abc123",
|
||||
"spring.ai.openai.image.options.model=MODEL_XYZ",
|
||||
"spring.ai.openai.image.options.n=3")
|
||||
// @formatter:on
|
||||
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
var imageProperties = context.getBean(OpenAiImageProperties.class);
|
||||
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
|
||||
|
||||
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
|
||||
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.openai.base-url=TEST_BASE_URL",
|
||||
"spring.ai.openai.api-key=abc123",
|
||||
"spring.ai.openai.image.base-url=TEST_BASE_URL2",
|
||||
"spring.ai.openai.image.api-key=456",
|
||||
"spring.ai.openai.image.options.model=MODEL_XYZ",
|
||||
"spring.ai.openai.image.options.n=3")
|
||||
// @formatter:on
|
||||
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
var imageProperties = context.getBean(OpenAiImageProperties.class);
|
||||
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
|
||||
|
||||
assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
|
||||
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
|
||||
|
||||
assertThat(imageProperties.getApiKey()).isEqualTo("456");
|
||||
assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
|
||||
|
||||
assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
|
||||
assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chatOptionsTest() {
|
||||
|
||||
@@ -256,4 +309,42 @@ public class OpenAiPropertiesTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void imageOptionsTest() {
|
||||
new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.openai.api-key=API_KEY",
|
||||
"spring.ai.openai.base-url=TEST_BASE_URL",
|
||||
|
||||
"spring.ai.openai.image.options.n=3",
|
||||
"spring.ai.openai.image.options.model=MODEL_XYZ",
|
||||
"spring.ai.openai.image.options.quality=hd",
|
||||
"spring.ai.openai.image.options.response_format=url",
|
||||
"spring.ai.openai.image.options.size=1024x1024",
|
||||
"spring.ai.openai.image.options.width=1024",
|
||||
"spring.ai.openai.image.options.height=1024",
|
||||
"spring.ai.openai.image.options.style=vivid",
|
||||
"spring.ai.openai.image.options.user=userXYZ"
|
||||
)
|
||||
// @formatter:on
|
||||
.withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
|
||||
.run(context -> {
|
||||
var imageProperties = context.getBean(OpenAiImageProperties.class);
|
||||
var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
|
||||
|
||||
assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
|
||||
assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
|
||||
|
||||
assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
|
||||
assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
|
||||
assertThat(imageProperties.getOptions().getQuality()).isEqualTo("hd");
|
||||
assertThat(imageProperties.getOptions().getResponseFormat()).isEqualTo("url");
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user