Skip to content

Commit

Permalink
Support OpenAI in Spring Cloud Azure (#35551)
Browse files Browse the repository at this point in the history
* add openai
  • Loading branch information
Netyyyy authored Jun 27, 2023
1 parent cf5c9fa commit ae3e7c8
Show file tree
Hide file tree
Showing 19 changed files with 927 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,7 @@
<Class name="com.azure.spring.cloud.core.implementation.credential.resolver.AzureNamedKeyCredentialResolver"/>
<Class name="com.azure.spring.cloud.core.implementation.credential.resolver.AzureSasCredentialResolver"/>
<Class name="com.azure.spring.cloud.service.implementation.storage.credential.StorageSharedKeyCredentialResolver"/>
<Class name="com.azure.spring.cloud.service.implementation.openai.credential.NonAzureOpenAIKeyCredentialResolver"/>
</Or>
<Method name="resolve"/>
</And>
Expand Down
1 change: 1 addition & 0 deletions eng/versioning/version_client.txt
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ com.azure.spring:spring-cloud-azure-starter-redis;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-keyvault;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-keyvault-certificates;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-keyvault-secrets;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-openai;4.9.0-beta.1;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-servicebus-jms;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-servicebus;4.8.0;4.9.0-beta.1
com.azure.spring:spring-cloud-azure-starter-storage;4.8.0;4.9.0-beta.1
Expand Down
5 changes: 5 additions & 0 deletions sdk/boms/spring-cloud-azure-dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@
<artifactId>spring-cloud-azure-starter-keyvault-certificates</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-openai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-servicebus</artifactId>
Expand Down
12 changes: 12 additions & 0 deletions sdk/spring/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ parameters:
displayName: 'spring-cloud-azure-starter-keyvault-secrets'
type: boolean
default: true
- name: release_springcloudazurestarteropenai
displayName: 'spring-cloud-azure-starter-openai'
type: boolean
default: true
- name: release_springcloudazurestarterservicebus
displayName: 'spring-cloud-azure-starter-servicebus'
type: boolean
Expand Down Expand Up @@ -416,6 +420,14 @@ extends:
skipUpdatePackageJson: true
skipVerifyChangelog: true
releaseInBatch: ${{ parameters.release_springcloudazurestarterkeyvaultsecrets }}
- name: spring-cloud-azure-starter-openai
groupId: com.azure.spring
safeName: springcloudazurestarteropenai
skipPublishDocGithubIo: true
skipPublishDocMs: true
skipUpdatePackageJson: true
skipVerifyChangelog: true
releaseInBatch: ${{ parameters.release_springcloudazurestarteropenai }}
- name: spring-cloud-azure-starter-servicebus
groupId: com.azure.spring
safeName: springcloudazurestarterservicebus
Expand Down
2 changes: 2 additions & 0 deletions sdk/spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<module>spring-cloud-azure-starter-keyvault</module>
<module>spring-cloud-azure-starter-keyvault-certificates</module>
<module>spring-cloud-azure-starter-keyvault-secrets</module>
<module>spring-cloud-azure-starter-openai</module>
<module>spring-cloud-azure-starter-servicebus-jms</module>
<module>spring-cloud-azure-starter-servicebus</module>
<module>spring-cloud-azure-starter-storage</module>
Expand Down Expand Up @@ -99,6 +100,7 @@
<module>spring-cloud-azure-starter-keyvault</module>
<module>spring-cloud-azure-starter-keyvault-certificates</module>
<module>spring-cloud-azure-starter-keyvault-secrets</module>
<module>spring-cloud-azure-starter-openai</module>
<module>spring-cloud-azure-starter-servicebus-jms</module>
<module>spring-cloud-azure-starter-servicebus</module>
<module>spring-cloud-azure-starter-storage</module>
Expand Down
6 changes: 6 additions & 0 deletions sdk/spring/spring-cloud-azure-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
<version>12.17.2</version> <!-- {x-version-update;com.azure:azure-storage-queue;dependency} -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-ai-openai</artifactId>
<version>1.0.0-beta.2</version> <!-- {x-version-update;com.azure:azure-ai-openai;dependency} -->
<optional>true</optional>
</dependency>

<!-- Spring -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.implementation.openai;

import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.spring.cloud.autoconfigure.AzureServiceConfigurationBase;
import com.azure.spring.cloud.autoconfigure.condition.ConditionalOnAnyProperty;
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
import com.azure.spring.cloud.autoconfigure.implementation.openai.properties.AzureOpenAIProperties;
import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
import com.azure.spring.cloud.service.implementation.openai.OpenAIClientBuilderFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
* {@link EnableAutoConfiguration Auto-configuration} for Azure Open AI support.
*
* @since 4.9.0-beta.1
*/
@ConditionalOnClass(OpenAIClientBuilder.class)
@ConditionalOnProperty(value = "spring.cloud.azure.openai.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnAnyProperty(prefix = "spring.cloud.azure.openai", name = "endpoint")
public class AzureOpenAIAutoConfiguration extends AzureServiceConfigurationBase {

AzureOpenAIAutoConfiguration(AzureGlobalProperties azureGlobalProperties) {
super(azureGlobalProperties);
}

@Bean
@ConfigurationProperties(AzureOpenAIProperties.PREFIX)
AzureOpenAIProperties azureOpenAIProperties() {
return loadProperties(getAzureGlobalProperties(), new AzureOpenAIProperties());
}

@Bean
@ConditionalOnMissingBean
OpenAIClient openAIClient(OpenAIClientBuilder builder) {
return builder.buildClient();
}

@Bean
@ConditionalOnMissingBean
OpenAIAsyncClient openAIAsyncClient(OpenAIClientBuilder builder) {
return builder.buildAsyncClient();
}

@Bean
@ConditionalOnMissingBean
OpenAIClientBuilder openAIClientBuilder(OpenAIClientBuilderFactory factory) {
return factory.build();
}

@Bean
@ConditionalOnMissingBean
OpenAIClientBuilderFactory openAIClientBuilderFactory(AzureOpenAIProperties properties,
ObjectProvider<AzureServiceClientBuilderCustomizer<OpenAIClientBuilder>> customizers) {
OpenAIClientBuilderFactory factory = new OpenAIClientBuilderFactory(properties);
factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_OPENAI);
customizers.orderedStream().forEach(factory::addBuilderCustomizer);
return factory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.implementation.openai.properties;

import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.spring.cloud.autoconfigure.implementation.properties.core.AbstractAzureHttpConfigurationProperties;
import com.azure.spring.cloud.service.implementation.openai.OpenAIClientProperties;

/**
* Configuration properties for Azure OpenAI.
*/
public class AzureOpenAIProperties extends AbstractAzureHttpConfigurationProperties implements OpenAIClientProperties {

public static final String PREFIX = "spring.cloud.azure.openai";

/**
* Endpoint of the Azure OpenAI. For instance, 'https://{azure-openai-name}.openai.azure.com/'.
*/
private String endpoint;

/**
* Azure OpenAI service version used when making API requests.
*/
private OpenAIServiceVersion serviceVersion;

/**
* The API key to authenticate the non-Azure OpenAI service (https://platform.openai.com/docs/api-reference/authentication).
*/
private String nonAzureOpenAIKey;

/**
* Key to authenticate for accessing the Azure OpenAI.
*/
private String key;

public String getEndpoint() {
return endpoint;
}

public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}

public OpenAIServiceVersion getServiceVersion() {
return serviceVersion;
}

public void setServiceVersion(OpenAIServiceVersion serviceVersion) {
this.serviceVersion = serviceVersion;
}

@Override
public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

@Override
public String getNonAzureOpenAIKey() {
return nonAzureOpenAIKey;
}

public void setNonAzureOpenAIKey(String nonAzureOpenAIKey) {
this.nonAzureOpenAIKey = nonAzureOpenAIKey;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ com.azure.spring.cloud.autoconfigure.kafka.AzureEventHubsKafkaOAuth2AutoConfigur
com.azure.spring.cloud.autoconfigure.kafka.AzureEventHubsKafkaBinderOAuth2AutoConfiguration,\
com.azure.spring.cloud.autoconfigure.keyvault.secrets.AzureKeyVaultSecretAutoConfiguration,\
com.azure.spring.cloud.autoconfigure.keyvault.certificates.AzureKeyVaultCertificateAutoConfiguration,\
com.azure.spring.cloud.autoconfigure.implementation.openai.AzureOpenAIAutoConfiguration,\
com.azure.spring.cloud.autoconfigure.servicebus.AzureServiceBusAutoConfiguration,\
com.azure.spring.cloud.autoconfigure.servicebus.AzureServiceBusMessagingAutoConfiguration,\
com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration,\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.implementation.openai;

import com.azure.ai.openai.OpenAIServiceVersion;
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.cosmos.CosmosClientBuilder;
import com.azure.spring.cloud.autoconfigure.AbstractAzureServiceConfigurationTests;
import com.azure.spring.cloud.autoconfigure.TestBuilderCustomizer;
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
import com.azure.spring.cloud.autoconfigure.implementation.openai.properties.AzureOpenAIProperties;
import com.azure.spring.cloud.service.implementation.openai.OpenAIClientBuilderFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;

class AzureOpenAIAutoConfigurationTests extends AbstractAzureServiceConfigurationTests<
OpenAIClientBuilderFactory, AzureOpenAIProperties> {

static final String TEST_ENDPOINT_HTTPS = "https://test.openai.azure.com/";
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AzureOpenAIAutoConfiguration.class));

@Override
protected ApplicationContextRunner getMinimalContextRunner() {
return this.contextRunner
.withPropertyValues("spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS);
}

@Override
protected String getPropertyPrefix() {
return AzureOpenAIProperties.PREFIX;
}

@Override
protected Class<OpenAIClientBuilderFactory> getBuilderFactoryType() {
return OpenAIClientBuilderFactory.class;
}

@Override
protected Class<AzureOpenAIProperties> getConfigurationPropertiesType() {
return AzureOpenAIProperties.class;
}

@Test
void configureWithoutOpenAIClientBuilder() {
this.contextRunner
.withPropertyValues("spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS)
.withClassLoader(new FilteredClassLoader(OpenAIClientBuilder.class))
.run(context -> assertThat(context).doesNotHaveBean(AzureOpenAIAutoConfiguration.class));
}

@Test
void configureWithOpenAIDisabled() {
this.contextRunner
.withPropertyValues(
"spring.cloud.azure.openai.enabled=false",
"spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS)
.run(context -> assertThat(context).doesNotHaveBean(AzureOpenAIAutoConfiguration.class));
}

@Test
void configureWithoutEndpoint() {
this.contextRunner
.run(context -> assertThat(context).doesNotHaveBean(AzureOpenAIAutoConfiguration.class));
}

@Test
void configureWithEndpoint() {
this.contextRunner
.withPropertyValues("spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS)
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
.withBean(OpenAIClientBuilder.class, () -> mock(OpenAIClientBuilder.class))
.run(context -> {
assertThat(context).hasSingleBean(AzureOpenAIAutoConfiguration.class);
assertThat(context).hasSingleBean(AzureOpenAIProperties.class);
assertThat(context).hasSingleBean(OpenAIClientBuilderFactory.class);
assertThat(context).hasSingleBean(OpenAIClientBuilder.class);
assertThat(context).hasSingleBean(OpenAIClient.class);
assertThat(context).hasSingleBean(OpenAIAsyncClient.class);
});
}

@Test
void customizerShouldBeCalled() {
OpenAIBuilderCustomizer customizer = new OpenAIBuilderCustomizer();
this.contextRunner
.withPropertyValues("spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS)
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
.withBean("customizer1", OpenAIBuilderCustomizer.class, () -> customizer)
.withBean("customizer2", OpenAIBuilderCustomizer.class, () -> customizer)
.run(context -> assertThat(customizer.getCustomizedTimes()).isEqualTo(2)
);
}

@Test
void otherCustomizerShouldNotBeCalled() {
OpenAIBuilderCustomizer customizer = new OpenAIBuilderCustomizer();
OtherBuilderCustomizer otherCustomizer = new OtherBuilderCustomizer();
this.contextRunner
.withPropertyValues("spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS)
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
.withBean("customizer1", OpenAIBuilderCustomizer.class, () -> customizer)
.withBean("customizer2", OpenAIBuilderCustomizer.class, () -> customizer)
.withBean("customizer3", OtherBuilderCustomizer.class, () -> otherCustomizer)
.run(context -> {
assertThat(customizer.getCustomizedTimes()).isEqualTo(2);
assertThat(otherCustomizer.getCustomizedTimes()).isEqualTo(0);
});
}

@Test
void configurationPropertiesShouldBind() {
String azureKeyCredential = "azure-key-credential";
String nonAzureOpenAIKeyCredential = "non-azure-key-credential";
this.contextRunner
.withPropertyValues(
"spring.cloud.azure.openai.endpoint=" + TEST_ENDPOINT_HTTPS,
"spring.cloud.azure.openai.key=" + azureKeyCredential,
"spring.cloud.azure.openai.non-azure-openai-key=" + nonAzureOpenAIKeyCredential,
"spring.cloud.azure.openai.service-version=v2022_12_01",
"spring.cloud.azure.credential.client-id=openai-client-id"
)
.withBean(AzureGlobalProperties.class, AzureGlobalProperties::new)
.withBean(OpenAIClientBuilder.class, () -> mock(OpenAIClientBuilder.class))
.run(context -> {
assertThat(context).hasSingleBean(AzureOpenAIProperties.class);
AzureOpenAIProperties properties = context.getBean(AzureOpenAIProperties.class);
assertEquals(TEST_ENDPOINT_HTTPS, properties.getEndpoint());
assertEquals(azureKeyCredential, properties.getKey());
assertEquals(nonAzureOpenAIKeyCredential, properties.getNonAzureOpenAIKey());
assertEquals(OpenAIServiceVersion.V2022_12_01, properties.getServiceVersion());
assertEquals("openai-client-id", properties.getCredential().getClientId());
});
}

private static class OpenAIBuilderCustomizer extends TestBuilderCustomizer<OpenAIClientBuilder> {

}

private static class OtherBuilderCustomizer extends TestBuilderCustomizer<CosmosClientBuilder> {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ private AzureSpringIdentifier() {
public static final String AZURE_SPRING_KEY_VAULT_CERTIFICATES = "az-sp-kv-ct/" + VERSION;

public static final String AZURE_SPRING_MYSQL_OAUTH = "az-sp-mysql/" + VERSION;
public static final String AZURE_SPRING_OPENAI = "az-sp-opai/" + VERSION;
public static final String AZURE_SPRING_POSTGRESQL_OAUTH = "az-sp-psql/" + VERSION;

/**
Expand Down
Loading

0 comments on commit ae3e7c8

Please sign in to comment.