Skip to content

Commit bdd0aa1

Browse files
magware-devsobychacko
authored andcommitted
Add AzureOpenAIClientBuilderCustomizer interface
Introduces a new customization point for Azure OpenAI client configuration through the AzureOpenAIClientBuilderCustomizer interface. This allows applications to customize the OpenAIClientBuilder while preserving the default auto-configuration behavior. - Add AzureOpenAIClientBuilderCustomizer interface (since 1.0.0-M6) - Modify all OpenAIClientBuilder bean creation methods to apply customizers - Add integration test verifying ordered customizer application Signed-off-by: Manuel Andreo Garcia <manuel@magware.dev> Signed-off-by: Soby Chacko <soby.chacko@broadcom.com>
1 parent e92616b commit bdd0aa1

File tree

3 files changed

+61
-5
lines changed

3 files changed

+61
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.springframework.ai.autoconfigure.azure.openai;
2+
3+
import com.azure.ai.openai.OpenAIClientBuilder;
4+
5+
/**
6+
* Callback interface that can be implemented by beans wishing to customize the
7+
* {@link OpenAIClientBuilder} whilst retaining the default auto-configuration.
8+
*
9+
* @author Manuel Andreo Garcia
10+
* @since 1.0.0-M6
11+
*/
12+
@FunctionalInterface
13+
public interface AzureOpenAIClientBuilderCustomizer {
14+
15+
/**
16+
* Customize the {@link OpenAIClientBuilder}.
17+
* @param clientBuilder the {@link OpenAIClientBuilder} to customize
18+
*/
19+
void customize(OpenAIClientBuilder clientBuilder);
20+
21+
}

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
*
5555
* @author Piotr Olaszewski
5656
* @author Soby Chacko
57+
* @author Manuel Andreo Garcia
5758
*/
5859
@AutoConfiguration
5960
@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class })
@@ -66,7 +67,9 @@ public class AzureOpenAiAutoConfiguration {
6667

6768
@Bean
6869
@ConditionalOnMissingBean // ({ OpenAIClient.class, TokenCredential.class })
69-
public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties connectionProperties) {
70+
public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties connectionProperties,
71+
ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
72+
7073
if (StringUtils.hasText(connectionProperties.getApiKey())) {
7174

7275
Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
@@ -77,17 +80,21 @@ public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties c
7780
.map(entry -> new Header(entry.getKey(), entry.getValue()))
7881
.collect(Collectors.toList());
7982
ClientOptions clientOptions = new ClientOptions().setApplicationId(APPLICATION_ID).setHeaders(headers);
80-
return new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
83+
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
8184
.credential(new AzureKeyCredential(connectionProperties.getApiKey()))
8285
.clientOptions(clientOptions);
86+
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
87+
return clientBuilder;
8388
}
8489

8590
// Connect to OpenAI (e.g. not the Azure OpenAI). The deploymentName property is
8691
// used as OpenAI model name.
8792
if (StringUtils.hasText(connectionProperties.getOpenAiApiKey())) {
88-
return new OpenAIClientBuilder().endpoint("https://api.openai.com/v1")
93+
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint("https://api.openai.com/v1")
8994
.credential(new KeyCredential(connectionProperties.getOpenAiApiKey()))
9095
.clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
96+
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
97+
return clientBuilder;
9198
}
9299

93100
throw new IllegalArgumentException("Either API key or OpenAI API key must not be empty");
@@ -97,14 +104,16 @@ public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties c
97104
@ConditionalOnMissingBean
98105
@ConditionalOnBean(TokenCredential.class)
99106
public OpenAIClientBuilder openAIClientWithTokenCredential(AzureOpenAiConnectionProperties connectionProperties,
100-
TokenCredential tokenCredential) {
107+
TokenCredential tokenCredential, ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
101108

102109
Assert.notNull(tokenCredential, "TokenCredential must not be null");
103110
Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
104111

105-
return new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
112+
OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
106113
.credential(tokenCredential)
107114
.clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
115+
applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
116+
return clientBuilder;
108117
}
109118

110119
@Bean
@@ -169,4 +178,9 @@ public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(Ope
169178
return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions());
170179
}
171180

181+
private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder,
182+
ObjectProvider<AzureOpenAIClientBuilderCustomizer> customizers) {
183+
customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder));
184+
}
185+
172186
}

spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiAutoConfigurationIT.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.net.URI;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.concurrent.atomic.AtomicBoolean;
2324
import java.util.stream.Collectors;
2425

2526
import com.azure.ai.openai.OpenAIClient;
@@ -33,6 +34,9 @@
3334
import com.azure.core.http.HttpResponse;
3435
import org.junit.jupiter.api.Test;
3536
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
37+
38+
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAIClientBuilderCustomizer;
39+
3640
import reactor.core.publisher.Flux;
3741

3842
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
@@ -59,6 +63,7 @@
5963
* @author Christian Tzolov
6064
* @author Piotr Olaszewski
6165
* @author Soby Chacko
66+
* @author Manuel Andreo Garcia
6267
* @since 0.8.0
6368
*/
6469
@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+")
@@ -228,4 +233,20 @@ void audioTranscriptionActivation() {
228233
.run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty());
229234
}
230235

236+
@Test
237+
void openAIClientBuilderCustomizer() {
238+
AtomicBoolean firstCustomizationApplied = new AtomicBoolean(false);
239+
AtomicBoolean secondCustomizationApplied = new AtomicBoolean(false);
240+
this.contextRunner
241+
.withBean("first", AzureOpenAIClientBuilderCustomizer.class,
242+
() -> clientBuilder -> firstCustomizationApplied.set(true))
243+
.withBean("second", AzureOpenAIClientBuilderCustomizer.class,
244+
() -> clientBuilder -> secondCustomizationApplied.set(true))
245+
.run(context -> {
246+
context.getBean(OpenAIClientBuilder.class);
247+
assertThat(firstCustomizationApplied.get()).isTrue();
248+
assertThat(secondCustomizationApplied.get()).isTrue();
249+
});
250+
}
251+
231252
}

0 commit comments

Comments
 (0)