Skip to content

Commit 15df47d

Browse files
jduboistzolov
authored andcommitted
feat: add official OpenAI Java SDK integration (#4688)
Introduces support for the official OpenAI Java SDK (openai-java) as an alternative to the existing Spring AI OpenAI implementation. New modules: - models/spring-ai-openai-sdk: Core SDK implementation - auto-configurations/models/spring-ai-autoconfigure-model-openai-sdk: Auto-configuration - spring-ai-spring-boot-starters/spring-ai-starter-model-openai-sdk: Starter module Features: - OpenAiSdkChatModel/OpenAiSdkChatOptions with streaming, tool calling, and multimodal support - OpenAiSdkEmbeddingModel/OpenAiSdkEmbeddingOptions with configurable dimensions - OpenAiSdkImageModel/OpenAiSdkImageOptions with DALL-E integration - Native support for OpenAI, Microsoft Foundry (Azure), and GitHub Models - Auto-detection of service provider based on base URL - Passwordless authentication for Microsoft Foundry (via azure-identity) - Comprehensive test suite with integration tests - Complete documentation for Chat, Embedding, and Image models - Update AiProvider enum to include OPENAI_SDK - Update SpringAIModels constants - Update documentation navigation and model comparison table Fix #3368 Co-authored-by: Christian Tzolov <[email protected]> Signed-off-by: Julien Dubois <[email protected]> Signed-off-by: Christian Tzolov <[email protected]>
1 parent 89a3b32 commit 15df47d

File tree

66 files changed

+15197
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+15197
-2
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ target
33
.classpath
44
.project
55
.settings
6+
.env
67
bin
78
build.log
89
integration-repo
@@ -55,4 +56,3 @@ tmp
5556

5657

5758
plans
58-

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public Function<Request, Response> weatherFunction() {
118118
@Bean
119119
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction3() {
120120
MockWeatherService weatherService = new MockWeatherService();
121-
return (weatherService::apply);
121+
return weatherService::apply;
122122
}
123123

124124
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai-parent</artifactId>
9+
<version>2.0.0-SNAPSHOT</version>
10+
<relativePath>../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-model-openai-sdk</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI OpenAI SDK Auto Configuration</name>
15+
<description>Spring AI OpenAI SDK Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>[email protected]:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
25+
<dependencies>
26+
27+
<!-- Spring AI dependencies -->
28+
29+
<dependency>
30+
<groupId>org.springframework.ai</groupId>
31+
<artifactId>spring-ai-openai-sdk</artifactId>
32+
<version>${project.parent.version}</version>
33+
</dependency>
34+
35+
<!-- Spring AI auto configurations -->
36+
37+
<dependency>
38+
<groupId>org.springframework.ai</groupId>
39+
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
40+
<version>${project.parent.version}</version>
41+
</dependency>
42+
43+
<dependency>
44+
<groupId>org.springframework.ai</groupId>
45+
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
46+
<version>${project.parent.version}</version>
47+
</dependency>
48+
49+
<dependency>
50+
<groupId>org.springframework.ai</groupId>
51+
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
52+
<version>${project.parent.version}</version>
53+
</dependency>
54+
55+
<dependency>
56+
<groupId>org.springframework.ai</groupId>
57+
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
58+
<version>${project.parent.version}</version>
59+
</dependency>
60+
61+
<!-- Boot dependencies -->
62+
<dependency>
63+
<groupId>org.springframework.boot</groupId>
64+
<artifactId>spring-boot-starter</artifactId>
65+
<optional>true</optional>
66+
</dependency>
67+
68+
<dependency>
69+
<groupId>org.springframework.boot</groupId>
70+
<artifactId>spring-boot-configuration-processor</artifactId>
71+
<optional>true</optional>
72+
</dependency>
73+
74+
<dependency>
75+
<groupId>org.springframework.boot</groupId>
76+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
77+
<optional>true</optional>
78+
</dependency>
79+
80+
<!-- Non Spring Boot dependencies -->
81+
82+
<dependency>
83+
<groupId>org.jetbrains.kotlin</groupId>
84+
<artifactId>kotlin-reflect</artifactId>
85+
<optional>true</optional>
86+
</dependency>
87+
88+
<!-- Test dependencies -->
89+
<dependency>
90+
<groupId>org.springframework.ai</groupId>
91+
<artifactId>spring-ai-test</artifactId>
92+
<version>${project.parent.version}</version>
93+
<scope>test</scope>
94+
</dependency>
95+
96+
<dependency>
97+
<groupId>org.springframework.ai</groupId>
98+
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
99+
<version>${project.parent.version}</version>
100+
<scope>test</scope>
101+
</dependency>
102+
103+
<dependency>
104+
<groupId>org.springframework.boot</groupId>
105+
<artifactId>spring-boot-starter-test</artifactId>
106+
<scope>test</scope>
107+
</dependency>
108+
109+
<dependency>
110+
<groupId>org.mockito</groupId>
111+
<artifactId>mockito-core</artifactId>
112+
<scope>test</scope>
113+
</dependency>
114+
</dependencies>
115+
116+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.openaisdk.autoconfigure;
18+
19+
import org.springframework.ai.openaisdk.AbstractOpenAiSdkOptions;
20+
import org.springframework.util.StringUtils;
21+
22+
public final class OpenAiSdkAutoConfigurationUtil {
23+
24+
private OpenAiSdkAutoConfigurationUtil() {
25+
// Avoids instantiation
26+
}
27+
28+
public static ResolvedConnectionProperties resolveConnectionProperties(AbstractOpenAiSdkOptions commonProperties,
29+
AbstractOpenAiSdkOptions modelProperties) {
30+
31+
var resolved = new ResolvedConnectionProperties();
32+
33+
resolved.setBaseUrl(StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl()
34+
: commonProperties.getBaseUrl());
35+
36+
resolved.setApiKey(StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey()
37+
: commonProperties.getApiKey());
38+
39+
String organizationId = StringUtils.hasText(modelProperties.getOrganizationId())
40+
? modelProperties.getOrganizationId() : commonProperties.getOrganizationId();
41+
resolved.setOrganizationId(organizationId);
42+
43+
resolved.setCredential(modelProperties.getCredential() != null ? modelProperties.getCredential()
44+
: commonProperties.getCredential());
45+
46+
resolved.setTimeout(
47+
modelProperties.getTimeout() != null ? modelProperties.getTimeout() : commonProperties.getTimeout());
48+
49+
resolved.setModel(StringUtils.hasText(modelProperties.getModel()) ? modelProperties.getModel()
50+
: commonProperties.getModel());
51+
52+
resolved.setMicrosoftDeploymentName(StringUtils.hasText(modelProperties.getMicrosoftDeploymentName())
53+
? modelProperties.getMicrosoftDeploymentName() : commonProperties.getMicrosoftDeploymentName());
54+
55+
resolved.setMicrosoftFoundryServiceVersion(modelProperties.getMicrosoftFoundryServiceVersion() != null
56+
? modelProperties.getMicrosoftFoundryServiceVersion()
57+
: commonProperties.getMicrosoftFoundryServiceVersion());
58+
59+
// For boolean properties, use modelProperties value, defaulting to
60+
// commonProperties if needed
61+
resolved.setMicrosoftFoundry(modelProperties.isMicrosoftFoundry() || commonProperties.isMicrosoftFoundry());
62+
63+
resolved.setGitHubModels(modelProperties.isGitHubModels() || commonProperties.isGitHubModels());
64+
65+
resolved.setMaxRetries(modelProperties.getMaxRetries() != null ? modelProperties.getMaxRetries()
66+
: commonProperties.getMaxRetries());
67+
68+
resolved
69+
.setProxy(modelProperties.getProxy() != null ? modelProperties.getProxy() : commonProperties.getProxy());
70+
71+
resolved.setCustomHeaders(modelProperties.getCustomHeaders() != null ? modelProperties.getCustomHeaders()
72+
: commonProperties.getCustomHeaders());
73+
74+
return resolved;
75+
}
76+
77+
public static class ResolvedConnectionProperties extends AbstractOpenAiSdkOptions {
78+
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.openaisdk.autoconfigure;
18+
19+
import com.openai.client.OpenAIClient;
20+
import com.openai.client.OpenAIClientAsync;
21+
import io.micrometer.observation.ObservationRegistry;
22+
23+
import org.springframework.ai.chat.observation.ChatModelObservationConvention;
24+
import org.springframework.ai.model.SpringAIModelProperties;
25+
import org.springframework.ai.model.SpringAIModels;
26+
import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate;
27+
import org.springframework.ai.model.tool.ToolCallingManager;
28+
import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate;
29+
import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
30+
import org.springframework.ai.openaisdk.AbstractOpenAiSdkOptions;
31+
import org.springframework.ai.openaisdk.OpenAiSdkChatModel;
32+
import org.springframework.ai.openaisdk.setup.OpenAiSdkSetup;
33+
import org.springframework.beans.factory.ObjectProvider;
34+
import org.springframework.boot.autoconfigure.AutoConfiguration;
35+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
36+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
37+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
38+
import org.springframework.context.annotation.Bean;
39+
40+
/**
41+
* Chat {@link AutoConfiguration Auto-configuration} for OpenAI SDK.
42+
*
43+
* @author Christian Tzolov
44+
*/
45+
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
46+
@EnableConfigurationProperties({ OpenAiSdkConnectionProperties.class, OpenAiSdkChatProperties.class })
47+
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OPENAI_SDK,
48+
matchIfMissing = true)
49+
public class OpenAiSdkChatAutoConfiguration {
50+
51+
@Bean
52+
@ConditionalOnMissingBean
53+
public OpenAiSdkChatModel openAiChatModel(OpenAiSdkConnectionProperties commonProperties,
54+
OpenAiSdkChatProperties chatProperties, ToolCallingManager toolCallingManager,
55+
ObjectProvider<ObservationRegistry> observationRegistry,
56+
ObjectProvider<ChatModelObservationConvention> observationConvention,
57+
ObjectProvider<ToolExecutionEligibilityPredicate> openAiToolExecutionEligibilityPredicate) {
58+
59+
OpenAiSdkAutoConfigurationUtil.ResolvedConnectionProperties resolvedConnectionProperties = OpenAiSdkAutoConfigurationUtil
60+
.resolveConnectionProperties(commonProperties, chatProperties);
61+
62+
OpenAIClient openAIClient = this.openAiClient(resolvedConnectionProperties);
63+
64+
OpenAIClientAsync openAIClientAsync = this.openAiClientAsync(resolvedConnectionProperties);
65+
66+
var chatModel = new OpenAiSdkChatModel(openAIClient, openAIClientAsync, chatProperties.getOptions(),
67+
toolCallingManager, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
68+
openAiToolExecutionEligibilityPredicate.getIfUnique(DefaultToolExecutionEligibilityPredicate::new));
69+
70+
observationConvention.ifAvailable(chatModel::setObservationConvention);
71+
72+
return chatModel;
73+
}
74+
75+
private OpenAIClient openAiClient(AbstractOpenAiSdkOptions resolved) {
76+
77+
return OpenAiSdkSetup.setupSyncClient(resolved.getBaseUrl(), resolved.getApiKey(), resolved.getCredential(),
78+
resolved.getMicrosoftDeploymentName(), resolved.getMicrosoftFoundryServiceVersion(),
79+
resolved.getOrganizationId(), resolved.isMicrosoftFoundry(), resolved.isGitHubModels(),
80+
resolved.getModel(), resolved.getTimeout(), resolved.getMaxRetries(), resolved.getProxy(),
81+
resolved.getCustomHeaders());
82+
}
83+
84+
private OpenAIClientAsync openAiClientAsync(AbstractOpenAiSdkOptions resolved) {
85+
86+
return OpenAiSdkSetup.setupAsyncClient(resolved.getBaseUrl(), resolved.getApiKey(), resolved.getCredential(),
87+
resolved.getMicrosoftDeploymentName(), resolved.getMicrosoftFoundryServiceVersion(),
88+
resolved.getOrganizationId(), resolved.isMicrosoftFoundry(), resolved.isGitHubModels(),
89+
resolved.getModel(), resolved.getTimeout(), resolved.getMaxRetries(), resolved.getProxy(),
90+
resolved.getCustomHeaders());
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.openaisdk.autoconfigure;
18+
19+
import org.springframework.ai.openaisdk.AbstractOpenAiSdkOptions;
20+
import org.springframework.ai.openaisdk.OpenAiSdkChatOptions;
21+
import org.springframework.boot.context.properties.ConfigurationProperties;
22+
import org.springframework.boot.context.properties.NestedConfigurationProperty;
23+
24+
/**
25+
* OpenAI SDK Chat autoconfiguration properties.
26+
*
27+
* @author Christian Tzolov
28+
*/
29+
@ConfigurationProperties(OpenAiSdkChatProperties.CONFIG_PREFIX)
30+
public class OpenAiSdkChatProperties extends AbstractOpenAiSdkOptions {
31+
32+
public static final String CONFIG_PREFIX = "spring.ai.openai-sdk.chat";
33+
34+
public static final String DEFAULT_CHAT_MODEL = OpenAiSdkChatOptions.DEFAULT_CHAT_MODEL;
35+
36+
private static final Double DEFAULT_TEMPERATURE = 1.0;
37+
38+
@NestedConfigurationProperty
39+
private final OpenAiSdkChatOptions options = OpenAiSdkChatOptions.builder()
40+
.model(DEFAULT_CHAT_MODEL)
41+
.temperature(DEFAULT_TEMPERATURE)
42+
.build();
43+
44+
public OpenAiSdkChatOptions getOptions() {
45+
return this.options;
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.openaisdk.autoconfigure;
18+
19+
import org.springframework.ai.openaisdk.AbstractOpenAiSdkOptions;
20+
import org.springframework.boot.context.properties.ConfigurationProperties;
21+
22+
@ConfigurationProperties(OpenAiSdkConnectionProperties.CONFIG_PREFIX)
23+
public class OpenAiSdkConnectionProperties extends AbstractOpenAiSdkOptions {
24+
25+
public static final String CONFIG_PREFIX = "spring.ai.openai-sdk";
26+
27+
}

0 commit comments

Comments
 (0)