Add OpenAiImageClient auto-configuration

Fixes gh-289

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2024-02-02 17:42:49 +01:00
committed by Christian Tzolov
parent 6cf2f86412
commit 41a459a10b
4 changed files with 176 additions and 3 deletions

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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());
});
}
}

View File

@@ -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");
});
}
}